Skip to content

Time-based subscription token with recurring payment approvals and auto-deletion on expiry

Subscription

Instruction text for agents that use the Subscription skill, loaded by bb dev skills subscription and the MCP get_skill_instructions tool.

Try it

Paste this into Claude Code, Codex, or Cursor with the BitBadges MCP server wired. The agent loads the skill, builds, verifies, and hands back a link to review and sign.

text
Load the subscription skill and build me one: time-based subscription token with recurring payment approvals and auto-deletion on expiry. Use the session tools, run validate, review, and simulate in parallel, fix anything critical, then call get_review_url and give me the link.

From a shell, bb dev skills subscription prints the same instructions.

Summary

Required standards: ["Subscriptions"]

  • validTokenIds: must be exactly one token ID [{ "start": "1", "end": "1" }]
  • Subscription faucet approval requirements:
    • fromListId: "Mint"
    • overridesFromOutgoingApprovals: true
    • coinTransfers: at least 1 entry, both override flags false
    • predeterminedBalances.incrementedBalances.durationFromTimestamp: must be non-zero (duration in ms)
    • allowOverrideTimestamp: must be true
    • incrementTokenIdsBy: "0", incrementOwnershipTimesBy: "0"
    • orderCalculationMethod: must have exactly one method true (default: useOverallNumTransfers)
  • Duration constants: monthly = "2592000000", annual = "31536000000", daily = "86400000"
  • Critical: recurringOwnershipTimes must be all-zeros { startTime: "0", intervalLength: "0", chargePeriodLength: "0" }, chain enforces mutual exclusivity with durationFromTimestamp

Instructions

Subscription Collection Configuration

When creating a subscription collection, you must follow these exact requirements:

Preferred path: preset (one short tool call)

The faucet approval is fully canonical. Use subscription.faucet:

ts
add_preset_approval({
  presetId: "subscription.faucet",
  params: {
    paymentRecipient: "bb1...",
    paymentDenom: "<ibc/... or native>",
    paymentAmount: "<base units>",
    durationMs: "2592000000"   // monthly; daily="86400000", annual="31536000000"
  }
})

list_presets({skill: "subscription"}) for params. Fall back to add_approval for exotic variants (multi-token tiers, manager-only mints, etc.).

Required Structure

  1. Standards: must include "Subscriptions"

    • "standards": ["Subscriptions"]
  2. Invariants: must set noCustomOwnershipTimes to false

    • Subscriptions use time-dependent ownership, this invariant must be false or subscriptions cannot function.
    • "invariants": { "noCustomOwnershipTimes": false, ... }
  3. validTokenIds: must be exactly one token ID (per tier)

    • "validTokenIds": [{ "start": "1", "end": "1" }]
  4. Subscription Faucet Approval Requirements:

    • fromListId: must be "Mint"
    • tokenIds: must be exactly 1 token: [{ "start": "1", "end": "1" }]
    • coinTransfers: must have at least 1 entry, no override flags (both false)
    • predeterminedBalances.incrementedBalances:
      • durationFromTimestamp: must be non-zero (subscription duration in milliseconds)
      • allowOverrideTimestamp: must be true
      • incrementTokenIdsBy: "0"
      • incrementOwnershipTimesBy: "0"
    • orderCalculationMethod: must have exactly one method set to true (default: useOverallNumTransfers)
    • overridesFromOutgoingApprovals: true (required for Mint approvals)

Duration Constants (in milliseconds)

  • Monthly: "2592000000" (30 days)
  • Annual: "31536000000" (365 days)
  • Daily: "86400000" (24 hours)

Complete Example

json
{
  "standards": ["Subscriptions"],
  "validTokenIds": [{ "start": "1", "end": "1" }],
  "collectionApprovals": [{
    "fromListId": "Mint",
    "toListId": "All",
    "initiatedByListId": "All",
    "approvalId": "subscription-mint",
    "tokenIds": [{ "start": "1", "end": "1" }],
    "transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
    "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
    "approvalCriteria": {
      "coinTransfers": [{
        "to": "bb1creator...",
        "coins": [{ "denom": "ubadge", "amount": "5000000000" }]
      }],
      "predeterminedBalances": {
        "incrementedBalances": {
          "startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
          "durationFromTimestamp": "2592000000",
          "allowOverrideTimestamp": true,
          "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
        },
        "orderCalculationMethod": {
          "useOverallNumTransfers": true
        }
      },
      "overridesFromOutgoingApprovals": true
    }
  }]
}
{
  "standards": ["Subscriptions"],
  "validTokenIds": [{ "start": "1", "end": "1" }],
  "collectionApprovals": [{
    "fromListId": "Mint",
    "toListId": "All",
    "initiatedByListId": "All",
    "approvalId": "subscription-mint",
    "tokenIds": [{ "start": "1", "end": "1" }],
    "transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
    "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
    "approvalCriteria": {
      "coinTransfers": [{
        "to": "bb1creator...",
        "coins": [{ "denom": "ubadge", "amount": "5000000000" }],
        "overrideFromWithApproverAddress": false,
        "overrideToWithInitiator": false
      }],
      "predeterminedBalances": {
        "incrementedBalances": {
          "startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
          "incrementTokenIdsBy": "0",
          "incrementOwnershipTimesBy": "0",
          "durationFromTimestamp": "2592000000",
          "allowOverrideTimestamp": true,
          "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
          "allowOverrideWithAnyValidToken": false
        },
        "orderCalculationMethod": {
          "useOverallNumTransfers": true,
          "usePerToAddressNumTransfers": false,
          "usePerFromAddressNumTransfers": false,
          "usePerInitiatedByAddressNumTransfers": false,
          "useMerkleChallengeLeafIndex": false,
          "challengeTrackerId": ""
        },
        "manualBalances": []
      },
      "overridesFromOutgoingApprovals": true,
      "merkleChallenges": []
    }
  }]
}

Subscription-Specific Gotchas

  • Must have exactly 1 token ID (not multiple)
  • coinTransfers override flags must be false (not true)
  • durationFromTimestamp must be non-zero
  • allowOverrideTimestamp must be true
  • Critical mutual exclusivity: The chain enforces that only one of durationFromTimestamp, incrementOwnershipTimesBy, or recurringOwnershipTimes can be non-zero. For subscriptions, use durationFromTimestamp and keep recurringOwnershipTimes as all zeros: { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }. do not set non-zero values in recurringOwnershipTimes, the template already has the correct structure.

Common Mistakes

  • Don't set recurringOwnershipTimes to non-zero values, it is mutually exclusive with durationFromTimestamp. Keep all fields as "0".
  • Don't forget durationFromTimestamp must be non-zero, this is the subscription duration in milliseconds (e.g. "2592000000" for 30 days).
  • Don't forget allowOverrideTimestamp: true, subscriptions need this so each mint gets its own start timestamp.
  • Don't use multiple token IDs, subscriptions must use exactly one token ID [{ "start": "1", "end": "1" }].
  • Don't set coinTransfers override flags to true, for standard subscription payments, both overrideFromWithApproverAddress and overrideToWithInitiator must be false.
  • Don't set noCustomOwnershipTimes: true in invariants, subscriptions require noCustomOwnershipTimes: false (or omit the invariant) because each subscription period mints a new ownershipTime window.

Edit this page on GitHub

For agents

How agents read these docs