# credit-token

<!-- generated by site/scripts/gen-skills.ts; do not hand-edit -->

# 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": "",
  "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.
