Skip to content

Increment-only, non-transferable credit token purchased with any ICS20 denom. Users pay X of a denom and receive Y tokens as credits/proof of payment. For a 1:1 backed token with on-chain transferability, use the Smart Token standard instead.

Credit Token

Instruction text for agents that use the Credit Token skill, loaded by bb dev skills credit-token and the MCP get_skill_instructions tool.

Reference collections on mainnet: 23.

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 credit-token skill and build me one: increment-only, non-transferable credit token purchased with any ICS20 denom. Users pay X of a denom and receive Y tokens as credits/proof of payment. For a 1:1 backed token with on-chain transferability, use the Smart Token standard instead. 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 credit-token prints the same instructions.

Summary

Required standards: ["Credit Token"]

  • Increment-only, non-transferable (soulbound) fungible token purchased with ICS20 denom
  • validTokenIds: [{ "start": "1", "end": "1" }] (single token ID)
  • One Mint approval with approvalId "credit-scaled" using allowAmountScaling (single scaled approval supersedes the legacy 8-10 tier approach; legacy tiers still supported for backward compat but deprecated)
  • Lock canUpdateCollectionApprovals (use permanentlyForbiddenTimes)
  • defaultBalances: autoApproveAllIncomingTransfers: true, autoApproveSelfInitiatedOutgoingTransfers: true, autoApproveSelfInitiatedIncomingTransfers: true
  • Credit-scaled approval: overridesFromOutgoingApprovals: true, mustPrioritize: true, coinTransfers[0].coins[0].amount = "1" (micro-payment unit)
  • Must include alias path for display
  • All permissions locked with full-range permanentlyForbiddenTimes
  • Key difference from Smart Token: one-way minting only, no backing/unbacking, no transferability

Instructions

Credit Token Configuration

Concept

A Credit Token is an increment-only, non-transferable fungible token that users purchase with an ICS20 denom (USDC, ATOM, BADGE, etc.). Tokens serve as both proof of payment and consumable credits. This is a one-way system, tokens can only be minted (incremented), never sold back, burned, or transferred between users. For a 1:1 backed token with on-chain transferability, use the Smart Token standard instead.

Payment Flow

When a user mints credit tokens, the payment (coinTransfers) goes directly to a payout address specified in the approval. The tokens are not redeemable post-mint, they are increment-only. The payout address receives the ICS20 denom immediately upon mint; there is no escrow or redemption mechanism.

Usage Pattern: totalUsed / totalCreditsPaidFor

Exact Purchase Quotes

Use bb credit-tokens quote <id> --units <integer> before bb credit-tokens purchase. Units are an integer multiplier of raw approval terms, not display credits. Quotes show exact payment and mint base amounts, recipient, ratio, and display amounts when decimal metadata is known. Unknown display metadata is null. Oversized requests are rejected, never capped silently; legacy tiers require exactly one pack. Multiple tiers require explicit --tier selection. MCP exposes the same operation as standard_credit_tokens_quote.

Quotes do not establish eligibility, reserve capacity, include fees, or authorize spending. Re-read and simulate before signing. Purchased credits are not remaining credits: the service provider owns authenticated usage accounting, and the generic quote returns remainingCredits: null.

Credit tokens are designed for systems that track consumption off-chain. The on-chain token balance represents totalCreditsPaidFor, the total credits ever purchased. An off-chain system tracks totalUsed. The remaining budget is simply balance - totalUsed.

Example: BitBadges API Credits (mainnet collection 84; local collection 23, APITOKEN)

  • User purchases 10 USDC → receives 1,000,000 APITOKEN (on-chain balance = 1,000,000)
  • User makes API calls (including the AI Builder) → backend tracks totalUsed (e.g., 250,000 APITOKEN used)
  • Remaining budget = on-chain balance (1,000,000) - totalUsed (250,000) = 750,000
  • User purchases 5 more USDC → on-chain balance increments to 1,500,000
  • Remaining budget = 1,500,000 - 250,000 = 1,250,000
  • The balance only ever goes up (increment-only). The off-chain totalUsed only ever goes up. Budget = balance - totalUsed.

Required Structure

  1. Standards: "standards": ["Credit Token"]
  2. validTokenIds: [{ "start": "1", "end": "1" }] (single fungible token ID)
  3. One scaled Mint approval (see "The Scaled Credit Approval" below), no other approvals. Credit tokens are soulbound: no transfer, no burn.
  4. defaultBalances must auto-approve all transfer directions:
    • "autoApproveAllIncomingTransfers": true
    • "autoApproveSelfInitiatedOutgoingTransfers": true
    • "autoApproveSelfInitiatedIncomingTransfers": true
  5. Alias path required for display (see below).
  6. All permissions frozen (full-range permanentlyForbiddenTimes on every permission, with the appropriate scope).

The Scaled Credit Approval (single approval: replaces tiers)

The purchase form uses amount scaling to support arbitrary purchase sizes in one approval. Users pick a quantity on the frontend; the chain scales startBalances and coinTransfers by the multiplier. Base unit of the approval: 1 micro-payment → tokensPerUnit micro-tokens.

approvalId must be exactly "credit-scaled", the frontend purchase form detects scaled approvals by this id. Both tracker ids (amountTrackerId in approvalAmounts and maxNumTransfers) must also equal "credit-scaled".

Preferred path: preset (short call, canonical shape)

The single approval's shape is fully canonical, use the preset instead of hand-writing the 60-line approvalCriteria JSON:

ts
add_preset_approval({
  presetId: "credit-token.scaled",
  params: {
    paymentDenom: "<ics20 denom - native like 'ubadge', 'uatom', or an 'ibc/...' hash>",
    paymentRecipient: "<bb1... creator or treasury address>",
    tokensPerUnit: "<base-unit ratio; see Conversion rate below>"
  }
})

Discover params + other credit-token presets with list_presets({skill: "credit-token"}). Presets route through the same validators as hand-built approvals, output is structurally identical. Use overrides for small tweaks (custom transferTimes window, etc.); for shape changes the preset doesn't cover, fall back to add_approval.

Conversion rate: tokensPerUnit is a base-unit ratio

Like every chain amount, tokensPerUnit is in base units, specifically, it's a ratio of two base-unit amounts: how many micro-tokens to mint per one micro-unit of payment. Because the preset's coinTransfer is fixed at "1" micro-payment-unit and the chain scales both sides together at purchase, the ratio you write here is the per-micro-unit mint rate directly.

Computation:

Given a user-stated rate "X display-payment-units → Y display-token-units", with payment decimals Dp and token decimals Dt:

text
tokensPerUnit = (Y × 10^Dt) / (X × 10^Dp)
             = (Y / X) × 10^(Dt - Dp)

When Dt === Dp (the common case: the frontend mint form auto-matches token decimals to payment decimals), 10^(Dt - Dp) = 1, so tokensPerUnit = Y / X, just the user's stated ratio, no zeros appended.

Worked examples (assume matching decimals):

  • "1 payment → 100 tokens" → tokensPerUnit: "100"
  • "1 payment → 1000 tokens" → tokensPerUnit: "1000"
  • "1 payment → 1 token" → tokensPerUnit: "1"
  • "5 payment → 100 tokens" → tokensPerUnit: "20" (100 / 5)

Common mistake to avoid: do not multiply by 10^Dt alone (e.g. 100 × 10^6 = 100000000). That converts display-tokens to micro-tokens but forgets that the payment side is also already in micro-units, the two conversions cancel. If a sanity check shows your number has Dp+ trailing zeros per display unit the user stated, you've double-converted.

For mismatched decimals: call lookup_token_info to confirm both decimals, then use the full formula.

Template

json
{
  "fromListId": "Mint",
  "toListId": "All",
  "initiatedByListId": "All",
  "transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
  "tokenIds": [{ "start": "1", "end": "1" }],
  "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
  "approvalId": "credit-scaled",
  "uri": "",
  "approvalCriteria": {
    "predeterminedBalances": {
      "incrementedBalances": {
        "startBalances": [{ "amount": "<tokensPerUnit>", "tokenIds": [{"start":"1","end":"1"}], "ownershipTimes": [{"start":"1","end":"18446744073709551615"}] }],
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
        "allowAmountScaling": true,
        "maxScalingMultiplier": "18446744073709551615"
      },
      "orderCalculationMethod": { "useOverallNumTransfers": true, "usePerToAddressNumTransfers": false, "usePerFromAddressNumTransfers": false, "usePerInitiatedByAddressNumTransfers": false, "useMerkleChallengeLeafIndex": false, "challengeTrackerId": "" }
    },
    "approvalAmounts": { "overallApprovalAmount": "0", "perToAddressApprovalAmount": "0", "perFromAddressApprovalAmount": "0", "perInitiatedByAddressApprovalAmount": "0", "amountTrackerId": "credit-scaled", "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" } },
    "maxNumTransfers": { "overallMaxNumTransfers": "0", "perToAddressMaxNumTransfers": "0", "perFromAddressMaxNumTransfers": "0", "perInitiatedByAddressMaxNumTransfers": "0", "amountTrackerId": "credit-scaled", "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" } },
    "coinTransfers": [{
      "to": "<payment_recipient_address>",
      "coins": [{ "amount": "1", "denom": "<ics20_denom>" }]
    }],
    "overridesFromOutgoingApprovals": true,
    "mustPrioritize": true
  }
}
{
  "fromListId": "Mint",
  "toListId": "All",
  "initiatedByListId": "All",
  "transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
  "tokenIds": [{ "start": "1", "end": "1" }],
  "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
  "approvalId": "credit-scaled",
  "uri": "",
  "customData": "",
  "version": "0",
  "approvalCriteria": {
    "predeterminedBalances": {
      "manualBalances": [],
      "incrementedBalances": {
        "startBalances": [{ "amount": "<tokensPerUnit>", "tokenIds": [{"start":"1","end":"1"}], "ownershipTimes": [{"start":"1","end":"18446744073709551615"}] }],
        "incrementTokenIdsBy": "0",
        "incrementOwnershipTimesBy": "0",
        "durationFromTimestamp": "0",
        "allowOverrideTimestamp": false,
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
        "allowOverrideWithAnyValidToken": false,
        "allowAmountScaling": true,
        "maxScalingMultiplier": "18446744073709551615"
      },
      "orderCalculationMethod": { "useOverallNumTransfers": true, "usePerToAddressNumTransfers": false, "usePerFromAddressNumTransfers": false, "usePerInitiatedByAddressNumTransfers": false, "useMerkleChallengeLeafIndex": false, "challengeTrackerId": "" }
    },
    "approvalAmounts": { "overallApprovalAmount": "0", "perToAddressApprovalAmount": "0", "perFromAddressApprovalAmount": "0", "perInitiatedByAddressApprovalAmount": "0", "amountTrackerId": "credit-scaled", "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" } },
    "maxNumTransfers": { "overallMaxNumTransfers": "0", "perToAddressMaxNumTransfers": "0", "perFromAddressMaxNumTransfers": "0", "perInitiatedByAddressMaxNumTransfers": "0", "amountTrackerId": "credit-scaled", "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" } },
    "coinTransfers": [{
      "to": "<payment_recipient_address>",
      "coins": [{ "amount": "1", "denom": "<ics20_denom>" }],
      "overrideFromWithApproverAddress": false,
      "overrideToWithInitiator": false
    }],
    "merkleChallenges": [],
    "mustOwnTokens": [],
    "overridesFromOutgoingApprovals": true,
    "overridesToIncomingApprovals": false,
    "mustPrioritize": true
  }
}

Legacy (deprecated, do not produce): older credit-token collections shipped 8-10 fixed-amount approvals with ids credit-1, credit-5, credit-10, …, credit-1000000000. The frontend still renders those for backward compatibility but new builds must use the single credit-scaled approval above. Do not mix the two.

Alias Path (required)

Must include an alias path so tokens display nicely. The alias path requires at least one denomUnits entry with decimals > 0, the chain rejects an empty or zero-decimal denom units array with "denom unit decimals cannot be 0". Pick a sensible display exponent (6 is typical for fungible tokens):

json
{
"aliasPathsToAdd": [{
  "denom": "u<symbol_lowercase>",
  "conversion": {
    "sideA": { "amount": "1" },
    "sideB": [{ "amount": "1", "ownershipTimes": [{"start":"1","end":"18446744073709551615"}], "tokenIds": [{"start":"1","end":"1"}] }]
  },
  "symbol": "<SYMBOL>",
  "denomUnits": [{ "decimals": "6", "symbol": "<SYMBOL>", "isDefaultDisplay": true, "metadata": { "uri": "ipfs://METADATA_ALIAS_<symbol_lowercase>_UNIT", "customData": "" } }],
  "metadata": { "uri": "ipfs://METADATA_ALIAS_u<symbol_lowercase>", "customData": "" }
}]
}

Permissions (All Locked)

All permissions should be locked (set permanentlyForbiddenTimes to the full range):

json
{
  "collectionPermissions": {
    "canDeleteCollection": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}]}],
    "canArchiveCollection": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}]}],
    "canUpdateStandards": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}]}],
    "canUpdateCustomData": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}]}],
    "canUpdateManager": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}]}],
    "canUpdateCollectionMetadata": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}]}],
    "canUpdateValidTokenIds": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}], "tokenIds": [{"start": "1", "end": "18446744073709551615"}]}],
    "canUpdateTokenMetadata": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}], "tokenIds": [{"start": "1", "end": "18446744073709551615"}]}],
    "canUpdateCollectionApprovals": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}], "fromListId": "All", "toListId": "All", "initiatedByListId": "All", "transferTimes": [{"start": "1", "end": "18446744073709551615"}], "tokenIds": [{"start": "1", "end": "18446744073709551615"}], "ownershipTimes": [{"start": "1", "end": "18446744073709551615"}], "approvalId": "All"}],
    "canAddMoreAliasPaths": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}]}],
    "canAddMoreCosmosCoinWrapperPaths": [{"permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [{"start": "1", "end": "18446744073709551615"}]}]
  }
}

Key Differences from Smart Token

  • Increment-only: tokens can only be minted (purchased), never redeemed, burned, or decreased
  • Non-transferable: soulbound, no peer-to-peer transfers. If users need transferability, use Smart Token instead
  • No backing/unbacking: one-way minting only, no cosmosCoinBackedPath
  • Single scaled approval: one credit-scaled approval with allowAmountScaling: true handles all purchase sizes
  • Credits never expire: ownership times cover full range

Common Mistakes

  • Don't ship the legacy credit-1 / credit-5 / credit-10 / … tier approvals on new collections, produce one approval with approvalId: "credit-scaled" instead.
  • Don't forget allowAmountScaling: true on the credit-scaled approval, without it the frontend can't scale payments.
  • Don't set coinTransfers[0].coins[0].amount to anything other than "1" on the scaled approval, the amount is the per-micro-unit rate; the frontend multiplies at purchase time.
  • Don't add transferable or burnable approvals, credit tokens are soulbound. Only the one scaled Mint approval.
  • Don't forget mustPrioritize: true, required so the scaled approval wins during purchase.
  • Don't forget the alias path, credit tokens will not display properly without one.
  • Don't use numbers instead of strings for amounts, all values must be string-encoded.
  • Don't confuse credit tokens with smart tokens, credit tokens are one-way minting only with no backing/unbacking or transferability.

Edit this page on GitHub

For agents

How agents read these docs