# BitBadges documentation, every page in one file
# Generated from the corpus at build time. Page count: 255.
# The SDK reference (/sdk/reference) and proto schema (/chain/proto) are
# generated separately and excluded here; browse them on the site.
## File: README.md
---
description: "What BitBadges is, how the chain, the hosted services, and the tools fit together, what one collection looks like, and where to start."
---
# BitBadges Documentation
Set `SIGNER` to the wallet address authorized to sign the transaction, matching its explicit creator or sender. Browser deployment requires this binding even when reading a saved transaction.
BitBadges is a Cosmos SDK Layer 1 whose core module, `x/tokenization`, is a complete token standard: every collection, balance, approval, and permission is chain state that the module enforces on every transfer. This page explains the system end to end, shows what a token collection looks like, and sends you to the right tab.
## Create with Your AI
Tell Claude what you want to build:
```text
Hey Claude, create me a 5 ATOM / month subscription.
```
With the BitBadges MCP builder connected, your AI can turn that request into a collection with payment rules and expiring membership tokens. It asks for the details, builds and checks the transaction, and returns a link where you review and sign with your wallet.
1. [Set Up Your AI](agents/setup.md): connect the builder to Claude Code, Claude Desktop, Cursor, Codex, or another MCP client.
2. [Create Your First Collection](start/first-collection.md): go from the subscription prompt to a reviewed transaction. The monthly preset uses 30-day periods; automatic renewal requires subscriber authorization.
3. Refine the result in conversation: choose the payment coin, transfer rules, supply limits, or what the manager can change.
The same creation flow works across the token standard:
```text
Create me a payment request for 10 USDC and give me the review link.
```
```text
Make a 500-piece NFT collection where only I can mint.
```
For a build flow inside your application, use the [Programmatic Agent](agents/programmatic-agent.md). For repeatable commands and direct TypeScript integration, use the [CLI](cli/README.md) and [SDK](sdk/README.md).
## Create in the Browser
On [bitbadges.io](https://bitbadges.io), you can also create collections, set transferability, run claims, and trade with a wallet. These docs explain the token model and cover integrations, automation, custom plugins, and node operations.
## What You Get
| Layer | Pieces | What it does |
| --- | --- | --- |
| Chain (`bitbadges-1`) | `x/tokenization`, `x/gamm`, `x/managersplitter`, IBC, EVM precompiles | Tokens with time-based balances and three-level transfer approvals. A Balancer-style DEX. Wrapping to `x/bank` denoms for IBC. Solidity access through precompiles at `0x...1001` to `0x...1003`. |
| Hosted services | BitBadges API, claims and plugins, Sign In with BitBadges, bitbadges.io | Indexed reads (balances, metadata, activity), off-chain claim gating with 16 built-in plugins plus your own HTTP plugins, OAuth-style sign in, and a no-code site. |
| Tools | `bb` CLI, `bitbadges` npm package, MCP builder tools, Claude Code plugin | Build, check, simulate, preview, sign, and broadcast transactions from a terminal, TypeScript, or an AI agent. |
The chain enforces the rules. The hosted services make them easy to read and to gate. The tools generate valid transactions so you rarely hand-write the structures below.
## One Collection, Annotated
A collection is created with one message. This example fixes the valid IDs at 1 through 100 and caps supply at one unit per ID. Its locked mint rule lets only the creator initiate up to 100 mints, issuing IDs in order. The manager can still edit metadata and add post-mint transfer rules, but cannot change minting or force holders to transfer their tokens.
The addresses and metadata URIs are illustrative. Replace both `creator` and `manager`, plus the mint rule's `initiatedByListId`, with your wallet address. Supply your own metadata URIs. Recipients accept incoming transfers by default and can change their own incoming approvals. The example has no post-mint transfer rule, so holders cannot transfer tokens until the manager adds one.
```json fold=3-17,19-25,31-37,62-66,75-97,107-112,114-158,166-177
{
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"defaultBalances": {
"balances": [],
"outgoingApprovals": [],
"incomingApprovals": [],
"autoApproveSelfInitiatedOutgoingTransfers": false,
"autoApproveSelfInitiatedIncomingTransfers": false,
"autoApproveAllIncomingTransfers": true,
"userPermissions": {
"canUpdateOutgoingApprovals": [],
"canUpdateIncomingApprovals": [],
"canUpdateAutoApproveSelfInitiatedOutgoingTransfers": [],
"canUpdateAutoApproveSelfInitiatedIncomingTransfers": [],
"canUpdateAutoApproveAllIncomingTransfers": []
}
},
"validTokenIds": [{ "start": "1", "end": "100" }],
"collectionPermissions": {
"canDeleteCollection": [],
"canArchiveCollection": [],
"canUpdateStandards": [],
"canUpdateCustomData": [],
"canUpdateManager": [],
"canUpdateCollectionMetadata": [],
"canUpdateValidTokenIds": [
{
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"permanentlyPermittedTimes": [],
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
],
"canUpdateTokenMetadata": [],
"canUpdateCollectionApprovals": [
{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "All",
"permanentlyPermittedTimes": [],
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
],
"canAddMoreAliasPaths": [],
"canAddMoreCosmosCoinWrapperPaths": []
},
"manager": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionMetadata": {
"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"customData": ""
},
"tokenMetadata": [
{
"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/{id}.json",
"customData": "",
"tokenIds": [{ "start": "1", "end": "100" }]
}
],
"customData": "",
"collectionApprovals": [
{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "100" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "",
"customData": "",
"approvalId": "manager-mint",
"approvalCriteria": {
"merkleChallenges": [],
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [
{
"amount": "1",
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "1" }]
}
],
"incrementTokenIdsBy": "1",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false,
"allowAmountScaling": false,
"maxScalingMultiplier": "0"
},
"orderCalculationMethod": {
"useOverallNumTransfers": true,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
}
},
"approvalAmounts": {
"overallApprovalAmount": "0",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"overallMaxNumTransfers": "100",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"perInitiatedByAddressMaxNumTransfers": "0",
"amountTrackerId": "mint",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"coinTransfers": [],
"requireToEqualsInitiatedBy": false,
"requireFromEqualsInitiatedBy": false,
"requireToDoesNotEqualInitiatedBy": false,
"requireFromDoesNotEqualInitiatedBy": false,
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": false,
"autoDeletionOptions": {
"afterOneUse": false,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
},
"mustOwnTokens": [],
"dynamicStoreChallenges": [],
"ethSignatureChallenges": [],
"senderChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"recipientChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"initiatorChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"altTimeChecks": {
"offlineHours": [],
"offlineDays": [],
"offlineMonths": [],
"offlineDaysOfMonth": [],
"offlineWeeksOfYear": [],
"timezoneOffsetMinutes": "0",
"timezoneOffsetNegative": false
},
"mustPrioritize": false,
"votingChallenges": [],
"allowBackedMinting": false,
"allowSpecialWrapping": false,
"evmQueryChallenges": [],
"userApprovalSettings": {
"allowedDenoms": [],
"disableUserCoinTransfers": false,
"userRoyalties": { "percentage": "0", "payoutAddress": "" }
}
},
"version": "0"
}
],
"standards": ["NFTs"],
"isArchived": false,
"mintEscrowCoinsToTransfer": [],
"cosmosCoinWrapperPathsToAdd": [],
"invariants": {
"noCustomOwnershipTimes": false,
"maxSupplyPerId": "1",
"noForcefulPostMintTransfers": true,
"disablePoolCreation": false,
"evmQueryChallenges": []
},
"aliasPathsToAdd": []
}
```
The Collapsed view hides default fields. Select Full to inspect the complete JSON. The field reference is in [MsgCreateCollection](token-standard/messages/msg-create-collection.md).
This object is the message value, not a transaction envelope. Save the Full JSON as `collection-value.json`, make the replacements above, then wrap it for the CLI using `jq`:
```bash
jq '{messages: [{typeUrl: "/tokenization.MsgCreateCollection", value: .}]}' collection-value.json > collection.json
bb check collection.json --depth structural
```
| Field | What it decides | Read |
| --- | --- | --- |
| `validTokenIds` | Which token IDs exist. IDs are numbers; fungible or non-fungible depends only on how many units you mint per ID. | [Collections](token-standard/concepts/collections.md) |
| `collectionMetadata`, `tokenMetadata` | Where the name, image, and description live. `{id}` expands per token. Inline JSON in `customData` needs no hosting. | [Collections](token-standard/concepts/collections.md) |
| `collectionApprovals` | Who can move which tokens from whom to whom, when, and under which conditions. `fromListId: "Mint"` makes this a mint rule. | [Transferability](token-standard/concepts/transferability.md) |
| `approvalCriteria` | The conditions: caps via trackers, a fixed mint order via predetermined balances, payments, Merkle proofs, votes, ownership checks, EVM queries, time windows. | [Approval Criteria](token-standard/approval-criteria/README.md) |
| `ownershipTimes` | Balances carry a time range. A subscription is a balance that expires; a vesting schedule is a balance that starts later. No follow-up transaction. | [Balances](token-standard/concepts/balances.md) |
| `collectionPermissions` | What the manager may still change, per time range, and whether that answer is frozen. This example locks the valid ID range and mint rules; `invariants.maxSupplyPerId` caps supply per ID. | [Permissions](token-standard/concepts/permissions.md) |
| `standards` | Labels that tell apps how to interpret the collection. | [Collections](token-standard/concepts/collections.md) |
Token transfers are checked against collection-level, sender outgoing, and recipient incoming approvals. Collection criteria can explicitly override the user approval layers. Wrapped-token movements on BitBadges also use this approval model; remote chains do not execute BitBadges approval rules. [Concepts](token-standard/concepts/README.md) walks the model in dependency order.
## What Changes Versus ERC-20 and ERC-721
| Need | Contract standards | BitBadges |
| --- | --- | --- |
| Fungible and non-fungible | Two standards, two contracts | One collection. `amount` per token ID. |
| Expiring or scheduled ownership | Custom contract, a cron job, or a burn later | `ownershipTimes` on the balance. The chain reports the balance as absent outside the range. |
| Transfer rules | `require` statements in Solidity, per contract, audited each time | Approvals with criteria, checked on token transfers and wrapped-token movements on BitBadges. |
| Allowlists and blocklists | Mappings in the contract | Reusable [Address Lists](token-standard/concepts/address-lists.md): `"All"`, `"Mint"`, `"!bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"`, or stored lists. |
| Mint gating | A merkle-drop contract | [Merkle Challenges](token-standard/approval-criteria/merkle-challenges.md) on-chain, or [Claims](api/claims/README.md) with plugins off-chain that produce the proof. |
| Royalties and payments | EIP-2981 hints that marketplaces may ignore | [Coin Transfers](token-standard/approval-criteria/coin-transfers.md) and [user royalties](token-standard/approval-criteria/user-approval-settings.md) enforced inside the transfer. |
| Upgradability | Proxy patterns | [Permissions](token-standard/concepts/permissions.md) with permitted and forbidden time ranges, freezable per field. |
| Compliance checks | Off-chain, or a per-token contract | KYC via [dynamic stores](token-standard/approval-criteria/dynamic-store-challenges.md), business hours via [Alt Time Checks](token-standard/approval-criteria/alt-time-checks.md), on-chain EVM reads via [EVM Query Challenges](token-standard/approval-criteria/evm-query-challenges.md). See [Compliance Zones](token-standard/concepts/compliance-zones.md). |
| Cross-chain | Bridges | [Wrapper paths](token-standard/ibc/cosmos-coin-wrapper-paths.md) turn tokens into `x/bank` denoms for IBC and DEX trading. Approvals apply on the BitBadges side; remote voucher transfers follow the remote chain's rules. |
| Contracts | Everything | Optional. Solidity reads and writes the module through [precompiles](chain/evm/README.md). |
The longer argument is in [Why BitBadges](about/README.md) and [Comparisons](about/comparisons.md).
## What People Build
Each row below has a longer pitch, with the fields that matter and a prompt for an agent, under [Use Cases](use-cases/README.md).
| Build | Mechanism | Start |
| --- | --- | --- |
| NFT or fungible collection | `validTokenIds` plus a mint approval | [Create a Collection](guides/create-a-collection.md) |
| Subscription, membership, expiring credential | `ownershipTimes` and recurring predetermined balances | [Subscriptions and Time-Based Tokens](guides/subscriptions-and-time-based-tokens.md) |
| Tradable, burnable, soulbound, or admin-revocable tokens | Post-mint approvals and overrides | [Set Transferability](guides/set-transferability.md) |
| Airdrop, allowlist, quest, social-gated mint | Claims with plugins that emit a Merkle proof | [Distribute with Claims](guides/distribute-with-claims.md) |
| Token-gated API or content | BB-402: a 402 response, a signed proof, a balance check | [Gate access](guides/gate-access.md) |
| Login with a wallet | Sign In with BitBadges (OAuth flow) | [Sign In Users](guides/sign-in-users.md) |
| Compliant asset with KYC and transfer limits | Dynamic stores, trackers, alt time checks, compliance zones | [Compliance Zones](token-standard/concepts/compliance-zones.md) |
| Stablecoin-backed or IBC-backed token | Backed minting against an existing denom | [Smart Tokens and Vaults](guides/smart-tokens-and-vaults.md) |
| Liquidity pool or swap | Wrap, then `x/gamm` | [Trade on the DEX](guides/trade-on-the-dex.md) |
| Auction, bounty, crowdfund, prediction market, payment request, product catalog | Standards with `bb` verbs and agent skills | [Standards](cli/standards.md), [Skills](agents/skills/README.md) |
| Agent-controlled vault with spend limits | Outgoing approvals with daily-reset trackers | [Spending Authorization](agents/spending-authorization.md) |
## Pick a Path
Choose a path for reading data or building transactions. Reads need no wallet signature. Writes require review and a signed broadcast. Mainnet is the live network; testnet is offline.
### CLI
```bash
curl -fsSL https://install.bitbadges.io | sh
bb settings set apiKey "$BITBADGES_API_KEY" # key from https://bitbadges.io/developer
bb api tokens get-collection 1 # read through the BitBadges API
bb build --help # 19 builders: subscription, smart-token, auction, transfer, ...
bb check collection.json # validate a saved collection or transaction
bb simulate collection.json
bb deploy collection.json --browser --expected-address "$SIGNER" # review and sign in the browser
```
[Quickstart](start/quickstart.md), [CLI reference](cli/README.md)
### TypeScript
```ts
import { BigIntify, BitBadgesAPI } from 'bitbadges';
const api = new BitBadgesAPI({ convertFunction: BigIntify, apiKey: process.env.BITBADGES_API_KEY });
const { collection } = await api.getCollection('1');
console.log(collection.collectionId, collection.validTokenIds);
```
Install `bitbadges` and set `BITBADGES_API_KEY` from the [developer portal](https://bitbadges.io/developer) before running this server-side example. It reads collection 1. For a write, follow [Transactions](sdk/transactions/README.md) to select a signer and prepare a transfer using tokens you own and a recipient you choose.
[SDK reference](sdk/README.md), [Transactions](sdk/transactions/README.md)
### AI Agent
Start with the CLI: discover the installed commands and standard instructions, build a proposal, then review it before requesting a signature. The SDK package also ships the optional `bitbadges-builder` MCP server. The separately installed Claude Code plugin routes agents through these same commands and instructions.
```bash
bb --help-json
bb dev skills
bb build --help
```
[Set Up Your AI](agents/setup.md), [Claude Code Plugin](agents/claude-code-plugin.md), [MCP Builder Tools](agents/mcp-tools.md)
### No Code
The [Create tab](https://bitbadges.io/create) and the [developer portal](https://bitbadges.io/developer) create collections, claims, address lists, and API keys without an integration. Paste any transaction JSON at `bitbadges.io/mint/local-builder` to review and sign it.
## Networks
| Network | Chain ID | EVM chain ID | Endpoints |
| --- | --- | --- | --- |
| mainnet | `bitbadges-1` | `50024` | `https://rpc.bitbadges.io`, `https://lcd.bitbadges.io`, `https://evm-rpc.bitbadges.io`, `https://api.bitbadges.io` |
| testnet | offline | | [Testnet status](chain/testnet.md) |
Full table, denoms, and node setup: [Network](chain/README.md).
## The Tabs
| Tab | Read it when you want to | Start at |
| --- | --- | --- |
| Docs | Install, run a first transaction, do a task, use the site, or read why BitBadges exists | [Quickstart](start/quickstart.md), [Using the Frontend](using-the-frontend/README.md), [Guides](guides/README.md), [About](about/README.md) |
| Token Standard | Understand the data model, approval criteria, every message and query, and wrapping | [Overview](token-standard/README.md) |
| Chain | The other modules, the DEX, EVM precompiles, endpoints, denoms, running a node | [Chain](chain/README.md) |
| API | Call the hosted API, run claims and plugins, add Sign In with BitBadges | [BitBadges API](api/README.md), [OpenAPI reference](/api-reference) |
| SDK & CLI | Use `bitbadges` from TypeScript or drive everything from `bb` | [SDK](sdk/README.md), [CLI](cli/README.md) |
| Agents | Wire Claude, Cursor, Codex, or any MCP client to build tokens | [Agents](agents/README.md) |
For agents reading this site: [Reading the Docs](agents/reading-the-docs.md) lists `llms.txt`, the single-file corpus, and the URL patterns.
## File: about/README.md
---
description: "Why BitBadges exists, the three theses behind it, the design decisions, and what the x/tokenization standard provides."
---
# Why BitBadges
BitBadges is a Cosmos SDK Layer 1 built around one module, `x/tokenization`, that serves as a complete token standard for RWAs, compliance, payments, and custom transferability. This page states the reasoning behind that choice. The mechanics live in the [Token Standard](../token-standard/README.md) tab.
| Page | Read it when |
| --- | --- |
| [Use Cases](use-cases.md) | You want to know what people build with the standard |
| [Comparisons](comparisons.md) | You are coming from ERC-20, ERC-3643, or another L1 |
| [BADGE Token](badge-token.md) | You need the native coin, its purposes, and the validator model |
| [Links and Resources](links.md) | You want the app, explorer, socials, brand assets, or how to contribute |
| [FAQ](faq.md) | You have a short question, including the fee schedule |
## Theses
1. The next wave of tokenization needs a new standard. Existing ones are built on older technology and are not enough.
2. Compliance and transferability are not an allowlist, a blocklist, or a transferable-versus-soulbound switch. They are a set of moving parts: time gating, ownership checks, approvals, who can send to whom, who can initiate, revocation, freezing, and more. On-chain compliance has to handle all of these automatically, and that logic belongs in the token standard, not in a manually updated list.
3. A standardized, reusable, no-code approach wins over per-use-case smart contracts over time.
The motive is the same as the theses. Blockchains and interoperability have potential that the current infrastructure and token standards cannot deliver.
## Problems with Existing Standards
ERC-20, ERC-721, CW-20, ICS-20, `x/bank`, `x/tokenfactory`, and `x/nft` cover many use cases, but the approach has structural problems:
- Too simple. Mint, transfer, and burn are not enough for most real-world applications. The industry has stayed on these primitives for more than ten years because of technical debt.
- Vulnerable by default. Each token contract is a new deployment and a new attack surface, and each needs its own audit.
- Complex and expensive. Even a basic contract needs technical knowledge to implement, deploy, and maintain.
- Low interoperability. Tokens stay siloed in one ecosystem, which splits a user base across chains. IBC is the best current answer and still leaves room to improve.
- Fragmented. Competing standards add incompatible twists, which creates confusion.
## Design Decisions
### Universality
One standard for NFTs, fungible tokens, subscriptions, quests, credentials, real-world assets, and regulatory compliance. Adapters and wrappers can expose simpler interfaces, but compatibility requires an explicit mapping of balances, ownership times, and transfer rules.
### A Module, Not Contracts
The standard is a Cosmos SDK module. Every feature is implemented once and reused by every collection. Creating a collection is a message with parameters, which is how the BitBadges site is no-code by default. The expectation is that nearly all users never write code, whatever the complexity of the use case. One reusable, tested code path replaces a new contract per token.
### Ever-Evolving
Features are added to the module as the need appears, without accruing technical debt in deployed contracts. If a use case is missing, the answer is to add it to the standard.
### IBC-First
BitBadges is Cosmos native with IBC at the core:
1. A configured [Cosmos coin wrapper path](../token-standard/ibc/cosmos-coin-wrapper-paths.md) exposes tokens as an ICS-20-compatible bank denom for supported IBC routes.
2. Payments, subscriptions, swaps, and liquidity can use any IBC denom.
3. The module is IBC-enabled, so core messages can run over IBC, including one-signature multi-hop transfers.
For example, a team can create a token on BitBadges, wrap 20% of the supply to ICS-20, send 10% to Osmosis, send 10% to an EVM chain over IBC Eureka, and keep the rest under time-dependent release in the native module. Think of BitBadges as a layer above IBC: launch on BitBadges for the features, wrap to IBC for reach.
### Value-Add over Duplication
BitBadges prioritizes utility that Cosmos does not already have. Where a service already exists in the ecosystem, the preferred path is to wrap tokens and send them there over IBC rather than rebuild it.
## What the Standard Provides
### Time-Dependent Accounting
Every balance carries ownership times, down to the millisecond. A balance is an ownership right for a token id over a time range. Bob can own token 5 until next July, at which point ownership ends with no further transaction. This supports auto-expiring and renewing subscriptions, vesting, and time-based release schedules. Recurring subscriptions combine this with a bot-tipping system for the recurring payment.
### Three Transferability Levels
1. Collection-level approvals. The manager defines the collection-wide rules and can override user-level approvals when configured to.
2. Outgoing approvals. Each sender sets rules for transfers out (for example listings).
3. Incoming approvals. Each recipient sets rules for transfers in (for example bids).
A transfer succeeds only if the sender has the balance, a collection-level approval matches, and the outgoing and incoming approvals match (unless the collection approval overrides them). Swaps and pools that execute native token transfers use the same checks. Wrapping and IBC routes must be configured explicitly; a bank-denom representation on another chain does not automatically run BitBadges approval logic.
### Approval Criteria
Every approval, on every level, can specify who can send, who can receive, who can initiate, transfer times, ownership times, predetermined or tallied amounts, the number of transfers, revocability, freezing, BADGE or other IBC coin transfers, royalties, recurrence, non-transferability, incrementing token ids, and ownership of other tokens. See [Approval Criteria](../token-standard/approval-criteria/README.md).
### Off-Chain Criteria
An oracle-like path lets a service check off-chain criteria and hand the user a signed code to redeem on-chain. BitBadges runs one such service with plugins and external integrations for criteria such as Discord membership, X followers, email, passwords and claim codes, private off-chain data, AI agents, and custom endpoints. A custom criteria service moves the off-chain evaluation trust to that service; the chain verifies the configured proof, not the underlying off-chain fact. See [Distribute with Claims](../guides/distribute-with-claims.md).
### Manager Permissions
Each collection can have a manager with fine-grained, lockable permissions: update metadata, update transferability, archive, delete, add tokens, pause transfers, throttle volume, and more. Checks and balances are enforced on-chain. See [Permissions](../token-standard/concepts/permissions.md).
### EVM and IBC Extension
The goal is that every use case works natively. Where custom logic is needed, EVM contracts call into the module through precompiles, and the module reaches other environments over IBC. See [EVM](../chain/evm/README.md).
## Related
- [Token Standard](../token-standard/README.md)
- [Quickstart](../start/quickstart.md)
- [Comparisons](comparisons.md)
## File: about/badge-token.md
---
description: "BADGE, the native coin of the BitBadges chain. Its purposes, supply and distribution, and the validator rewards and proof-of-authority model."
---
# BADGE Token
BADGE is the native gas coin of the BitBadges chain (denom `ubadge`). Read the [BADGE disclosure](https://bitbadges.io/credits-disclosure) and the other policies on the site for full disclaimers.
## Purposes
1. Gas and transaction fees.
2. Proof of stake and proof of authority. Validators bond BADGE to secure the network.
3. In-site currency. The site prioritizes more established coins such as USDC for in-site use.
4. Fee sink. The 0.1% taker fee on swaps goes to the community pool. See [FAQ](faq.md) for the fee schedule.
## Distribution
The [explorer](https://explorer.bitbadges.io) shows current supply, distribution, and validators. Read current chain state rather than treating a historical allocation as today's circulating supply.
BADGE trades on Osmosis at [app.osmosis.zone/assets/BADGE](https://app.osmosis.zone/assets/BADGE) and works with other IBC-enabled services.
## Security Model
BitBadges combines proof-of-stake validator rewards with a proof-of-authority delegation model.
### Validator Rewards and Delegations
Validator rewards, program eligibility, and delegated allocations can change independently. A delegation shown in the explorer is voting power assigned to a validator; it is not evidence that the validator owns those tokens or that a historical reward has been paid.
Use the [BADGE disclosure](https://bitbadges.io/credits-disclosure) for published program terms, and check the [explorer](https://explorer.bitbadges.io) and relevant governance proposals for current delegations and executed distributions. Do not infer circulating supply or a restriction on selling from delegated balances alone.
## Related
- [FAQ](faq.md)
- [Network](../chain/README.md)
- [Run a Node](../chain/run-a-node.md)
## File: about/comparisons.md
---
description: "How the BitBadges L1 and token standard compare to other chains and to ERC-3643, and how ERC-3643 works as an interface on BitBadges."
---
# Comparisons
BitBadges is its own Cosmos SDK Layer 1 with a token standard that works like an API: the logic is implemented once in the module, and each collection customizes it through messages. This page compares that design to other protocols and to ERC-3643 (T-REX), a standard for permissioned security tokens.
## BitBadges L1 Versus Other Protocols
### Chain Architecture
BitBadges is a Layer 1, not a Layer 2 or a sidechain. It is built with the Cosmos SDK, which provides the foundation for its configured Cosmos modules and IBC support. The native token standard has its own balances and approval model. EVM contracts can access it through precompiles; ERC-compatible wrappers require explicit integration. A native collection is not automatically an ERC-20 contract or a Bitcoin Ordinal.
### Security Model
BitBadges is less decentralized today than the largest protocols, and decentralization grows over time. The intended role is one part of an application stack rather than the whole of it. An application can use BitBadges for authentication and gating while accepting payments in its native currency.
### Cross-Chain Interoperability
IBC connects BitBadges through configured channels and supported routes. Wrapper paths expose compatible bank denoms; integration depends on the destination chain and application. See [IBC](../token-standard/ibc/README.md).
### Token Standard
Protocols such as Ethereum rely on ERC-20 and ERC-721, which need a smart contract deployment per token. That approach grows complex, adds a new attack surface per contract, and does not model the structure most use cases need. On BitBadges, everything is implemented natively and a collection customizes the requests, which is why the BitBadges site is no-code by default.
### Smart Contract Support
Ethereum and Solana have broad smart contract support. BitBadges supports EVM contracts, including contract-based token implementations. Its native collections use `x/tokenization`; contracts can access that module through precompiles. The goal remains a standard where custom contracts are never needed. See [EVM](../chain/evm/README.md).
## BitBadges and ERC-3643
ERC-3643 and BitBadges are not competing standards. They operate at different layers. The BitBadges standard enforces compliance at the protocol level, and ERC-3643 can serve as a Solidity interface that calls into it through EVM precompiles. A Solidity developer keeps the ERC-3643 API and gets protocol-level enforcement behind it.
### Overview
ERC-3643 (T-REX, Token for Regulated EXchanges) is an Ethereum standard for compliant security tokens. The [ERC-3643 Association](https://www.erc3643.org/) reports more than $32 billion in tokenized assets on its website (checked 7 September 2026); this is an association-reported adoption figure, not a measure of technical compatibility.
BitBadges enforces compliance rules, transfer restrictions, and approval logic in the chain itself. No contract deployment is needed. For EVM developers, precompiles expose the native standard through Solidity interfaces such as ERC-3643.
### Feature Comparison
| Feature | BitBadges protocol standard | ERC-3643 interface (through precompiles on BitBadges, or natively on Ethereum) |
| --- | --- | --- |
| Enforcement layer | Protocol level; the chain enforces the rules | Contract level; Solidity contracts enforce the rules |
| Smart contracts required | No; collections are configured through transaction messages | Yes; each token deploys several contracts (token, identity registry, compliance module, claim topics) |
| Deployment experience | No-code site, CLI template builders, or MCP builder tools | Developer only; needs Solidity expertise and contract deployment |
| Multi-chain support | IBC over configured, supported routes | Requires a supported cross-chain transport or wrapper |
| Identity and compliance | Built-in approval criteria: ownership requirements, merkle proofs, signature challenges, on-chain queries | ONCHAINID identity framework with claim topics and trusted issuers |
| Transfer restrictions | Per-approval rules: address lists, time windows, amount limits, tracker-based caps, 2FA gating, coin payment requirements | Compliance modules with rule contracts (country restrictions, investor limits, time locks) |
| Permissioning | Granular, lockable permissions; each field can be frozen or left manager-controlled independently | Owner and agent roles with recovery mechanisms |
| Token types | Fungible tokens, NFTs, subscriptions, vaults, prediction markets, bounties, and more from one standard | Primarily fungible equity and security tokens |
| Forced transfers | Admin override approvals | Recovery and forced transfer functions |
| Supply control | Configurable mint and burn rules with lockable permissions | Mint and burn controlled by token agents |
### ERC-3643 Strengths
- Institutional tooling: an established implementation and identity framework designed for permissioned tokens.
- Regulatory track record: purpose-built for securities compliance with established legal frameworks.
- Ecosystem maturity: identity providers, compliance modules, and institutional tooling.
- ONCHAINID: a mature decentralized identity framework for KYC and AML.
### BitBadges Strengths
- No smart contract development: a compliant token is configured through transaction parameters or the no-code site.
- Shared enforcement: native transfers are checked by the module even when initiated through a precompile. Security still depends on the module, configured approvals, manager permissions, and any application contracts.
- Broader token types: subscriptions, prediction markets, vaults, auctions, bounties, and NFTs from the same standard.
- Agent friendly: MCP builder tools and CLI template builders let AI agents create and manage compliant tokens.
- Cosmos ecosystem: IBC connectivity through supported channels and routes. Destination support and enabled channels determine where an asset can move.
- Lower barrier: no Solidity, gas optimization, or contract auditing.
### How They Work Together
Through EVM precompiles, a Solidity contract calls the native token standard with the ERC-3643 function signatures developers already know. The precompile bridges the call; the chain enforces the transfer rules, identity checks, and compliance logic, not the contract. The ERC-3643 interface is optional: the no-code site, the CLI builders, and the MCP builder tools talk to the protocol standard directly. See [Tokenization Precompile](../chain/evm/tokenization-precompile/README.md) and the [ERC-3643 specification](https://www.erc3643.org/).
## Related
- [Why BitBadges](README.md)
- [Transferability](../token-standard/concepts/transferability.md)
- [Permissions](../token-standard/concepts/permissions.md)
- [EVM](../chain/evm/README.md)
## File: about/faq.md
---
description: "Short answers on smart contracts, compliance enforcement, L1 versus L2, ERC compatibility, EVM extension, and the protocol fee schedule."
---
# FAQ
Short answers to the questions that come up most. Each answer links to the page with the detail.
## Are Smart Contracts Needed?
A collection can be created and managed through native transaction messages without deploying a custom smart contract. Use the site, CLI, or SDK for the patterns supported by the module. Applications may still need backend logic or EVM contracts for behavior outside those primitives.
## How Is Compliance Checked on Every Transfer?
The `collectionApprovals` of a collection are checked on every transfer. They define the collection-wide transferability rules. A transfer that satisfies no collection approval fails. Applications and pools that execute native token transfers use those checks. Wrapping and cross-chain movement require their own supported paths; a destination chain does not automatically execute BitBadges approval logic. See [Transferability](../token-standard/concepts/transferability.md).
## Is BitBadges an L1 or an L2?
BitBadges is its own Layer 1 built with the Cosmos SDK.
## Why a Registry Instead of a Contract per Collection?
A reusable standard exercised thousands of times beats unique, vulnerable contracts over the long run. The registry approach also improves scalability, consistency, and standardization.
## Are Tokens ERC-721, ERC-20, or ERC-3643 Compatible?
Native collections use their own token model and are not automatically ERC contracts. EVM contracts can integrate through precompiles and expose compatible interfaces; the wrapper must map the native balances and permissions to that interface. See [Comparisons](comparisons.md).
## Can the Standard Be Extended with Smart Contracts?
Yes. The token standard is a Cosmos module. The aim is that no smart contract is ever needed, but EVM environments can call into the module, and the module can call into other environments. See [EVM](../chain/evm/README.md).
## What Does the Protocol Charge? Can I Add Affiliate Fees?
BitBadges charges a 0.1% fee at the protocol level on swaps and on paid transfers:
- `x/tokenization` transfers with coin payments (`coinTransfers`): 0.1% in the payment denom.
- `x/gamm` swaps: a 0.1% taker fee.
Plain `x/tokenization` transfers without a payment, and IBC or `x/bank` sends, carry no protocol fee.
Affiliate fees are yours to design on top of that:
- `x/tokenization`: add another coin transfer to the approval (for example a 0.1% payout to the initiator).
- `x/gamm`: an affiliates field is built into the protocol. The protocol itself takes nothing from it. The BitBadges API takes a 20% cut of affiliate fees routed through it.
## Related
- [Why BitBadges](README.md)
- [BADGE Token](badge-token.md)
- [Links and Resources](links.md)
## File: about/links.md
---
description: "Official BitBadges links: app, explorer, install, packages, API reference, socials, integrations, brand assets, and how to contribute."
---
# Links and Resources
Official links for the app, the code, the community, and the brand. Discord is the preferred contact. See the rest of the docs for specific subjects.
## Product
- [BitBadges app](https://bitbadges.io)
- [Explorer](https://explorer.bitbadges.io)
- [Get featured on the Explore page](https://tally.so/r/mBy2aR)
## Install
```bash
curl -fsSL https://install.bitbadges.io | sh
```
One command installs the chain binary (with the `bb` alias) and the SDK CLI on Linux, macOS (Intel and Apple Silicon), and Windows (Git Bash or WSL). See [Quickstart](../start/quickstart.md) and [CLI](../cli/README.md).
- [Chain binary releases](https://github.com/BitBadges/bitbadgeschain/releases)
- [`bitbadges` on npm](https://www.npmjs.com/package/bitbadges): the SDK, the SDK CLI, and the MCP builder tools in one package.
```bash
npm install bitbadges # SDK in a project
bun add bitbadges # or with bun
npm install -g bitbadges # SDK CLI and MCP builder tools globally
```
## Documentation and Reference
- [API reference](/api-reference): the interactive reference for every BitBadges API route.
- [SDK](../sdk/README.md) and [BitBadges API](../api/README.md).
- [Reading the Docs](../agents/reading-the-docs.md): `llms.txt`, `for-llms.txt` (the full docs as one text file), and `bb dev docs`.
- [Proto definitions](https://github.com/BitBadges/bitbadgeschain/tree/master/proto)
## Socials
- [Discord](https://discord.com/invite/TJMaEd9bar)
- [X (Twitter)](https://twitter.com/bitbadges_io)
- [Telegram](https://t.me/bitbadges_chat)
- [LinkedIn](https://linkedin.com/company/bitbadges)
- [GitHub](https://github.com/bitbadges)
## Integrations
- [Zapier](https://zapier.com/apps/bitbadges/integrations)
## Brand Guidelines
Use the BitBadges name and logo on your site as you see fit. Ask on Discord for other logos, images, or information, or with any question about usage.
WordmarkLogo mark
## Contributing
Everything is open source at [github.com/bitbadges](https://github.com/bitbadges). Contributions to the core source code are welcome.
- Build on BitBadges: a dApp, a developer tool, or your own frontend for tokens. Reach out on Discord to get it listed in the Ecosystem.
- Improvement proposals: [github.com/BitBadges/BBIPs](https://github.com/BitBadges/BBIPs).
- Bug bounties: to be announced.
- Developer rewards: hackathons, bug bounties, and a developer funding pool are planned.
## Related
- [Quickstart](../start/quickstart.md)
- [FAQ](faq.md)
## File: about/use-cases.md
---
description: "What people build with the BitBadges token standard, grouped by domain, with the feature each use case relies on."
---
# Use Cases
The token standard tokenizes assets, services, and rights with protocol-level transferability rules, payments in any IBC denom, and time-dependent balances. Any use case in crypto is a tokenization use case in some form, so the module also serves as a general primitive for application development.
## Asset Tokenization
- Compliant tokenized assets. Compliance checks run on every transfer at the protocol level, including in liquidity pools and orderbooks, so no application has to enforce the rules itself.
- Real-world assets. Jewelry, art, collectibles, or any product, purchasable with any IBC currency and moved with permissioned IBC transfers.
- Real estate with license verification. A property license is verified on every transfer, so only licensed properties move.
- Compliant ICS-20 derivatives. Wrap an existing ICS-20 token such as USDC into a derivative (for example clUSDC) backed 1:1 by the original, with rate limits, compliance checks, and withdrawal restrictions. This gives a chain a permissioned version of a standard token without modifying the token itself. See [Smart Tokens and Vaults](../guides/smart-tokens-and-vaults.md).
## NFTs and Collectibles
- Collectibles and profile pictures. Standard NFT mechanics with rarity and full transferability controls.
- Gated NFTs and quests. Mints gated by claim plugins or external integrations: email verification, Discord membership, or a custom requirement.
- Quest rewards. NFTs with payouts attached, gated by criteria-based approvals, for games, loyalty programs, and engagement campaigns.
## Soulbound Tokens and Compliance
- Soulbound tokens. A non-transferable collection locks tokens to addresses for achievements, credentials, attestations, and proof of anything.
- Compliance as a service. Keep your token on ERC-3643 or `x/bank` and use BitBadges as the compliance check by verifying ownership of compliance tokens: licenses, subscriptions, NFTs, KYC credentials, and address lists. Configure the approval criteria needed for the application rather than assuming token ownership proves every compliance requirement. See [Comparisons](comparisons.md).
## Subscriptions and Recurring Payments
- Auto-renewing subscriptions. Subscription tokens purchasable with any IBC currency that renew through bots, with optional provider revocation for cancellation policies. See [Subscriptions and Time-Based Tokens](../guides/subscriptions-and-time-based-tokens.md).
- Payroll automation. Recurring payments with no-code setup. Verify that recipients still hold an employment NFT or meet custom criteria before each payment.
## Securities and Financial Instruments
- Stocks with accredited investor KYC. Transferability approvals allow only verified investors to receive the security.
- Freezable and revocable currencies. The manager can freeze or revoke transfers, as regulated currencies, stablecoins, and other instruments require.
- Bonds and CDs with clawbacks. Administrative clawback controls built into the instrument.
## Time-Based Systems
- Vesting. Time-dependent balances and approvals release tokens on a schedule for employee options, token vesting, and gradual rewards.
- Time-vested escrow. Assets stay in custody until a time condition is met, for milestone payments and conditional releases.
- Expiring access tokens. Tickets and session credentials that expire through time-dependent balances.
- Auto-expiring occupation tokens. Rentals, bookings, and temporary ownership that end at a set time with no manual step.
- Rental agreements. Time-dependent leases with custom terms enforced through approvals. Contracts, payments, and access rights are on-chain and expire automatically.
## Legal and Compliance
- Dispute resolution and clearing. Define the rules to hold, freeze, or reverse transactions.
- Advanced admin controls. Revocability and freezability for regulatory compliance or emergency asset protection.
- Intellectual property rights. Patents and IP rights with licensing terms enforced on every transfer.
## Business Operations
- Refund and return policies. Receipt tokens with return policies expressed as transferability approvals and refunds paid out on-chain.
- Address lists and reputation. Public lists of scammers, compromised keys, and trusted entities that any application can reference.
## Features Every Use Case Shares
- Protocol-level enforcement: rules run in the chain, not in each application.
- Multi-currency: payments in any IBC-compatible denom.
- Custom transferability: who can transfer, to whom, and under what conditions.
- Time dependence: schedules, expirations, and vesting in the balance itself.
- Claim plugins and external integrations for off-chain eligibility checks.
- On-chain verification of configured approval criteria and submitted proofs; off-chain facts still depend on the services attesting to them.
## Related
- [Use Cases by Build](../use-cases/README.md)
- [Why BitBadges](README.md)
- [Guides](../guides/README.md)
- [Transferability](../token-standard/concepts/transferability.md)
## File: agents/README.md
---
description: "Install paths, quickstart, and the review-and-sign handoff for AI agents and bots that build on BitBadges."
---
# Agents
This tab is for AI agents, bots, and the people wiring them: how to install, which surface to use, and how a built transaction reaches a human wallet for signature.
## Discover Installed Capabilities
Use the CLI first. Discover a small operation list, then load only the schema or standard instructions needed for the task:
```bash
bb dev capabilities
bb dev capabilities build_payment_request_v2
bb dev skills payment-request
bb build payment-request-v2 --list-examples
```
The capability response includes `schemaVersion` and `catalogHash`. Schemas come from the same installed registry as MCP's `get_capabilities`; do not assume a downloaded documentation page matches an older installed binary. This catalog covers shared builder tools. Standard-specific CLI commands remain discoverable through `bb --help-json`, and native Cosmos operations through `bb tx --help`. Offline skill/schema discovery does not require an API key.
## Ask the Human to Pay
The primary payment flow uses the human's main wallet. The agent resolves the exact recipient, network, asset, amount, and invoice obligation, builds the proposed payment, and opens a browser request for the human to review and sign. An agent-owned wallet is optional.
```bash
bb pay-requests show 46
bb pay-requests pay 46 --creator "$PAYER" --obligation payment-1 --units 1 --browser
```
Use the real collection and obligation IDs from the merchant's request. Follow [Main-Wallet Payment Requests](main-wallet-payments.md) for the complete flow, [CLI Automation](../cli/agent-workflow.md) for machine-readable discovery and retries, and [Standards](../standards/README.md) to choose the correct payment model. A one-off invoice payment does not grant ongoing spending permission.
For unattended operation or a withdrawal budget, compare [spending authority models](spending-models.md) before funding an agent. Prompts and hooks do not constrain a signing key that the agent controls.
## Create Your First Collection
Connect your client using [Set Up Your AI](setup.md), then ask:
```text
Hey Claude, create me a 5 ATOM / month subscription.
```
The builder gathers the details, generates the collection and payment rules, checks the transaction, and returns a link to review and sign with your wallet. [Your First Collection](../start/first-collection.md) walks through the full flow, including 30-day periods and renewal authorization. You can use the same workflow for NFTs, fungible tokens, smart tokens, and payment requests.
## Install
```bash
curl -fsSL https://install.bitbadges.io | sh
export BITBADGES_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef # your key from bitbadges.io/developer
bb settings set apiKey "$BITBADGES_API_KEY"
bb doctor
```
This installs the chain binary and the SDK CLI as `bb`. Every path below starts from this install. Get an API key at [bitbadges.io/developer](https://bitbadges.io/developer); the example key above is fake. Per-harness MCP configs (Claude Desktop, Cursor, Windsurf, Codex, VS Code, Zed, no-tools LLMs): [Set Up Your AI](setup.md).
## What to Say
Once a harness is wired, these prompts map onto the MCP builder tools and the skills. Copy one and change the names.
Create a token:
```text
Create an NFT collection called Demo NFTs with 100 tokens, manager bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d, fully immutable after creation.
```
```text
Build a fungible token called Demo Coin with 1,000,000 units of token ID 1 and a public mint of up to 10 per address.
```
```text
Make a USDC-backed smart token with symbol vUSDC and a daily withdraw limit of 1000.
```
Distribute:
```text
Mint 100 Demo Coin to bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue and bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf in the same transaction that creates the collection.
```
```text
Create a code-gated claim for collection 1 with 50 codes, one use per address.
```
Gate:
```text
Check whether bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue owns at least 1 of token ID 1 in collection 1.
```
```text
Build a 30-day subscription called Demo Membership at 10 USDC per interval paid to bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d.
```
Trade:
```text
Add a tradable listing approval to collection 1 so anyone can buy token ID 5 for 25 USDC.
```
```text
Give my agent wallet bb18cad7xxsk3drvwdxeasc3wqn2plftpzq2tsrsr a daily budget of 100 units of collection 2 that it can send to bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue.
```
Inspect:
```text
Explain collection 1 for an auditor: who can change what, and how do tokens move.
```
```text
Review this transaction file for problems before I sign it.
```
Every build ends the same way: the agent calls `get_review_url` (or runs `bb preview`) and hands you a bitbadges.io link where you review and sign with your own wallet; the agent never holds the key.
## Paths
| Path | Best for | Install | Reference |
| --- | --- | --- | --- |
| CLI and chain binary (start here) | Terminal agents, shell scripts, any language | `curl -fsSL https://install.bitbadges.io \| sh` | [CLI](../cli/README.md) |
| MCP builder tools | Cursor, Claude Desktop, Codex, any MCP client | `npm i -g bitbadges` | [MCP Builder Tools](mcp-tools.md) |
| Claude Code plugin | Claude Code users: auto-wired MCP plus 8 workflow skills | `/plugin marketplace add BitBadges/bitbadges-plugin` then `/plugin install bitbadges` | [Claude Code Plugin](claude-code-plugin.md) |
| Programmatic agent | Node processes that build from a prompt with your own Anthropic or OpenAI key | `npm i bitbadges @anthropic-ai/sdk` | [Programmatic Agent](programmatic-agent.md) |
| SDK signing client | Full TypeScript bots that sign and broadcast | `npm i bitbadges` | [Signing Client](../sdk/transactions/signing-client.md) |
| Direct HTTP | Lightweight scripts in any language | REST calls to `api.bitbadges.io` | [API](../api/README.md) |
| Spending authorization | Daily caps, time windows, and revocation for a delegate wallet | on-chain approval | [Spending Authorization](spending-authorization.md) |
Bring your own AI. The MCP server, the CLI, and the plugin are model-agnostic; the harness you already run provides the model. The site runs no LLM of its own and never holds your model key: it receives what you built, reviews it, and signs it.
```bash
# Step 1: install the chain binary and CLI (always)
curl -fsSL https://install.bitbadges.io | sh
bb settings set apiKey "$BITBADGES_API_KEY"
# Step 2: optionally add a harness convenience
# Claude Code:
# /plugin marketplace add BitBadges/bitbadges-plugin
# /plugin install bitbadges
# Cursor, Claude Desktop, other MCP clients:
# claude mcp add bitbadges-builder -- npx -y -p bitbadges bitbadges-builder
```
## Optional Agent-Owned Wallet (TypeScript)
This path is for funds intentionally assigned to an agent-controlled wallet. Use the browser handoff above when a human should approve each payment with their main wallet.
```bash
npm install bitbadges
```
```ts
import { BitBadgesSigningClient, GenericEvmAdapter, MsgTransferTokens, NETWORK_CONFIGS } from 'bitbadges';
// 1. Create an adapter from a mnemonic (server-side only; never commit the phrase)
const adapter = await GenericEvmAdapter.fromMnemonic(process.env.MNEMONIC!, NETWORK_CONFIGS['mainnet'].evmRpcUrl);
// 2. Create a signing client (mainnet)
const client = new BitBadgesSigningClient({
adapter,
network: 'mainnet'
});
// 3. Fund client.address with BADGE for fees before the first broadcast
// 4. Broadcast your first transaction: send 5 units of Demo Coin (collection 2, token ID 1) to bob
const result = await client.signAndBroadcast([
new MsgTransferTokens({
creator: client.address,
collectionId: '2',
transfers: [
{
from: client.address,
toAddresses: ['bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue'],
balances: [
{
amount: '5',
tokenIds: [{ start: '1', end: '1' }],
ownershipTimes: [{ start: '1', end: '18446744073709551615' }]
}
]
}
]
})
]);
console.log('TX Hash:', result.txHash, 'success:', result.success);
```
Examples target mainnet because testnet is offline. The faucet API shape and the testnet status live on [Testnet](../chain/testnet.md).
## Agent Workflow with the CLI
The CLI returns JSON on every command, accepts stdin (`-`), file paths (`@file.json`), and inline JSON, and needs no build step.
```bash
# Discover the command tree as JSON (for LLM tool discovery)
bb --help-json
bb api --help
bb api tokens --help
# Query
bb api tokens get-collection 1
bb api tokens get-balance-by-address 1 bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue
bb api accounts get-account --body '{"address":"bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"}'
bb api --search owners
# Review and audit
bb check tx.json
bb explain tx.json
bb check 42
bb explain 42
# Browse docs and skills
bb dev docs all
bb dev docs learn/approvals
bb dev skills
bb dev skills smart-token
# Sign and broadcast with the chain binary
bb keys add agent-wallet
bb tx tokenization create-collection ./collection.json \
--from agent-wallet --chain-id bitbadges-1 \
--node https://rpc.bitbadges.io:443 \
--gas auto --gas-adjustment 1.5 --gas-prices 10ubadge
# Or broadcast a signed tx through the API
bb api tx broadcast-tx --body @signed-tx.json
```
`bb api tokens get-collection 1` returns the standard envelope (mainnet output, trimmed to the first fields):
```json
{
"ok": true,
"data": {
"collection": {
"collectionId": "1",
"collectionMetadata": {
"metadata": {
"name": "BitBadges Examples",
"description": "Badges are generic and can be created for many different purposes and use cases. This collection highlights some of our favorites.",
"image": "ipfs://QmNytJNN44stkMndshtdfcCW2mzaCm6A23maiKaQvUqoj8"
},
"uri": "ipfs://QmSTZZPgYF58gS9bM7q3nWVegUJH51WBdT91fz7q94qDwS",
"customData": ""
}
}
},
"warnings": [],
"error": null
}
```
`--dry-run` simulates any API call without side effects.
### Which Surface Does What
| Task | Surface |
| --- | --- |
| Query data, browse docs, review transactions | CLI (`bb`) |
| Build collections with guided workflows | MCP builder tools |
| Sign and broadcast | Chain binary (`bb tx`) or SDK signing client |
| Key management | Chain binary (`bb keys`) |
### API Key Versus Session
The API key is required on every BitBadges API call and carries the app scope. Anything that mutates an account, manages keys, or publishes signed data also needs a user scope: a session cookie from `bb auth login`. The CLI is wallet-agnostic. Pair it with `bb sign-arbitrary` for headless Cosmos signing, or paste in a signature from any external wallet.
```bash
# 1. Fetch a challenge (saves the nonce cookie locally for step 3)
MSG=$(bb auth challenge --address $(bb keys show agent-wallet -a) | jq -r .data.message)
# 2. Sign offline with the chain binary
SIG_JSON=$(bb sign-arbitrary agent-wallet "$MSG")
# 3. Post the signature (stores the session under ~/.bitbadges/auth.json)
bb auth login \
--address "$(echo "$SIG_JSON" | jq -r .address)" \
--signature "$(echo "$SIG_JSON" | jq -r .signature)" \
--public-key "$(echo "$SIG_JSON" | jq -r .pubKey)" \
--message "$MSG"
# 4. Add --with-session to Full Access requests
bb api accounts get-account --body '{"address":"bb18cad7xxsk3drvwdxeasc3wqn2plftpzq2tsrsr"}' --with-session
```
Sessions are multi-account and multi-network. Full reference: [Auth](../cli/auth.md).
### Deprecation Banner
Legacy forms (`bb cli `, `bitbadges-cli sign-with-browser`, `bitbadges-cli gen-tx-payload`, and the per-utility top-level names that moved under `bb account`, `bb dev`, and `bb settings`) print a one-line deprecation banner to stderr during the migration window. Stdout (the JSON envelope) is unchanged. Set `BB_QUIET=1` in the agent's environment if banner noise breaks line-based stderr parsers; the same flag suppresses every command's auto-review commentary. The release after the migration window hard-fails the old forms, so update prompts and scripts ahead of the cutover.
### Example: Automated Balance Check and Mint
```bash
#!/bin/bash
# Check bob's Demo Coin balance and mint when it drops below a threshold
COLLECTION_ID=2
ADDRESS="bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"
THRESHOLD=10
BALANCE=$(bb api tokens get-balance-by-address "$COLLECTION_ID" "$ADDRESS" --condensed)
# bb api emits the envelope; the body lives at .data
AMOUNT=$(echo "$BALANCE" | jq -r '.data.balances[0].amount // "0"')
if [ "$AMOUNT" -lt "$THRESHOLD" ]; then
echo "Balance $AMOUNT below threshold $THRESHOLD, minting"
bb check ./mint-tx.json
bb tx tokenization transfer-tokens ./mint-tx.json \
--from agent-wallet --chain-id bitbadges-1 \
--node https://rpc.bitbadges.io:443 \
--gas auto --gas-prices 10ubadge
fi
```
## Hand Off to the Browser to Sign
In the browser-handoff workflow, agents build and people sign. The link opens the review flow (Preview, Review Items, Transferability, Permissions, then wallet signature). Separately, a bot configured with a signing key can use the SDK or CLI to broadcast; see [Spending Authorization](spending-authorization.md) for bounded delegation.
| From | Get the link |
| --- | --- |
| MCP or Claude Code | call `get_review_url` and open `reviewUrl` (see [MCP Builder Tools](mcp-tools.md#hand-off-to-the-browser)) |
| CLI | `bb preview tx.json --open` (or `bb build vault --backing-coin USDC --name "Demo Vault" \| bb preview - --open`) |
| Programmatic agent | `result.reviewUrl` |
| Any LLM, no tools | paste the JSON into `bitbadges.io/mint/local-builder` |
Two carriers exist. `bb preview` and `get_review_url` upload the transaction to the open preview endpoint and return a short `?code=prv_...` link that expires in 1 hour. The programmatic agent returns a `#tx=` link that carries the whole transaction in the URL hash, so nothing is uploaded. Update transactions (a non-zero `collectionId`) route to `/update/local-builder/:id` so the site diffs against on-chain state. Details: [Analyze](../cli/analyze.md) and [Deploy](../cli/deploy.md).
## Metadata Without Hosting
The CLI builders and templates accept `--name`, `--image`, and `--description` (or `--name` plus `--description` for approvals, which have no image) and serialize them into the on-chain `customData` field. The BitBadges API, the SDK, and the site parse `customData` on read and surface it as the resolved metadata, so an agent can ship a working collection without an IPFS pin or a Pinata account. Pass `--uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json` (a file you host) instead to host the JSON yourself. The URI takes priority when both are set. On-chain shape: [Collections](../token-standard/concepts/collections.md).
{% hint style="warning" %}
`--image` should be a URL, not bytes. Inline `customData` lives on-chain: you pay gas per byte and blocks have a hard size cap. Pre-host images on IPFS or any URL host and pass the URL to `--image`. Inline `customData` is for the metadata wrapper (name, description, link to image), not the image itself.
{% endhint %}
For zero hosting and an image, the SDK ships a deterministic SVG placeholder-art generator (`import { generatePlaceholderArt } from 'bitbadges'`). It produces 1-8 KB `data:image/svg+xml;base64,...` URIs you can pass to `--image`. The same seed always produces the same art. The SVG bytes still live on-chain, so a 1-8 KB SVG costs about 10-80k extra gas per write versus a hosted URL. Use it for placeholder looks, not for image-heavy or high-frequency-update collections.
## Network Configuration
| Network | API URL | Node LCD | Cosmos chain ID | EVM chain ID | EVM RPC |
| --- | --- | --- | --- | --- | --- |
| mainnet | `https://api.bitbadges.io` | `https://lcd.bitbadges.io` | `bitbadges-1` | 50024 | `https://evm-rpc.bitbadges.io` |
| testnet (offline) | `https://api.bitbadges.io/testnet` | `https://lcd-testnet.bitbadges.io` | `bitbadges-2` | 50025 | `https://evm-rpc-testnet.bitbadges.io` |
| local | `http://localhost:3001` | `http://localhost:1317` | `bitbadges-1` | 90123 | `http://localhost:8545` |
Testnet endpoints when it returns: RPC `https://rpc-testnet.bitbadges.io`, WebSocket `wss://rpc-testnet.bitbadges.io/websocket`. Status: [Testnet](../chain/testnet.md). Full endpoint table: [Network](../chain/README.md).
## Token-Gated API Access (BB-402)
BB-402 lets any server gate API access behind on-chain token ownership with the standard HTTP 402 status code. BB-402 uses token ownership as the primitive: a soulbound token that costs X USDC is a verifiable on-chain receipt, and the same protocol handles subscriptions, tiered access, reputation, blocklists, and compound conditions with `$and` / `$or` logic.
```text
Agent --> Server: GET /api/data
Server --> Agent: 402 { ownershipRequirements, message }
Agent --> Server: GET /api/data + X-BB-Proof: { address, chain, message, signature }
Server --> Agent: 200 OK (or 403)
```
Guide: [Gate access](../guides/gate-access.md). Spec: [BB-402](../token-standard/bb-402/README.md).
## In This Tab
| Page | Read it when |
| --- | --- |
| [Set Up Your AI](setup.md) | You want the exact MCP config for your harness |
| [MCP Builder Tools](mcp-tools.md) | You run an MCP client and want the full tool list, client configs, and workflows |
| [Claude Code Plugin](claude-code-plugin.md) | You use Claude Code and want the auto-wired MCP plus workflow skills |
| [Programmatic Agent](programmatic-agent.md) | You build from prompts in Node with your own Anthropic or OpenAI key |
| [Spending Authorization](spending-authorization.md) | You give an agent a delegate wallet with on-chain caps and revocation |
| [Bot Examples](bot-examples.md) | You want copy-paste patterns: mint, gate, react to events |
| [Reading the Docs](reading-the-docs.md) | Your agent needs to fetch documentation by URL, tool, or CLI |
| [Skills](skills/README.md) | You want the per-token-type build instructions the builder ships with |
## Related
- [Claims API](../api/claims/endpoints.md) for automated minting through claims
- [WebSocket Events](../chain/websocket-events.md)
- [BitBadges AI Quickstarter](https://github.com/BitBadges/bitbadges-quickstarter-ai) (GitHub template repo)
- [SDK AI agent guide](https://github.com/BitBadges/bitbadgesjs/blob/main/packages/bitbadgesjs-sdk/AI_AGENT_GUIDE.md)
## File: agents/bot-examples.md
---
description: "Copy-paste bot patterns on mainnet. Mint a fungible token, conditional transfer, ownership gating, event subscription, and the builder tool workflow."
---
# Bot Examples
Copy-paste patterns for bots and agents that act on mainnet. Each example is a complete script; the addresses are the docs fixtures (alice `bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d` runs the bot, bob `bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue` is the user), so swap in your own.
Testnet is offline, so every example targets mainnet. Test on a low-value collection first. Testnet status and the faucet API shape: [Testnet](../chain/testnet.md).
Every example that reads from the BitBadges API needs an API key in `BITBADGES_API_KEY`. Create one at [bitbadges.io/developer](https://bitbadges.io/developer). See [BitBadges API](../api/README.md).
## 1. Mint a Fungible Token
Create a fungible token collection with server-side signing. The collection message comes from `bb build`, the MCP builder tools (`get_transaction`), or the [Create a Collection](../guides/create-a-collection.md) guide, saved as `collection.json`.
```ts
import { readFileSync } from 'node:fs';
import { BitBadgesSigningClient, GenericEvmAdapter, NETWORK_CONFIGS, MsgCreateCollection } from 'bitbadges';
const adapter = await GenericEvmAdapter.fromMnemonic(process.env.MNEMONIC!, NETWORK_CONFIGS['mainnet'].evmRpcUrl);
const client = new BitBadgesSigningClient({
adapter,
network: 'mainnet'
});
// collection.json holds { "messages": [{ "typeUrl": "/tokenization.MsgCreateCollection", "value": {} }] } with the value filled in
const built = JSON.parse(readFileSync('./collection.json', 'utf8'));
const msg = new MsgCreateCollection({ ...built.messages[0].value, creator: client.address });
const result = await client.signAndBroadcast([msg]);
if (result.success) {
console.log('Collection created. TX:', result.txHash);
} else {
console.error('Failed:', result.error);
}
```
Load the `fungible-token` skill for the message shape: `bb dev skills fungible-token` or `get_skill_instructions({ skillId: "fungible-token" })`.
## 2. Check a Balance and Transfer on a Condition
Top up bob's Demo Coin (collection 2, token ID 1) when he holds fewer than 10 units.
```ts
import { BigIntify, BitBadgesAPI, BitBadgesSigningClient, GenericEvmAdapter, NETWORK_CONFIGS, MsgTransferTokens } from 'bitbadges';
const api = new BitBadgesAPI({ apiUrl: 'https://api.bitbadges.io', apiKey: process.env.BITBADGES_API_KEY, convertFunction: BigIntify });
const COLLECTION_ID = '2';
const BOB = 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue';
// Check balance
const balanceDoc = await api.getBalanceByAddress(COLLECTION_ID, BOB);
const held = balanceDoc.balances
.filter((b) => b.tokenIds.some((range) => range.start <= 1n && range.end >= 1n))
.reduce((sum, b) => sum + b.amount, 0n);
// Conditional transfer
if (held < 10n) {
const adapter = await GenericEvmAdapter.fromMnemonic(process.env.MNEMONIC!, NETWORK_CONFIGS['mainnet'].evmRpcUrl);
const client = new BitBadgesSigningClient({
adapter,
network: 'mainnet'
});
const result = await client.signAndBroadcast([
new MsgTransferTokens({
creator: client.address,
collectionId: COLLECTION_ID,
transfers: [
{
from: client.address,
toAddresses: [BOB],
balances: [
{
tokenIds: [{ start: 1n, end: 1n }],
ownershipTimes: [{ start: 1n, end: 18446744073709551615n }],
amount: 10n - held
}
]
}
]
})
]);
console.log('Transfer result:', result.txHash, result.success);
}
```
## 3. Gate Access (Verify Ownership)
Check that a user owns a token before granting access.
```ts
import { BigIntify, BitBadgesAPI } from 'bitbadges';
const api = new BitBadgesAPI({ apiUrl: 'https://api.bitbadges.io', apiKey: process.env.BITBADGES_API_KEY, convertFunction: BigIntify });
const GATING_COLLECTION_ID = '1';
async function checkAccess(userAddress: string): Promise {
try {
const balanceDoc = await api.getBalanceByAddress(GATING_COLLECTION_ID, userAddress);
// Does the user own token ID 1?
return balanceDoc.balances.some(
(b) => b.tokenIds.some((range) => range.start <= 1n && range.end >= 1n) && b.amount > 0n
);
} catch (error) {
console.error('Ownership check failed:', error);
return false;
}
}
const allowed = await checkAccess('bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue');
console.log(allowed ? 'grant access' : 'deny access');
```
For an HTTP-native version of this check (the server answers 402 and the agent proves ownership with a signature), use [Gate access](../guides/gate-access.md).
## 4. Subscribe to Events and React
Listen for transfers on a collection and act on each one.
```ts
import WebSocket from 'ws';
const RPC_WS = 'wss://rpc.bitbadges.io/websocket';
function connect() {
const ws = new WebSocket(RPC_WS);
ws.on('open', () => {
ws.send(
JSON.stringify({
jsonrpc: '2.0',
method: 'subscribe',
id: 1,
params: {
query: "tm.event='Tx' AND message.action='/tokenization.MsgTransferTokens'"
}
})
);
console.log('Listening for transfers');
});
ws.on('message', async (data: WebSocket.Data) => {
const msg = JSON.parse(data.toString());
if (msg.result?.data?.value?.TxResult) {
const txHash = msg.result.events?.['tx.hash']?.[0];
console.log('Transfer detected:', txHash);
await handleTransfer(txHash);
}
});
ws.on('close', () => {
console.log('Disconnected, reconnecting in 5s');
setTimeout(connect, 5000);
});
}
async function handleTransfer(txHash: string) {
// your logic: notify, update a database, trigger another transaction
console.log(`Processing transfer ${txHash}`);
}
connect();
```
Query syntax and event types: [WebSocket Events](../chain/websocket-events.md).
## 5. Builder Tool Workflow
With the [MCP Builder Tools](mcp-tools.md) (Claude Desktop, Claude Code, Cursor), build a collection with the session tools and verify before handing off. Every call below is the JSON the client sends; the same files work with `bb dev tools call --args-file`.
```text
# 1. Load the skill
get_skill_instructions({ "skillId": "fungible-token" })
# 2. Build with per-field tools (all in parallel)
set_standards({ "standards": ["Fungible Tokens"] })
set_valid_token_ids({ "tokenIds": [{ "start": "1", "end": "1" }] })
set_default_balances({
"defaultBalances": {
"balances": [],
"outgoingApprovals": [],
"incomingApprovals": [],
"autoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedIncomingTransfers": true,
"autoApproveAllIncomingTransfers": true
}
})
set_permissions({ "preset": "locked-approvals" })
set_collection_metadata({
"name": "Demo Coin",
"description": "Token managed by my AI agent",
"image": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/coin.png"
})
add_approval({
"approvalId": "public-mint",
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"approvalAmounts": {
"overallApprovalAmount": "1000000",
"perToAddressApprovalAmount": "10",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "public-mint",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
})
# 3. (Optional) auto-mint to yourself at creation
add_transfer({
"transfers": [
{
"from": "Mint",
"toAddresses": ["bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"],
"balances": [
{
"amount": "10",
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
],
"prioritizedApprovals": [
{ "approvalId": "public-mint", "approvalLevel": "collection", "approverAddress": "", "version": "0" }
],
"onlyCheckPrioritizedCollectionApprovals": true
}
]
})
# 4. Verify (in parallel)
validate_transaction({})
review_collection({})
simulate_transaction({})
```
`review_collection` returns findings and a summary. Fix anything critical before signing.
```json
{ "findings": ["…"], "summary": { "critical": 0, "warning": 1, "info": 3, "verdict": "warn" } }
```
```text
# 5. Export and hand off
get_transaction({})
get_review_url({}) # give the user reviewUrl to review and sign
```
```json
{ "success": true, "code": "prv_7tffq58d", "reviewUrl": "https://bitbadges.io/mint/local-builder?code=prv_7tffq58d", "expiresAt": 1788803578315, "expiresIn": "1 hour" }
```
### CLI Template Build
```bash
# Vault token (prints transaction JSON)
bb build vault --backing-coin USDC --name "Demo Vault" --description "USDC vault" \
--image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/vault.png --explain
# Subscription collection
bb build subscription --interval monthly --price 10 --denom USDC --recipient bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d \
--uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json
# Review and sign in the browser
bb build vault --backing-coin USDC --name "Demo Vault" --description "USDC vault" \
--image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/vault.png | bb preview - --open
```
Every template builder: [Build](../cli/build.md).
### Query Sequence (No Signing)
```text
search({ "query": "Demo Coin" })
-> query_collection({ "collectionId": "2" })
-> query_balance({ "collectionId": "2", "address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue" })
-> verify_ownership({ "address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue", "collectionId": "2", "tokenId": "1" })
```
The builder builds and validates but never signs or broadcasts. Sign with the SDK signing client, the chain binary, or the review-and-sign link.
## Tips for Agents
- Simulate before broadcasting. Use `simulate_transaction`, `bb simulate`, or the signing client's `simulate: true` option to catch errors before spending gas.
- Test on a low-value collection first. Testnet is offline; see [Testnet](../chain/testnet.md) for status and the faucet shape.
- Check `result.success` and `result.error` after every broadcast.
- The signing client handles nonce and sequence with retries.
- Keep credentials in environment variables (`MNEMONIC`, `BITBADGES_API_KEY`). Never hardcode them.
- Bound what an agent can spend with an on-chain [Spending Authorization](spending-authorization.md).
## Related
- [Signing Client](../sdk/transactions/signing-client.md)
- [Claims API](../api/claims/endpoints.md) for automated minting through claims
- [Agents](README.md)
## File: agents/claude-code-plugin.md
---
description: "Install and use the BitBadges Claude Code plugin. It wires the MCP builder tools and adds 8 workflow skills, two slash commands, and a builder subagent."
---
# Claude Code Plugin
The Claude Code plugin is a convenience layer on top of the chain binary and CLI for Claude Code users. It registers the `bitbadges-builder` MCP server and ships 8 skills that teach Claude how to use the CLI, the MCP tools, and the docs for common workflows.
```sh
curl -fsSL https://install.bitbadges.io | sh
bb settings set apiKey "$BITBADGES_API_KEY"
```
```text
/plugin marketplace add BitBadges/bitbadges-plugin
/plugin install bitbadges
/bitbadges:setup
```
The plugin is a thin harness, not a knowledge base. Token-type instructions live in the SDK and surface through `bb dev skills smart-token` (one command per skill id), the `get_skill_instructions` MCP tool, and the [Skills](skills/README.md) pages. The plugin's job is to teach Claude where to find them and how to compose them, not to ship one wrapper per token type.
## Prerequisites
The combined installer provides `bb`, the chain binary, and the SDK commands. The SDK package provides `bitbadges-cli` and `bitbadges-builder`. Offline building needs the SDK CLI; native Cosmos commands need the chain binary. MCP is optional for CLI work. Configure API access for remote operations when needed.
The plugin uses installed binaries and does not download a fallback. If a command is missing, `/bitbadges:setup` identifies the prerequisite for the task instead of treating every optional capability as a blocker.
Run `/bitbadges:setup` once to confirm the wiring, and `/bitbadges:status` for a health check at any time.
## What You Get
### MCP Server (Auto-Wired)
The plugin's `.mcp.json` registers `bitbadges-builder`, so there is no `claude mcp add` step:
```json
{
"mcpServers": {
"bitbadges-builder": {
"command": "bitbadges-builder",
"args": []
}
}
}
```
The server runs the same SDK version as the installed CLI. If the client cannot find it, check its PATH or configure the absolute binary path. A running MCP server is not necessary to use the CLI skills.
### Skills (8)
Each skill routes Claude to the right CLI command, MCP tool, or docs page for one workflow. None duplicates token-type knowledge from the SDK.
| Skill | What it teaches |
| --- | --- |
| `build` | Discovers standards with `bb dev skills`, loads the installed schema and examples, and constructs with CLI presets. Uses MCP as an optional adapter |
| `review` | Audit a transaction file or live collection for correctness, standards compliance, approval ordering, and UX issues. Wraps `bb check` and `review_collection` |
| `simulate` | Dry-run a transaction. Returns events and per-address balance diffs. Always before broadcast. Wraps `simulate_transaction` |
| `explain` | Plain-English description, audience-aware (user, developer, auditor). Wraps `explain_collection` and `bb explain` |
| `query` | Collections, balances, accounts, claims, standards, and every API route. Discovers routes with `--help-json` first, then calls |
| `address` | All six address operations: cosmos and EVM conversion, IBC backing, wrapper, mint-escrow, and alias derivations |
| `claim` | Build or audit an open, code-gated, password-gated, or whitelist-gated claim. Additional ownership requirements use the appropriate approval primitives |
| `broadcast` | Sign and broadcast. On rails: dry-run by default, explicit confirmation for live. Picks between chain binary, burner, browser bridge, and programmatic signer |
For deeper instructions on a specific token type, the plugin sends Claude to the SDK, CLI, or docs rather than redefining them locally.
### Slash Commands
| Command | What it does |
| --- | --- |
| `/bitbadges:setup` | Checks prerequisites for the selected task and guides local API configuration without asking for secrets in chat |
| `/bitbadges:status` | Runs CLI diagnostics and distinguishes offline readiness from remote connectivity. Live MCP connectivity is checked through client tool discovery |
### Subagent
`bitbadges-builder` is a focused builder agent. It uses CLI presets and canonical skills first, with MCP available when useful. It saves the proposal, reviews and simulates it, then chooses the signing path based on wallet ownership and granted authority. Use it for isolation from the main conversation: "Have the bitbadges-builder agent prepare an unsigned smart token proposal for review."
### SessionStart Pre-warm Hook
The compatibility hook checks the installed CLI version quietly. It performs no package downloads or configuration changes.
## API Key
The CLI configuration is shared with the MCP server. `/bitbadges:setup` reuses it without printing secrets and directs you to configure a key locally when remote access is needed. Get a key at [bitbadges.io/developer](https://bitbadges.io/developer). No key is needed for offline proposal construction.
## Migrate from a Manual MCP Setup
If you previously registered the server manually, inspect `claude mcp list` after installing the plugin. Remove only a confirmed duplicate manual registration, using the scope shown by your client; inspect `claude mcp remove --help` before changing it:
```text
claude mcp remove bitbadges-builder
```
`/bitbadges:setup` can help inspect the registrations. A matching server name alone does not establish that there is a duplicate.
## When You Do Not Need the Plugin
The plugin is for Claude Code only. Other harnesses get the same coverage from the MCP server and the skill docs:
- Cursor, Claude Desktop, other MCP clients: set up the `bitbadges-builder` server in your client ([MCP Builder Tools](mcp-tools.md)). The server exposes `get_skill_instructions({ "skillId": "smart-token" })` for on-demand loading, the same path the plugin uses.
- Generic LLMs, shell scripts, CI: use the [CLI](../cli/README.md). For skill instructions, read the [Skills](skills/README.md) pages or run `bb dev skills smart-token`.
- TypeScript developers: `npm install bitbadges` and use the [SDK](../sdk/README.md).
The CLI is the base layer. Skill content is rendered in one place ([Skills](skills/README.md)) and consumed by reference from the plugin, the MCP server, and the CLI.
## Source
- Plugin installation and public distribution: [BitBadges/bitbadges-plugin](https://github.com/BitBadges/bitbadges-plugin)
- Contributor source: [packages/agent-plugin in the private monorepo](https://github.com/BitBadges/bitbadges-monorepo/tree/main/packages/agent-plugin) (repository access required). Installation does not require monorepo access.
- MCP server: [bitbadgesjs-sdk/src/builder](https://github.com/BitBadges/bitbadgesjs/tree/main/packages/bitbadgesjs-sdk/src/builder)
- CLI: [bitbadgesjs-sdk/src/cli](https://github.com/BitBadges/bitbadgesjs/tree/main/packages/bitbadgesjs-sdk/src/cli)
- Skills source of truth: [skillInstructions.ts](https://github.com/BitBadges/bitbadgesjs/blob/main/packages/bitbadgesjs-sdk/src/builder/resources/skillInstructions.ts)
## File: agents/main-wallet-payments.md
---
description: "An agent proposes an exact invoice payment or coin transfer; the human reviews and signs with their main wallet, then the agent confirms and reconciles the result."
---
# Main-Wallet Payment Requests
An agent can ask, “May I send 5 USDC to this merchant?” without holding the human's keys or receiving a standing allowance. It constructs the exact transaction, opens the browser review, and waits for the human's wallet decision. Each new payment requires its own authorization.
## Resolve the Purchase
Before asking for a signature, establish the network, canonical asset denomination, exact amount, recipient, human signer, and purpose. If the merchant provides an invoice, obtain its actual collection ID and obligation reference. Check that its on-chain payouts match the agreed purchase.
“USDC” alone does not identify a network asset. Display symbols are convenience inputs, not proof of issuer or backing. Resolve the denomination and decimals through the chosen network's registry and show the resulting base units in the transaction review. Keep fees separate from the 5 USDC recipient payout.
## Pay an Existing Invoice
```bash
bb pay-requests show 46 --mainnet
bb pay-requests status 46 --mainnet
bb pay-requests pay 46 --creator "$PAYER" --obligation payment-1 --units 1 --mainnet --output-file payment.json
bb check payment.json
bb explain payment.json
bb deploy payment.json --browser --expected-address "$PAYER" --mainnet
```
`$PAYER` must be the human wallet's actual address. Replace the example IDs. The standard helper validates the collection and binds the payment to its actual approval. For a partial invoice, units are quanta that scale every payout leg; calculate the requested units from the invoice terms instead of treating them as display coins.
When the reviewed terms are already established, the inline equivalent is:
```bash
bb pay-requests pay 46 --creator "$PAYER" --obligation payment-1 --units 1 --browser --mainnet
```
A merchant or manager must first publish the invoice collection. An agent can prepare its creation transaction, but cannot assume an unpublished collection has a known ID or that invoice creation itself requires no signature. A direct transfer is available when a persisted invoice is unnecessary.
## Direct Coin Transfer
```bash
bb build send --from "$PAYER" --to "$RECIPIENT" --amount 5 --denom USDC --mainnet --output-file payment.json
bb explain payment.json
bb deploy payment.json --browser --expected-address "$PAYER" --mainnet
```
The inline form is `bb build send --from "$PAYER" --to "$RECIPIENT" --amount 5 --denom USDC --mainnet --browser`. The `send` builder uses `--from` as the expected signer. Verify the resolved asset and amount before proceeding.
This is a bank transfer, not an invoice collection. It does not create invoice obligations, progress counters, refund rights, or recurring consent. Use [invoice standards](../standards/payments.md) when those finite payment-tracking semantics are needed.
## What the Browser Binds
The versioned transaction request carries a request ID, expected signer, deployment network, Cosmos and EVM chain IDs, expiration, signing mode, and transaction messages. The page validates the request and supported message types. Explicit sender fields are preserved; omitted sender fields are populated only from the expected signer.
The normalized messages shown for review must be the messages passed to the wallet. Wallet address, selected network, request expiration, and reviewed message identity are checked again before signing. A transaction review must not silently add a login transaction or replace an explicit sender with the currently connected account.
The new page also accepts supported legacy CLI requests through a separate adapter. Supplied signer and chain fields are preserved and checked; missing values are pinned once from the selected wallet and page network for that review, with a finite page lifetime. Legacy requests cannot retroactively prove the original CLI network or an omitted expiry.
This binding protects the application's handoff. It does not prove that a merchant's prose description is true or that delivery will occur. Local and mainnet configurations can share a Cosmos chain ID; the application's deployment label is not a cryptographic replay boundary in raw Cosmos signed bytes.
## Wallet Decision and Fees
Give the person a short handoff before launching the browser: “Pay this invoice on mainnet from your wallet: 5 USDC to the reviewed merchant address. This is one payment, with no recurring allowance. Review the separate network fee in your wallet.” Use the actual proposal's terms; if it grants an allowance or has multiple recipients, describe those instead.
Keep the CLI running until it receives the result. Use `--no-open` to print the signing URL when automatic opening is inconvenient. Open it in a browser on the same machine as the CLI, or use the documented SSH port forwarding. The callback uses localhost, so sending the link to a phone or closing the terminal can prevent the result from returning even after a successful payment.
The human reviews all messages and confirms in the wallet. The frontend selects gas and fees at signing time. Browser `--fee`, `--fee-denom`, and `--gas` flags do not impose a fee cap; those CLI controls belong to other signing paths. Review the wallet's fee request separately from recipient payouts.
Use ordinary sign-and-broadcast for payments. `--sign-only` is for compatible Cosmos adapters returning raw signed bytes. EVM paths that send during signing are rejected before invoking the wallet. The current callback transport accepts at most 4096 base64 characters of signed bytes; larger results return an explicit error and must use another supported signing flow. Do not treat this callback as a general large-transaction export channel.
Declining before wallet dispatch means no wallet signing request should begin. Once wallet interaction is in flight, closing a page is not proof of cancellation or non-submission. A request expiry prevents starting another signing invocation; it does not revoke an already signed or submitted transaction.
## Confirm the Result
Browser callbacks distinguish signing, submission, cancellation, error, and an unknown submission outcome. `outcome: "unknown"` includes `retrySafe: false`; reconcile it before another attempt. Submission is returned with `confirmed: false` and `verification: "unverified"`. The CLI validates the returned request identity, signer, network, and mode, but does not independently prove the signature contents or successful chain execution from the callback alone.
```bash
bb tx status "$TX_HASH" --mainnet
bb tx wait "$TX_HASH" --mainnet --timeout 120
bb pay-requests status 46 --mainnet
```
Inspect the chain execution result. The indexer may need time to reflect the confirmed payment. Store the hash and invoice obligation reference so a lost callback can be reconciled without paying again. On a timeout or ambiguous wallet error, inspect wallet activity and chain status before retrying. Reusable links intentionally accept repeat payments.
| Observed result | Report to the person | Next step |
| --- | --- | --- |
| Unsigned proposal | Ready for review | Review terms before requesting a wallet signature |
| Signed bytes | Signed, not submitted by this flow | Broadcast only through an authorized signing workflow |
| Submitted hash | Submitted; confirmation pending | Inspect chain execution, then invoice progress |
| Confirmed execution, stale invoice view | Confirmed; dashboard catching up | Wait for indexing; do not pay again |
| Declined before wallet dispatch | Request declined | Leave the payment unsubmitted |
| Timeout, lost callback, or uncertain wallet error | Outcome unknown | Reconcile wallet activity and chain state before another attempt |
### Recover a Request
The CLI saves browser transaction requests and their callbacks in private local files before opening the signing page. It prints the request ID on stderr. Inspect them from a second terminal:
```bash
bb dev requests list
bb dev requests status "$REQUEST_ID"
bb dev requests resume "$REQUEST_ID"
```
`status` reports the recorded result and checks the original listener. `resume` returns the **same** `signUrl` only while that listener is alive and the request has not expired or completed. Keep the original CLI running; this is not a background signing service. Neither command submits a transaction or creates a replacement request. Missing or expired listeners produce `outcome: "unknown"`, `canResume: false`, and `retrySafe: false`.
Recorded callbacks remain unverified: a saved hash is not proof of successful execution. Use `bb tx status` / `bb tx wait` on the original network, then reconcile invoice or subscription state. Request files contain transaction details and signing links; keep them private. They live under the CLI configuration directory (`~/.bitbadges/signing-requests` by default; `BITBADGES_CONFIG_DIR` selects a separate directory). Restarting `deploy --browser` creates a new request, so never use it as an automatic retry after uncertainty.
## Recurring Consent Is Separate
A personal-sign message such as “I authorize this agent” does not install an on-chain spending approval. A successful invoice payment does not authorize the next purchase. A payment link is not automatic billing.
Use [Subscriptions](../standards/subscriptions.md) when the human intentionally authorizes recurring charges, and inspect its period, recipient, amount, expiry, and cancellation semantics separately. [Spending Authorization](spending-authorization.md) covers a distinct delegate-wallet design. Neither is required for the main-wallet request-per-payment flow.
## File: agents/mcp-tools.md
---
description: "Reference for the MCP builder tools. Install, client configs, every tool with its key params, workflows, resources, and the CLI access path."
---
# MCP Builder Tools
The MCP builder tools let an AI assistant build, review, simulate, and query BitBadges transactions. The CLI is the primary entry point; MCP adapts the shared tool registry for clients that prefer structured tool calls. The installed registry in [`bitbadgesjs-sdk/src/builder/tools/`](https://github.com/bitbadges/bitbadgesjs/tree/main/packages/bitbadgesjs-sdk/src/builder/tools) is authoritative when this reference and an installed version differ.
```bash
# Install globally
npm install -g bitbadges
# Start the MCP server from that same installation
bitbadges-builder
```
To build from your own Node code with the same tools and no MCP client, use the [Programmatic Agent](programmatic-agent.md). For terminal workflows without an MCP client, use the [CLI](../cli/README.md).
## Schemas and Examples
```bash
bb dev capabilities
bb dev capabilities build_subscription
bb dev tools call build_subscription --args-file subscription.json
```
The compact catalog lists operation IDs and CLI/MCP entry points. Requesting one ID includes its `inputSchema`; the standard builders also include a sample in `inputSchema.examples`. The equivalent MCP call is `get_capabilities` with `{"id":"build_subscription"}`. `schemaVersion` identifies the catalog format and `catalogHash` identifies its installed definitions.
Eighteen existing standard builders are exposed through this adapter, including subscriptions, auctions, bounties, crowdfunds, product catalogs, smart tokens, credits, and approval templates. They use the same core functions as the CLI. PaymentRequestV2 remains available as `build_payment_request_v2`, including invoice substandards. The catalog also exposes standard actions through the installed CLI, including `standard_credit_tokens_quote` for exact credit purchase units. Native Cosmos commands remain CLI-only; inspect the installed catalog rather than guessing a tool name.
The schemas enforce JSON shape and unknown-field rejection. Each builder also applies its existing runtime rules; schema validity alone does not prove a payment is authorized or will succeed on chain. IDs and integer base-unit amounts use strings where declared; older display-unit builder parameters use numbers. Returned approval amounts are encoded as decimal strings. Examples are unsigned proposals with sample addresses, IDs and metadata URLs; replace them, set the real signer, review, and simulate before requesting a signature.
## Client Configuration
### Claude Desktop
Add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"bitbadges-builder": {
"command": "bitbadges-builder",
"args": [],
"env": {
"BITBADGES_API_KEY": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
}
}
}
```
The key is a fake example. Configure your own key from [bitbadges.io/developer](https://bitbadges.io/developer) locally. Building proposals does not require a mnemonic. Use [main-wallet requests](main-wallet-payments.md) for browser signing.
### Claude Code
After the chain and CLI install (`curl -fsSL https://install.bitbadges.io | sh`), add the server by hand:
```bash
claude mcp add bitbadges-builder -- bitbadges-builder
```
Or install the [Claude Code Plugin](claude-code-plugin.md), which wires the same server and adds 8 workflow skills plus `/bitbadges:setup` and `/bitbadges:status`:
```text
/plugin marketplace add BitBadges/bitbadges-plugin
/plugin install bitbadges
```
The plugin is a convenience layer. The CLI install is what runs underneath.
### Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"bitbadges-builder": {
"command": "bitbadges-builder",
"args": [],
"env": {
"BITBADGES_API_KEY": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
}
}
}
```
### Environment Variables
| Variable | Required | Description |
| --- | --- | --- |
| `BITBADGES_API_KEY` | For queries, simulation, broadcast, and review links | Your BitBadges API key ([get one](https://bitbadges.io/developer)) |
| `BITBADGES_API_URL` | No | Override the API base (default `https://api.bitbadges.io`) |
| `BITBADGES_FRONTEND_URL` | No | Override the site base used in review links (default `https://bitbadges.io`) |
| `BITBADGES_CONFIG_DIR` | No | Separate CLI configuration, sessions, and signing-request files; useful for isolated agent environments |
| `BITBADGES_CLI_PATH` | No | Trusted installed CLI executable for standard actions; defaults to `bitbadges-cli` on PATH |
The MCP server does not need wallet private keys or mnemonics. Signing happens through the separately authorized CLI or wallet flow. It does not read `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`; your client provides the model.
## Tools
Params marked `*` are required. Session tools also accept `sessionId` and `creatorAddress` (`bb1` or `0x` form) for per-request isolation; those two are omitted from the tables.
A complete call, as the client sends it and as `bb dev tools call add_approval --args-file ./approval.json` reads it. This adds a public mint of Demo Coin (collection token ID 1) with at most 10 units per address:
```json
{
"sessionId": "demo-coin",
"creatorAddress": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"approvalId": "public-mint",
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"approvalAmounts": {
"overallApprovalAmount": "0",
"perToAddressApprovalAmount": "10",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "public-mint",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
}
```
The tool answers with the approval as stored in the session plus any validation notes.
### Standard Actions
Prefer the high-level builder or action when it fits. The `standard_*` tools cover payment requests, subscriptions, smart tokens, credits, products, auctions, crowdfunds, bounties, and prediction markets. Inputs come from the CLI command definitions, and results preserve the CLI JSON envelope. They query state or prepare unsigned messages; they never sign or broadcast.
```bash
bb dev capabilities standard_pay_requests_pay
bb pay-requests pay 46 --creator "$PAYER" --obligation payment-1 --units 1 --mainnet
```
Equivalent MCP arguments for `standard_pay_requests_pay`:
```json
{
"collectionId": "46",
"creator": "bb1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zql3w7",
"obligation": "payment-1",
"units": "1",
"mainnet": true
}
```
Replace the sample collection and payer. Action amounts are strings, preserving CLI decimal text; units follow that command's help. Signing, credential, endpoint, shell, and file-output arguments are excluded. The adapter starts `bitbadges-cli` directly with literal arguments and checks its capability hash before running the action. Install CLI and MCP from the same SDK version. `BITBADGES_CLI_PATH` can select a trusted installed CLI executable when the client cannot find it on PATH; it is operator configuration, not a tool argument.
For subscriptions, `standard_subscriptions_claim` prepares one payment; `standard_subscriptions_subscribe` also requests recurring consent. Renewal and cancellation require a successful read of existing approvals so unrelated consent is preserved. Do not infer refunds or prorations from cancellation.
For browser handoff use the CLI's `--browser` flow. `list_signing_requests` lists local request IDs; `signing_request_status` accepts `requestId` and optional `resume: true`. Resume returns the original signing URL only while its listener is active. These tools do not initiate payment or verify chain execution. See [request recovery](main-wallet-payments.md#recover-a-request).
### Session Builders
Each tool sets one field on a session-scoped transaction. Calls in the same round can run in parallel. Tools that need the API key say so.
| Tool | What it does | Key params |
| --- | --- | --- |
| `set_standards` | Set the collection's standards array, which selects the site's dedicated views | `standards*` (array, e.g. `["Subscriptions"]`, `["NFTs"]`, `["Smart Token"]`) |
| `set_valid_token_ids` | Set which token ID ranges exist. Fungible tokens and subscriptions use one ID; NFTs use a range | `tokenIds*` (array of ranges) |
| `set_default_balances` | Set default balances for all users. Almost always empty balances with every auto-approve flag true. Mint recipients need a matching incoming approval, an auto-approve flag, or an explicit collection-level override | `defaultBalances*` (object) |
| `set_permissions` | Set collection permissions from a preset or a custom object. Fields are frozen (`permanentlyForbiddenTimes: FOREVER`) or neutral (`[]`) | `preset` (`fully-immutable`, `manager-controlled`, `locked-approvals` default), `permissions` (object, overrides preset) |
| `set_invariants` | Set on-chain invariants. They cannot be removed after creation | `invariants*` (object or null; keys `noCustomOwnershipTimes`, `maxSupplyPerId`, `cosmosCoinBackedPath`) |
| `set_manager` | Set the manager address. Defaults to the creator | `manager*` |
| `set_collection_metadata` | Set name, description, and image. Auto-creates a metadata placeholder URI | `name*`, `description*`, `image*` (`IMAGE_N`, an `https://` or `ipfs://` URL, or a `data:image/svg+xml;base64` URI) |
| `set_token_metadata` | Set metadata for token ID ranges. `{id}` works in the URI only | `tokenIds*`, `name*`, `description*`, `image*` |
| `set_custom_data` | Set the on-chain custom data string (any JSON or text) | `customData*` |
| `set_mint_escrow_coins` | Fund the mint escrow address at creation. Required for quest rewards and escrow payouts where `coinTransfers` use `overrideFromWithApproverAddress` | `coins*` (array; for quests `rewardAmount * maxClaims`) |
| `add_approval` | Add a collection approval: who can transfer what, when, under which conditions. Remove and re-add with the same `approvalId` to replace in place | `approvalId*`, `fromListId*` (`Mint`, `!Mint`, or an address), `toListId` (default `All`), `initiatedByListId`, `tokenIds`, `transferTimes`, `ownershipTimes`, `approvalCriteria` (object, non-default fields only; `overridesFromOutgoingApprovals` must be true for Mint approvals) |
| `add_preset_approval` | Add a collection approval from a named preset instead of hand-writing the approval criteria. Output is identical in shape to `add_approval` | `presetId*` (e.g. `credit-token.scaled`), `params*` (per-preset schema), `overrides` (deep merge; arrays replace, objects merge) |
| `list_presets` | List named approval presets with `presetId`, `name`, `description`, and `paramsSchema` | `skill` (filter by skill id, e.g. `credit-token`) |
| `remove_approval` | Remove a collection approval by id. Order is preserved on re-add | `approvalId*` |
| `set_approval_metadata` | Set a name and description on an approval. Image is always empty for approvals | `approvalId*`, `name*`, `description*` |
| `add_alias_path` | Add an alias path for ICS20-backed tokens or liquidity pools. Required for smart tokens. Decimals must match the IBC denom | `aliasPath*` (object), `pathName`, `pathDescription`, `pathImage`, `denomUnitName`, `denomUnitDescription`, `denomUnitImage` (off-chain, routed to `metadataPlaceholders`) |
| `remove_alias_path` | Remove an alias path by denom | `denom*` |
| `add_cosmos_wrapper_path` | Add a wrapper path that mints and burns a new ICS20 coin from collection tokens. Advanced; most cases want smart tokens, pools, or `coinTransfers`. Approvals for the wrapper address need `allowSpecialWrapping: true` and `mustPrioritize: true` | `wrapperPath*` (object), plus the same off-chain `pathName`, `pathDescription`, `pathImage`, `denomUnitName`, `denomUnitDescription`, and `denomUnitImage` params as `add_alias_path` |
| `remove_cosmos_wrapper_path` | Remove a wrapper path by denom | `denom*` |
| `add_transfer` | Append a `MsgTransferTokens` after the collection message for auto-mint at creation. `collectionId` is set to `"0"` (the new collection). Needs a matching mint approval | `transfers*` (array of from, to, balances, prioritized approval) |
| `remove_transfer` | Remove a transfer message by index. `messages[0]` is the collection and cannot be removed | `index*` (>= 1) |
| `set_is_archived` | Archive or unarchive. Archived collections stay on-chain but are hidden from browsing | `isArchived*` |
| `get_transaction` | Return the assembled transaction JSON with `metadataPlaceholders`. Numbers become strings. Blank `image` fields are auto-filled with a deterministic SVG seeded by the collection name | none |
| `get_review_url` | Final step: upload the transaction to the open preview endpoint and return a short bitbadges.io link the user opens to review and sign. Needs the API key to upload; the person opening the link needs none. Links expire after 1 hour. | `transaction` (defaults to the session), `frontendUrl` |
| `reset_session` | Clear all session state and start from a blank collection. Call it before building a second collection in the same conversation; session state is global and persists, so the new collection would otherwise inherit the previous approvals, metadata, alias paths, and transfers | `sessionId` (omit for the default session) |
`generate_placeholder_art` (`seed*`, `style`, `monogram`) still exists in source but is not in the MCP catalog: `get_transaction` fills blank images for you.
### Helper Builders
| Tool | What it does | Key params |
| --- | --- | --- |
| `build_claim` | Build a claim document for `POST /api/v0/claims`: code-gated, password-gated, whitelist-gated, or open | `claimType*`, `name*`, `maxUses*`, `description`, `numCodes`, `password`, `whitelist`, `maxUsesPerAddress`, `action` (links a collection approval), `showInSearchResults`, `categories` |
| `build_transfer` | Build a `MsgTransferTokens` by querying the collection and constructing the right `prioritizedApprovals` and `coinTransfers`. Supports mint, transfer, deposit (IBC to token), withdraw (token to IBC). Needs the API key | `collectionId*`, `fromAddress*` (`Mint` to mint), `toAddress*`, `tokenIds`, `amount` (default `"1"`), `intent` (`mint`, `transfer`, `deposit`, `withdraw`) |
| `build_dynamic_store` | Build transaction JSON for dynamic stores: create, update, delete, set values. Dynamic stores are on-chain allowlists usable in `dynamicStoreChallenges` | `action*` (`create`, `update`, `delete`, `set_value`, `batch_set_values`), `creator*`, `storeId`, `defaultValue`, `globalEnabled`, `uri`, `customData`, `address`, `value`, `entries` |
### Review and Analysis
| Tool | What it does | Key params |
| --- | --- | --- |
| `review_collection` | Deterministic review of a transaction or on-chain collection. Merges audit, standards, and UX findings into one `ReviewResult` with one verdict. Each finding has `code`, `severity`, `source`, `category`, and localized `title`, `detail`, `recommendation` | `collection*` (message, its value, a `{ messages }` transaction, or a raw collection), `context` (`onChainCollection`, `skipSources`, `hideAgentOnly`) |
| `flag_review_item` | Flag an assumption, substitution, or unsupported request for the user to check before broadcast. Flags surface in the review-and-sign flow | `kind*` (`assumption`, `substitution`, `unsupported_request`, `clarification_needed`, `design_choice`, `other`), `severity*` (`low`, `medium`, `high`), `message*`, `chosen*`, `alternative`, `fieldPath` |
| `explain_collection` | Human-readable explanation with optional Q&A. Covers what it is, how to get tokens, what the manager can change, trust signals, and risk. No API key | `collection*`, `question`, `audience` (`user` default, `developer`, `auditor`) |
| `analyze_collection` | Structured analysis of transferability, approvals, permissions, and how to obtain or transfer tokens. Feeds `MsgTransferTokens` construction. Needs the API key | `collectionId*` |
### Simulation and Validation
| Tool | What it does | Key params |
| --- | --- | --- |
| `simulate_transaction` | Dry-run without broadcasting. Returns raw events, parsed transfer events (coin, token, IBC), and per-address net balance changes. Defaults to the session transaction. Needs the API key | `transaction` or `transactionJson` |
| `validate_transaction` | Check a transaction against the critical rules: numbers as strings, required fields, list IDs. Defaults to the session transaction | `transaction` or `transactionJson` |
### Queries
All query tools need `BITBADGES_API_KEY`.
| Tool | What it does | Key params |
| --- | --- | --- |
| `query_collection` | Fetch a collection. Use `fields` to shrink the response | `collectionId*`, `includeMetadata` (default true), `fields` (array of top-level fields) |
| `query_balance` | Fetch the balance array, or one amount at the current time when `tokenId` is set | `collectionId*`, `address*`, `tokenId` |
| `query_dynamic_store` | Read a dynamic store: details, one address value, a paginated value list, or all stores by creator | `action*` (`get_store`, `get_value`, `list_values`, `list_by_creator`), `storeId`, `address`, `bookmark` |
| `verify_ownership` | Check that an address meets ownership requirements. Shorthand for one collection, or a full `AssetConditionGroup` for `$and` / `$or` / `$not` | `address*`, `collectionId`, `tokenId` (default `"1"`), `tokenIdEnd`, `minAmount` (default `"1"`), `requirements` (JSON string) |
| `search` | Search collections, accounts, and tokens | `query*` |
| `search_plugins` | Find off-chain claim plugins by text, fetch by id, or list a creator's public plugins. Any plugin is fetchable by id without auth | `searchValue`, `pluginIds`, `creatorAddress`, `bookmark` |
| `lookup_token_info` | Symbol, IBC denom, decimals, and pre-generated backing address for a token | `query*` (symbol like `USDC` or an `ibc/...` denom) |
### Component Generators
Stateless helpers that return one piece of a collection.
| Tool | What it does | Key params |
| --- | --- | --- |
| `generate_approval` | Build an approval by pattern | `approvalType*` (`public-mint`, `manager-mint`, `smart-token-backing`, `smart-token-unbacking`, `subscription`, `free-transfer`, `restricted-transfer`), `approvalId*`, `tokenIds`, `backingAddress`, `paymentAmount`, `paymentDenom`, `paymentRecipient`, `maxPerUser`, `totalMax`, `fromListId`, `toListId`, `initiatedByListId` |
| `generate_permissions` | Build a permissions object from a preset | `preset*` (`fully-immutable`, `manager-controlled`, `token-locked`, `custom`), `customPermissions` |
| `generate_backing_address` | Deterministic IBC backing address for a denom, plus list IDs for smart token approvals | `ibcDenom*` (denom or symbol) |
| `generate_alias_path` | Alias path config for swappable tokens and DEX display | `symbol*`, `decimals*`, `tokenId` (default `"1"`), `metadataUri`, `name`, `description` |
| `generate_wrapper_address` | Deterministic wrapper address for a wrapper path denom. No private key; protocol-controlled | `denom*` |
| `generate_unique_id` | Collision-free IDs like `prefix_a1b2c3d4` for new approvals and trackers. Keep original IDs on updates | `prefix*`, `count` (default 1) |
### Utilities
| Tool | What it does | Key params |
| --- | --- | --- |
| `validate_address` | Check an address and detect its chain type | `address*` |
| `convert_address` | Convert between `0x` and `bb1` formats | `address*`, `targetFormat` (`eth`, `bitbadges`) |
| `get_current_timestamp` | Current time in milliseconds with common offsets and durations | `offsetMs`, `offsetDays`, `offsetHours` |
| `diagnose_error` | Map a transaction error to a diagnosis and fix | `error*`, `context` |
| `search_knowledge_base` | Ranked snippets across embedded docs, learnings, recipes, error patterns, and critical rules | `query*`, `category` (`all`, `docs`, `learnings`, `recipes`, `errors`, `rules`) |
### Instructions and Docs
| Tool | What it does | Key params |
| --- | --- | --- |
| `get_skill_instructions` | Build instructions for one skill. Skill ids: `address-list`, `auction`, `auto-mint`, `bb-402`, `bounty`, `burnable`, `credit-token`, `crowdfund`, `custom-2fa`, `fungible-token`, `immutability`, `liquidity-pools`, `minting`, `multi-sig-voting`, `nft-collection`, `payment-protocol`, `payment-request`, `prediction-market`, `product-catalog`, `quest`, `smart-token`, `subscription`, `tradable` | `skillId*` |
| `fetch_docs` | Keyword search over the live docs export on docs.bitbadges.io. Returns the top matching sections | `topic*` |
Rendered skill pages: [Skills](skills/README.md).
## Workflows
### Session-Based Build
Ask your agent:
```text
Build a fungible token called Demo Coin with a 1,000,000 supply cap and a public mint of up to 10 per address. Use the session tools, run validate, review, and simulate in parallel, fix any critical findings, then call get_review_url and give me the link.
```
The prompt above produces this chain:
```text
set_standards + set_valid_token_ids + set_invariants + add_approval + set_permissions + set_default_balances + set_collection_metadata + set_token_metadata
-> (optional) add_transfer (auto-mint at creation)
-> validate_transaction + review_collection + simulate_transaction (in parallel)
-> fix errors with remove_approval + re-add (max 3 attempts)
-> get_transaction (final JSON)
-> get_review_url (link the user opens to review and sign)
```
1. Build. Call the per-field tools in parallel: standards, token IDs, invariants, approvals, permissions, metadata, balances.
2. Auto-mint (optional). Call `add_transfer` to append a `MsgTransferTokens` next to the collection creation.
3. Verify. Call `validate_transaction`, `review_collection`, and `simulate_transaction` in parallel. Fix errors with a targeted `remove_approval` and re-add.
4. Export. Call `get_transaction` for the final JSON.
5. Hand off. Call `get_review_url` and give the user `reviewUrl`. Prefer the link over pasting JSON: it is short and cannot be corrupted in transit.
### Query and Verification (No Signing)
```text
query_collection -> verify_ownership -> (act on the result)
```
### Auto-Mint at Creation
`add_transfer` mints to specific addresses in the same transaction as the collection creation. The transaction then holds two messages: `MsgCreateCollection` and `MsgTransferTokens`. (The session holds it as `MsgUniversalUpdateCollection`; `get_transaction` narrows it on the way out.) Use it for "mint 100 tokens to myself", "distribute tokens to the team", or "auto-mint at creation".
1. Build the collection with a mint approval (`add_approval` with `fromListId: "Mint"` and `initiatedByListId: `).
2. Call `add_transfer` with the recipient addresses, balances, and `prioritizedApprovals` that reference the mint approval.
3. Verify and export as normal. The transaction contains both messages.
Maximum 4 transfer messages per transaction.
## Hand Off to the Browser
The builder never signs or broadcasts. Three exits:
- `get_review_url` returns `reviewUrl`, where the user reviews and signs with a browser wallet. It is backed by a `prv_` code from `POST /api/v0/builder/preview`. Uploading the preview needs `BITBADGES_API_KEY`; opening the returned link needs none, because the unguessable code is the secret. The code expires in 1 hour. `BITBADGES_FRONTEND_URL` or the `frontendUrl` param points the link at testnet or a local site; a testnet `BITBADGES_API_URL` infers `https://testnet.bitbadges.io`.
- Save `get_transaction` output to a file and run `bb preview tx.json --open`, or `bb deploy --browser` / `--burner` from the [CLI](../cli/deploy.md).
- Sign with the [SDK signing client](../sdk/transactions/signing-client.md).
## Resources
The server also exposes embedded documents as MCP resources. Read them with your client's resource support or with `bb dev resources read bitbadges://recipes/all`.
| Resource URI | Name | Description |
| --- | --- | --- |
| `bitbadges://tokens/registry` | Token registry | IBC denoms, symbols, decimals, and pre-generated backing addresses |
| `bitbadges://rules/critical` | Critical rules | Rules every transaction must follow |
| `bitbadges://skills/all` | Skill instructions | Instructions for all builder skills |
| `bitbadges://docs/concepts` | Core concepts | Transferability, approvals, permissions, balances, address lists |
| `bitbadges://docs/examples` | Full examples | Complete transaction JSON for NFT collections, fungible tokens, and smart tokens |
| `bitbadges://recipes/all` | Code recipes and decision matrices | Snippets and decision matrices for common operations |
| `bitbadges://learnings/all` | Learnings and gotchas | Known gotchas, tips, and discoveries |
| `bitbadges://errors/patterns` | Error patterns | Error messages mapped to diagnoses and fixes |
| `bitbadges://docs/frontend` | Reference frontend patterns | Patterns from the reference site (Next.js and Ant Design) |
| `bitbadges://workflows/all` | Workflow chains | Step-by-step tool chains for multi-step operations |
| `bitbadges://schema/token-builder` | Token builder schema | Annotated schema for the session builders: design axes, field reference, approval patterns, validation checklist |
## Call Tools from the CLI
The same registry is reachable from `bb dev` as plain function calls, with no MCP round-trip.
```bash
bb dev tools list # full schemas, as JSON
bb dev tools list --names # `{ ok, data: { names: [...] } }` envelope; pipe to jq -r '.data.names[]'
bb dev tools call get_current_timestamp
bb dev tools call get_skill_instructions --args '{"skillId":"smart-token"}'
bb dev tools call set_collection_metadata --args-file ./metadata.json --session demo
```
| Flag | Description |
| --- | --- |
| `--args ` | Tool arguments as inline JSON |
| `--args-file ` | Tool arguments from a JSON file |
| `--session ` | Session id for stateful tools, persisted to `~/.bitbadges/sessions/.json`. Defaults to `--args.sessionId` or the built-in default session |
| `--raw` | Print the structured result instead of the formatted text block |
Stateful tools (`set_*`, `add_*`, `remove_*`, `get_transaction`) read and write the named session. Sessions survive across invocations, so an agent can compose a collection across many calls:
```bash
SESSION=demo-coin
bb dev tools call set_standards --session $SESSION --args '{"standards":["Fungible Tokens"]}'
bb dev tools call set_valid_token_ids --session $SESSION --args '{"tokenIds":[{"start":"1","end":"1"}]}'
bb dev tools call set_collection_metadata --session $SESSION --args '{"name":"Demo Coin","description":"One million units of token ID 1.","image":"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/coin.png"}'
bb dev tools call add_approval --session $SESSION --args-file ./approval.json # the public-mint call from the Tools section
bb dev tools call get_transaction --session $SESSION
```
Unknown tool names exit `1` and print the available tools on stderr.
```bash
bb session list # session ids on disk
bb session show demo # snapshot as JSON
bb session reset demo # delete the file
bb dev resources list # full metadata as JSON
bb dev resources list --uris # URIs only
bb dev resources read bitbadges://recipes/all
```
Flag-based template builders (`bb build `) are faster than composing tool calls when a template fits: [Build](../cli/build.md).
## Related
- [Programmatic Agent](programmatic-agent.md)
- [Claude Code Plugin](claude-code-plugin.md)
- [Skills](skills/README.md)
- [Dev commands](../cli/dev.md)
## File: agents/programmatic-agent.md
---
description: "Build collections from natural-language prompts in Node with BitBadgesBuilderAgent and your own Anthropic or OpenAI key: config, hooks, sessions, handoff."
---
# Programmatic Agent
`BitBadgesBuilderAgent` builds BitBadges collections from natural-language prompts inside your Node process, with your own Anthropic or OpenAI key. BitBadges never sees the key and never proxies the requests.
```ts
import { BitBadgesBuilderAgent } from 'bitbadges/builder/agent';
const agent = new BitBadgesBuilderAgent({ anthropicKey: process.env.ANTHROPIC_API_KEY });
const result = await agent.build('create a subscription token for $10/month, max 500 subscribers');
console.log(result.toString());
// BitBadgesBuilderAgent build: 2 message(s), valid, 18,204 tokens, $0.0732, 5 round(s)
console.log(result.transaction);
console.log(result.reviewUrl); // open in a browser to review and sign
```
This is the scriptable counterpart to the [MCP Builder Tools](mcp-tools.md).
| | `bb` CLI | MCP builder | Programmatic agent |
| --- | --- | --- | --- |
| Where it runs | Any shell | Claude Code, Cursor, Codex | Your Node process |
| Model | None; deterministic templates | Your harness supplies the model | Your Anthropic or OpenAI key |
| Good for | Scripts, CI, one-off builds | Interactive and exploratory work | Dapps, bots, games, fine-tuning |
## Install
Install the SDK plus the provider you use. Both providers are optional peer dependencies; install one.
```bash
# Anthropic (default)
npm install bitbadges @anthropic-ai/sdk
# OpenAI
npm install bitbadges openai
```
The SDK never bundles either provider. The key stays in your process.
```bash
# Pick one: Anthropic (default) or OpenAI. Values shown are fake.
export ANTHROPIC_API_KEY=sk-ant-api03-0123456789abcdef0123456789abcdef
export OPENAI_API_KEY=sk-proj-0123456789abcdef0123456789abcdef
# Optional: needed when prompts trigger query, search, or simulate tools. Key from https://bitbadges.io/developer
export BITBADGES_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
```
Anthropic and OpenAI keys are required only for `BitBadgesBuilderAgent`, the Node-side build loop on this page. The MCP server (`bitbadges-builder`, used by Cursor, Claude Desktop, Claude Code, Cline, OpenAI Codex, and Gemini Code Assist) is model-agnostic and does not read these variables. If you only want the MCP server, go to [MCP Builder Tools](mcp-tools.md).
## Zero-Config
### Anthropic (Default)
```ts
import { BitBadgesBuilderAgent } from 'bitbadges/builder/agent';
const agent = new BitBadgesBuilderAgent({ anthropicKey: process.env.ANTHROPIC_API_KEY });
const result = await agent.build('create a subscription token for $10/month, max 500 subscribers');
console.log(result.transaction);
```
### OpenAI
```ts
import { BitBadgesBuilderAgent } from 'bitbadges/builder/agent';
const agent = new BitBadgesBuilderAgent({
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY
});
const result = await agent.build('create a subscription token for $10/month, max 500 subscribers');
console.log(result.transaction);
```
Both providers run the same loop: same tools, same validation, same review pass, same token-type inference. The dispatcher translates the Anthropic-style internal message format to and from OpenAI's chat-completions shape at the API boundary.
Token-type inference has parity across providers. Both run a fast classifier (Anthropic Haiku or OpenAI `gpt-4o-mini`) before each build to pick the token-type skill. OpenAI uses native structured outputs (`response_format: json_schema, strict: true`), so the JSON contract is server-enforced.
## Auth Modes
### Anthropic
```ts
import Anthropic from '@anthropic-ai/sdk';
import { BitBadgesBuilderAgent } from 'bitbadges/builder/agent';
// 1. API key (most common)
new BitBadgesBuilderAgent({ anthropicKey: process.env.ANTHROPIC_API_KEY });
// 2. OAuth token, for Claude Code / Claude Pro flows
new BitBadgesBuilderAgent({ anthropicAuthToken: process.env.ANTHROPIC_OAUTH_TOKEN });
// 3. Pre-built Anthropic client, for custom retry or interceptor logic
new BitBadgesBuilderAgent({
anthropicClient: new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, baseURL: 'https://llm-gateway.example.com', maxRetries: 5 })
});
```
### OpenAI
```ts
import OpenAI from 'openai';
import { BitBadgesBuilderAgent } from 'bitbadges/builder/agent';
// 1. API key (most common)
new BitBadgesBuilderAgent({ provider: 'openai', apiKey: process.env.OPENAI_API_KEY });
// 2. Custom base URL, for Azure OpenAI, proxies, gateways
new BitBadgesBuilderAgent({ provider: 'openai', apiKey: process.env.OPENAI_API_KEY, baseURL: 'https://llm-gateway.example.com/v1' });
// 3. Pre-built OpenAI client, for custom retry, interceptor, or Azure AD logic
new BitBadgesBuilderAgent({
provider: 'openai',
providerClient: new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: 'https://llm-gateway.example.com/v1', maxRetries: 5 })
});
```
Environment variables are read when no explicit credentials are passed:
- Anthropic: `ANTHROPIC_API_KEY`, `ANTHROPIC_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN`
- OpenAI: `OPENAI_API_KEY`
- Shared: `BITBADGES_API_KEY`, `BITBADGES_API_URL`
## Customization
```ts
import { BitBadgesBuilderAgent, MemoryStore } from 'bitbadges/builder/agent';
const agent = new BitBadgesBuilderAgent({
// Provider: pick one of the auth-mode patterns above
anthropicKey: process.env.ANTHROPIC_API_KEY,
bitbadgesApiKey: process.env.BITBADGES_API_KEY,
model: 'sonnet', // Anthropic: 'haiku' | 'sonnet' (default) | 'opus'.
// OpenAI: pass a model id directly, e.g. 'gpt-4o' / 'gpt-4o-mini'.
validation: 'strict', // 'strict' | 'lenient' | 'off'
skills: ['subscription', 'fungible-token'], // limit the skill set
systemPromptAppend: 'Always use locked-approvals permissions.', // adds to the base prompt
maxRounds: 8, // agent loop cap
fixLoopMaxRounds: 3, // validation fix cap
autoInferTokenType: true, // default: token-type detection; false disables it
sessionStore: new MemoryStore(), // MemoryStore | FileStore | your own KVStore
hooks: {
onTokenUsage: (u) => console.log(`$${u.cumulativeCostUsd.toFixed(4)}`),
onToolCall: (e) => console.log(`[${e.name}] ${e.durationMs}ms`),
onStatusUpdate: (s) => console.log(`status: ${s}`), // "Building", "Validating", and so on
onLog: (e) => console.log(`[${e.type}] ${e.label}`), // info / ai_text / validation / error
onCompletion: (trace) => console.log(`done in ${trace.rounds} rounds`)
},
defaultCreatorAddress: 'bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d',
debug: false // true dumps prompts and responses to stderr
});
```
### Hook Contract
- `onTokenUsage` is load-bearing. It is awaited, and rejections propagate out of `build()`. Throw from it to enforce per-build quotas (the BitBadges API does this with its token ledger).
- `onCompletion` fires exactly once per `build()`, on success and on error, so cleanup runs either way.
- `onToolCall`, `onStatusUpdate`, and `onLog` are fire-and-forget observability hooks. Rejections are swallowed so a broken logger cannot hang a build.
- `onLog` receives `{ type: 'info' | 'ai_text' | 'validation' | 'error', label, data }` entries: round boundaries, the LLM's text responses, and validation-gate pass or fail. Useful for live dev consoles and audit log persistence.
### Validation Modes
- `'strict'` (default) throws `ValidationFailedError` if hard errors remain after the fix loop.
- `'lenient'` always returns. `result.valid` is false and hard errors are in `result.errors`.
- `'off'` skips the gate. Use only for experiments.
### Skills
Two inputs at two levels:
```ts
new BitBadgesBuilderAgent({
anthropicKey,
skills: ['nft-collection', 'smart-token'] // allowlist, constructor-level filter
});
await agent.build('mint 100 nfts', {
selectedSkills: ['nft-collection'] // actual injection for this build
});
```
`agent.listSkills()` returns every available skill (filtered by the constructor allowlist when set). `agent.describeSkill(id)` returns one by id. Discovery is code-only; there is no public marketplace endpoint. An unknown id in `selectedSkills` is dropped silently with no build failure. Set `debug: true` to log dropped ids.
#### How skill content is injected by mode
| Build mode | What gets injected |
| --- | --- |
| `create` | Full skill instructions with build recipes. The LLM follows them to construct a new collection from scratch |
| `update` / `refine` | Summaries only, with an explicit "do not rebuild the collection to match the skill" warning. The collection already exists on-chain; skills are reference context, not a blueprint |
If an update build rewrites too aggressively, drop skills from the per-build call. The agent then falls back to the generic `DOMAIN_KNOWLEDGE` guidance.
#### Token-type inference (auto-pick)
When the caller supplies no token-type skill, the agent classifies the prompt and prepends one high-confidence pick, or builds freestyle when nothing matches with confidence.
```ts
new BitBadgesBuilderAgent({
anthropicKey,
autoInferTokenType: true // default; false disables it
});
const result = await agent.build('monthly subscription for $10/mo');
result.inferredTokenType; // 'subscription', or null (freestyle), or undefined (skipped)
result.inferredTokenTypeSource; // 'standards' (existing-collection fast path) | 'llm'
result.inferredTokenTypeReasoning; // one-sentence rationale
```
Inference is skipped when `selectedSkills` already contains a token-type entry; explicit picks win. Non-token-type skills (additional-context) do not block inference. The type table lives in [Smart Tokens and Vaults](../guides/smart-tokens-and-vaults.md).
### Prompt-Injection Guard on the System-Prompt Slots
`systemPromptAppend` (additive) and `systemPrompt` (full replace) both pass through an injection-pattern check at construction. If either contains an obvious "ignore all previous instructions" or "you are now a..." payload, the constructor throws a `BitBadgesBuilderAgentError` with code `INVALID_SYSTEM_PROMPT_APPEND` or `INVALID_SYSTEM_PROMPT`. Hosted deployments that accept end-user input into these slots should still run their own `containsInjection` check at the trust boundary. The SDK check is defense in depth, not a replacement.
### Custom Tools
Add tools on top of the built-ins, or filter built-ins out:
```ts
new BitBadgesBuilderAgent({
anthropicKey,
tools: {
remove: ['build_claim'],
add: [{
definition: {
name: 'lookup_sku',
description: 'Return the price in USDC for a product SKU from the merchant catalog.',
input_schema: { type: 'object', properties: { sku: { type: 'string' } }, required: ['sku'] }
},
execute: async (args, ctx) => ({ sku: args.sku, priceUsdc: '25', requestedBy: ctx.sessionId })
}]
}
});
```
## Session Stores
Conversation messages and token counters persist so refinement works across HTTP requests.
```ts
import Redis from 'ioredis';
import { BitBadgesBuilderAgent, MemoryStore, FileStore, type KVStore, type KVStoreSetOptions } from 'bitbadges/builder/agent';
const anthropicKey = process.env.ANTHROPIC_API_KEY;
// Default: single process, in memory
new BitBadgesBuilderAgent({ anthropicKey, sessionStore: new MemoryStore() });
// Disk-backed: survives process restarts (default dir: ~/.bitbadges/agent-sessions)
new BitBadgesBuilderAgent({ anthropicKey, sessionStore: new FileStore({ dir: '/var/lib/bb' }) });
// Bring your own: any object matching the KVStore interface
class RedisStore implements KVStore {
private redis = new Redis(process.env.REDIS_URL!);
async get(key: string): Promise {
return this.redis.get(key);
}
async set(key: string, value: string, opts?: KVStoreSetOptions): Promise {
if (opts?.ttlSeconds) {
await this.redis.set(key, value, 'EX', opts.ttlSeconds);
} else {
await this.redis.set(key, value);
}
}
async delete(key: string): Promise {
await this.redis.del(key);
}
}
new BitBadgesBuilderAgent({ anthropicKey, sessionStore: new RedisStore() });
```
Pass the same `sessionId` across `.build()` calls to continue a session, for example for refinement.
## Result Shape
```ts
interface BuildResult {
valid: boolean;
transaction: any; // parsed object, not a JSON string
reviewUrl: string; // bitbadges.io link: review and sign this tx in the browser
errors: StructuredError[]; // code, message, path?, fixHint?
warnings: Warning[]; // non-fatal advisory notes
advisoryNotes: string[]; // raw review findings
validation: any; // SDK validator result
simulation: any | null; // null when no simulator is configured
audit: any | null; // review findings and summary
tokensUsed: number;
costUsd: number; // computed per selected model
rounds: number;
fixRounds: number;
trace: BuildTrace; // full messages, tool calls, prompt hash
toString(): string; // human-readable one-liner
}
```
Errors dispatch on `instanceof`:
```ts
import {
BitBadgesBuilderAgentError,
ValidationFailedError,
QuotaExceededError,
AnthropicAuthError,
AbortedError,
PeerDependencyError,
SimulationError
} from 'bitbadges/builder/agent';
```
## Review and Sign in the Browser
The SDK never signs for the user. `result.reviewUrl` is a bitbadges.io link that opens the transaction in the review-and-sign flow (Preview, Review Items, Transferability, Permissions, then wallet signature). The whole transaction rides in the URL hash (`#tx=`), so constructing the link needs no preview upload. Loading the site, fetching chain state, and signing or broadcasting still require network access. Set `BITBADGES_FRONTEND_URL` to point at testnet or a local site.
```ts
const result = await agent.build('create a subscription token for $10/mo');
console.log(result.reviewUrl);
// https://bitbadges.io/mint/local-builder#tx=
// For a one-message transaction the hash part looks like:
// eyJtZXNzYWdlcyI6W3sidHlwZVVybCI6Ii90b2tlbml6YXRpb24uTXNnRGVsZXRlT3V0Z29pbmdBcHByb3ZhbCIsInZhbHVlIjp7ImNyZWF0b3IiOiJiYjFwMHJyZWwzMzY1c2NhZHE1azlwdjB4MHpwOWoyMmpzNmRudzcwZCIsImNvbGxlY3Rpb25JZCI6IjIiLCJhcHByb3ZhbElkIjoiYWdlbnQtZGFpbHktYnVkZ2V0In19XX0
```
For a short, shareable link (chat, email, an LLM relaying it to a user), upload through the open preview endpoint instead. This is what the MCP `get_review_url` tool and `bb preview` do:
```ts
import { buildReviewUrlFromCode } from 'bitbadges/builder/agent';
const res = await fetch('https://api.bitbadges.io/api/v0/builder/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transaction: result.transaction })
});
const { code } = await res.json(); // for example prv_ab12cd34, valid 1 hour
console.log(buildReviewUrlFromCode('https://bitbadges.io', code, result.transaction));
// https://bitbadges.io/mint/local-builder?code=prv_ab12cd34
```
Update transactions (a non-zero `collectionId`) route to `/update/local-builder/:id` so the site diffs against on-chain state. The helpers `buildHandoffUrl`, `buildReviewUrlFromCode`, `detectExistingCollectionId`, and `encodeTxForHash` are exported from `bitbadges/builder/agent`.
## Image Placeholders
Two image-handling modes. Pick the one that fits your pipeline.
### 1. Real URLs in the Prompt
If you already host the images, put the URLs in the prompt. The LLM emits them verbatim into `metadataPlaceholders` entries.
```ts
await agent.build(
`Create an NFT collection with hero image https://cdn.example.com/hero.png and
token art https://cdn.example.com/t1.png`
);
```
No post-processing. The LLM has to copy the URLs faithfully, so keep them short and well-formed.
### 2. Placeholders Plus Post-Build Substitution
When the user is still choosing or uploading images at build time, use symbolic placeholders and swap them in after the build:
```ts
const result = await agent.build(prompt, {
availableImagePlaceholders: ['IMAGE_1', 'IMAGE_2']
});
const finalTx = agent.substituteImages(result.transaction, {
IMAGE_1: 'https://cdn.example.com/hero.png', // or a data: URL
IMAGE_2: 'ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/1.png'
});
```
The LLM wires `IMAGE_N` tokens into the metadata; you resolve them at the end. This matches the hosted site's flow.
### Detecting Stragglers
`agent.collectImageReferences(tx)` returns every `IMAGE_N` token still in the transaction. Use it as a pre-broadcast check: anything it returns is a placeholder that never got a real value and would land on-chain as-is.
## Health Check
```ts
const report = await agent.healthCheck();
// { anthropic: { ok: true, model: 'claude-sonnet-4-6' },
// bitbadgesApi: { ok: true, configured: true } }
```
## Validate Without Building
```ts
import { readFileSync } from 'node:fs';
const existing = JSON.parse(readFileSync('./tx.json', 'utf8'));
const { valid, errors, simulation } = await agent.validate(existing);
```
## Export as a Single Prompt for No-Tools LLMs
To hand the build to Claude.ai, ChatGPT, or Gemini (no tools there), `agent.exportPrompt()` assembles the no-tools variant of the system prompt concatenated with the user message. The LLM emits the final transaction JSON directly.
```ts
const { prompt } = await agent.exportPrompt(
'create a subscription token for $10/mo',
{ selectedSkills: ['subscription'] }
);
// Paste `prompt` into Claude.ai / ChatGPT / Gemini. The output is
// a { messages: [{ typeUrl, value }] } JSON object. Paste it into
// https://bitbadges.io/mint/local-builder ("Bring your transaction")
// to review and sign.
```
No Anthropic call is made. No validation, no simulation, no fix loop. This is a prompt-assembly helper and a best-effort path; `build()` remains the quality-gated path.
## Abort
```ts
const controller = new AbortController();
const result = agent.build(prompt, { abortSignal: controller.signal });
setTimeout(() => controller.abort(), 30_000);
// or: agent.abort()
```
## Cancellation and Streaming
- Cancellation: supported with `abortSignal` or `agent.abort()`.
- Streaming: not in v1. The agent returns when the build completes or throws. Use the `onTokenUsage` and `onToolCall` hooks for live progress.
## Prompt Caching (Automatic)
The agent uses Anthropic prompt caching on the stable prefix (system prompt, tool schemas, and inlined skill instructions). Builds inside a 5-minute window read those tokens from cache at about 10% of the regular input-token cost. Cache-creation tokens cost about 1.25x regular input on the miss. One hit pays the miss back; every hit after that is a saving.
Caching is on by default with nothing to configure. Skill ordering is canonicalized (alphabetical), so `['nft', 'subscription']` and `['subscription', 'nft']` hit the same cache key.
### When Caching Pays Off
The stable prefix is typically 10-15% of the per-build token count. The rest is dynamic user context and tool-calling round trips.
- First build with a new skill set: cache miss. You pay 1.25x on the prefix tokens. Net: a few cents more than no cache for a typical build.
- Second build within 5 minutes with the same skill set: cache hit. Prefix tokens cost 10% of full rate. Break-even lands about here.
- Steady state (several builds an hour with overlapping skill sets): cache-read tokens dominate the input count on `result.trace`. Savings depend on the fraction of input tokens actually read from cache. If only 10-15% is cacheable, a 90% discount on that fraction reduces total input cost by about 9-13.5%, before cache-write costs.
One-off scripts that run a single build pay the 1.25x write premium with no recovery. The delta is cents, so leave caching on, but do not count it as a headline optimization for low-volume use.
### Observability
The `onTokenUsage` hook reports cache counters per round:
```ts
new BitBadgesBuilderAgent({
anthropicKey,
hooks: {
onTokenUsage: (u) => {
console.log(
`round ${u.round}: ${u.inputTokens} in, ${u.outputTokens} out, ` +
`cache ${u.cacheReadTokens} read / ${u.cacheCreationTokens} write, ` +
`cumulative $${u.cumulativeCostUsd.toFixed(4)}`
);
}
}
});
```
`result.trace.cacheReadTokens` and `result.trace.cacheCreationTokens` carry cumulative counts for the whole build. A healthy steady state has `cacheReadTokens >> inputTokens`.
### What Invalidates the Cache
- 5-minute TTL since the last hit.
- Any change to the system prompt (for example a `systemPromptAppend` edit).
- Any change to the tool set (`tools.add` or `tools.remove`).
- Any change to the canonical skill set.
The per-request tail (request header, metadata, prompt text, refinement history) is never cached; it is expected to vary.
## Internals (Unstable Primitives)
To run your own loop (a different LLM, a custom strategy, fine-tuning data collection):
```ts
import {
buildSystemPrompt, DOMAIN_KNOWLEDGE, SKILL_INSTRUCTIONS,
runAgentLoop, runValidationGate, buildFixPrompt,
createAgentToolRegistry
} from 'bitbadges/builder/internals';
```
Not covered by semver. Anything here may be renamed or removed in a minor release. Use `bitbadges/builder/agent` (the stable path) whenever possible.
## Examples
Runnable scripts at [bitbadgesjs/packages/bitbadgesjs-sdk/examples/builder-agent/](https://github.com/BitBadges/bitbadgesjs/tree/main/packages/bitbadgesjs-sdk/examples/builder-agent):
- `zero-config.ts`: the 5-line sample
- `middle-tier.ts`: hooks, skills, file store, typed errors
- `diy-internals.ts`: OpenAI through `/internals` (unsupported)
## Troubleshooting
- `PeerDependencyError: @anthropic-ai/sdk is required`: run `npm install @anthropic-ai/sdk`.
- `Anthropic credentials are required`: set `ANTHROPIC_API_KEY` or pass `anthropicKey` / `anthropicAuthToken` to the constructor.
- `ValidationFailedError` after 3 fix rounds: the fix loop gave up. Inspect `err.errors` for structured causes and `err.advisoryNotes` for design concerns the agent considered but did not resolve. Raising `fixLoopMaxRounds` rarely helps; the prompt usually needs more constraints.
- Simulation reports a `jsonToTxBytes` error: encoding failed before the chain could evaluate the transaction. Check the message envelope, type URL, numeric strings, and SDK version, then re-run simulation; this error is not evidence that broadcast will succeed.
## Related
- [MCP Builder Tools](mcp-tools.md) (same tools, different runtime)
- [Agents](README.md) (terminal-first workflow)
- [SDK](../sdk/README.md)
## File: agents/reading-the-docs.md
---
description: "How an agent reads these docs. llms.txt, the full-text dump, bb dev docs, the fetch_docs MCP tool, and the URL patterns of this site."
---
# Reading the Docs
Agents can read this site four ways: two static files, one CLI command, and one MCP tool. Pick the one your harness already has. Every page is also served as Markdown at its URL plus `.md` (for example `https://docs.bitbadges.io/agents/setup.md`), and the Copy control at the top right of each page copies that same Markdown or a prompt that points an agent at it.
```bash
# Curated index (about 51 KB): start here, then fetch 1-3 pages
curl -sO https://docs.bitbadges.io/llms.txt
# Full-text dump of every page (about 1.9 MB); /for-llms.txt is the same file
curl -sO https://docs.bitbadges.io/llms-full.txt
# One page as Markdown
curl -s https://docs.bitbadges.io/agents/setup.md
# From the CLI (fetched from GitHub, cached 24 hours in ~/.bitbadges/docs-cache.json)
bb dev docs # navigable tree of sections
bb dev docs all # the full dump
bb dev docs messages/msg-transfer-tokens
bb dev docs approvals # partial match: first section containing "approvals"
bb dev docs --refresh # clear the cache
# Builder skills (shorthand for docs builder-skills)
bb dev skills
bb dev skills smart-token
```
## Download the Corpus
Two files, both regenerated with the docs. Every page of this site carries the same two links in its sidebar under **For agents**, with the current byte size next to each.
| Download | Size | What it is |
| --- | --- | --- |
| [llms.txt](https://docs.bitbadges.io/llms.txt) | about 51 KB | A curated index: every page in this site's navigation order, one line each, with its URL and one-sentence description. No page bodies. The generated SDK and proto reference trees are one line each rather than thousands |
| [llms-full.txt](https://docs.bitbadges.io/llms-full.txt) | about 1.9 MB | The whole corpus (216 pages, roughly 49,000 lines) concatenated into one plain-text file. Also served as [for-llms.txt](https://docs.bitbadges.io/for-llms.txt) |
**Which one to feed your agent:**
- **Context-limited agent, or one that can fetch URLs:** give it `llms.txt`. It is much smaller than the full corpus, and every line carries the URL of the page to fetch next. This is the default.
- **One-shot paste, or an agent with a large context and no network:** give it `llms-full.txt`. Roughly 400k tokens; paste it whole, attach it as a file, or `grep` it locally and paste the sections you hit.
Both are plain text with no markup beyond the markdown the pages are written in, so they need no preprocessing.
## CLI
`bb dev docs [section]` browses the same content offline after the first fetch. Sections nest with slashes (`bb dev docs token-standard/approval-criteria/merkle-challenges`). A single word with no exact match does partial matching. Reference: [Dev commands](../cli/dev.md).
## MCP Tool
`fetch_docs({ topic })` runs a keyword search over the live docs export and returns the top matching sections. `search_knowledge_base({ query, category })` searches the embedded docs, learnings, recipes, error patterns, and critical rules that ship inside the builder. Both are listed on [MCP Builder Tools](mcp-tools.md#instructions-and-docs).
## URL Patterns
Pages follow predictable paths, so an agent can guess a URL without an index.
| Pattern | Example |
| --- | --- |
| `/token-standard/messages/msg-` | `/token-standard/messages/msg-transfer-tokens` |
| `/token-standard/queries/` | `/token-standard/queries/get-balance` |
| `/token-standard/concepts/` | `/token-standard/concepts/balances`, `/token-standard/concepts/transferability` |
| `/token-standard/approval-criteria/` | `/token-standard/approval-criteria/approval-trackers` |
| `/cli/` | `/cli/build`, `/cli/deploy`, `/cli/analyze`, `/cli/dev` |
| `/api` and `/api-reference` | `/api/claims`, `/api/sign-in/setup`; `/api-reference` is the OpenAPI reference |
| `/agents/` | `/agents/mcp-tools`, `/agents/skills/smart-token` |
| `/guides/` | `/guides/create-a-collection`, `/guides/gate-access` |
Every page carries a `description:` frontmatter line, one `# H1`, and code before prose. Message names, query names, and CLI command groups use the exact casing from source.
## Related
- [Agents](README.md)
- [Dev commands](../cli/dev.md)
- [Skills](skills/README.md)
## File: agents/setup.md
---
description: "Set up your AI harness for BitBadges: the MCP server command and the config for Claude Code, Claude Desktop, Cursor, Windsurf, Codex CLI, VS Code, and Zed."
---
# Set Up Your AI
Start with the CLI. Add the plugin or MCP configuration below when your client needs it; neither is required for a terminal agent.
| Task | Needed |
| --- | --- |
| Discover standards and build unsigned proposals | SDK CLI (`bitbadges-cli`) |
| Use native Cosmos commands | Combined `bb` CLI and chain binary |
| Call tools through an MCP client | Installed `bitbadges-builder` binary |
| Query remote data or simulate | API access for the selected network |
| Ask a human to sign | Browser wallet and a running CLI process |
The combined installer provides the CLI and chain tools:
```bash
curl -fsSL https://install.bitbadges.io | sh
bb --version
bb dev skills
```
If only the SDK package is installed, use `bitbadges-cli dev skills` instead. Offline discovery and construction do not need an API key or wallet. Configure API access locally when needed; see [CLI setup](../cli/README.md). Do not paste keys into an agent conversation or commit them in project MCP configuration.
The MCP server command uses the binary from that same installation:
```bash
bitbadges-builder
```
The configurations below do not install or update packages when the client starts. The server uses the CLI's configured API access or environment overrides; see [MCP configuration](mcp-tools.md#environment-variables). If a desktop client cannot find the command, use the absolute path to the installed binary and restart the client. Browser signing does not require giving the server a private key or mnemonic.
## Claude Code
Plugin (auto-wires the server, adds 8 skills and two slash commands):
```text
/plugin marketplace add BitBadges/bitbadges-plugin
/plugin install bitbadges
/bitbadges:setup
```
Plain MCP, no plugin:
```bash
claude mcp add bitbadges-builder -- bitbadges-builder
```
Choose the plugin or a manual registration. If you already have both, inspect the registrations before removing the redundant one. Details: [Claude Code Plugin](claude-code-plugin.md).
## Claude Desktop
`claude_desktop_config.json`:
```json
{
"mcpServers": {
"bitbadges-builder": {
"command": "bitbadges-builder",
"args": []
}
}
}
```
## Cursor
`.cursor/mcp.json` in the project (or `~/.cursor/mcp.json` for all projects):
```json
{
"mcpServers": {
"bitbadges-builder": {
"command": "bitbadges-builder",
"args": []
}
}
}
```
## Windsurf
`~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"bitbadges-builder": {
"command": "bitbadges-builder",
"args": []
}
}
}
```
## Codex CLI
Add this table to `~/.codex/config.toml` (do not replace your existing configuration). Codex uses TOML, not the JSON `mcpServers` format used by some other clients. Export `BITBADGES_API_KEY` before launching Codex so `env_vars` can forward it:
```toml
[mcp_servers.bitbadges-builder]
command = "bitbadges-builder"
args = []
env_vars = ["BITBADGES_API_KEY"]
```
Restart Codex after editing, then use `/mcp` to inspect the server connection. See the [official OpenAI MCP documentation](https://developers.openai.com/codex/mcp) for configuration options.
## VS Code and GitHub Copilot
`.vscode/mcp.json` in the workspace:
```json
{
"servers": {
"bitbadges-builder": {
"type": "stdio",
"command": "bitbadges-builder",
"args": []
}
}
}
```
## Zed
`settings.json` (`zed: open settings`):
```json
{
"context_servers": {
"bitbadges-builder": {
"source": "custom",
"command": "bitbadges-builder",
"args": []
}
}
}
```
## Any LLM Without Tools
Claude.ai, ChatGPT, Gemini, or any chat model with no MCP access can still build. Ask it for the transaction JSON and take that JSON to the site to review and sign.
1. Give the model context: paste the relevant [skill page](skills/README.md), or the prompt from `agent.exportPrompt()` on the [Programmatic Agent](programmatic-agent.md#export-as-a-single-prompt-for-no-tools-llms).
2. Ask for the transaction object and nothing else:
```text
Return only a JSON object of the form { "messages": [{ "typeUrl": "/tokenization.MsgCreateCollection", "value": { "creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d", ... } }] } with the rest of value filled in. No prose, no code fence.
```
3. Paste it into `https://bitbadges.io/mint/local-builder` ("Bring your transaction"). Or, if you have the CLI, `bb preview tx.json --open` gives you a short review link, and a `#tx=` link opens the same page with the transaction in the URL hash.
A complete one-message example the model can return, revoking an outgoing approval:
```json
{
"messages": [
{
"typeUrl": "/tokenization.MsgDeleteOutgoingApproval",
"value": {
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "2",
"approvalId": "agent-daily-budget"
}
}
]
}
```
The same transaction as a hash link, which any chat can hand to the user:
```text
https://bitbadges.io/mint/local-builder#tx=eyJtZXNzYWdlcyI6W3sidHlwZVVybCI6Ii90b2tlbml6YXRpb24uTXNnRGVsZXRlT3V0Z29pbmdBcHByb3ZhbCIsInZhbHVlIjp7ImNyZWF0b3IiOiJiYjFwMHJyZWwzMzY1c2NhZHE1azlwdjB4MHpwOWoyMmpzNmRudzcwZCIsImNvbGxlY3Rpb25JZCI6IjIiLCJhcHByb3ZhbElkIjoiYWdlbnQtZGFpbHktYnVkZ2V0In19XX0
```
The site runs review, transferability, and permissions checks before the wallet signature, so an unvalidated model output still gets the same review as a tool-built one.
## Verify
```bash
bb doctor # chain binary, CLI, API key, network
bb dev tools list --names | jq -r '.data.names[]' # the same registry your client sees
```
In an MCP client, run this prompt and inspect the actual tool-call result, rather than a prose answer alone:
```text
Call get_current_timestamp and show its returned timestamp.
```
For a first task, ask: “Prepare an unsigned invoice for one payer. Use the installed schema, tell me any missing payment terms, and save the proposal for review.” Once the terms are correct, use the [main-wallet payment flow](main-wallet-payments.md) to request a signature. A successful discovery call verifies connectivity, not permission to spend.
## Related
- [MCP Builder Tools](mcp-tools.md)
- [Claude Code Plugin](claude-code-plugin.md)
- [Agents](README.md)
## File: agents/skills/README.md
---
description: "The builder skills: the instruction text AI agents load to build a specific token type or feature on BitBadges."
---
# Builder skills
Reference copy of the skill instructions that agents load before they build a collection, for people who want to read what the agent reads.
A skill is a block of instruction text for one token type or feature: required standards, the approvals to create, the invariants to set, the tool call order, and the mistakes to avoid. The text lives in the SDK at `packages/bitbadgesjs-sdk/src/builder/resources/skillInstructions.ts`. The MCP builder tools serve it through `get_skill_instructions`, and the Claude Code plugin loads it when a skill is selected. These pages are generated from that file.
## Fetch a skill
```bash
bb dev skills # list every skill id
bb dev skills smart-token
```
MCP builder tools:
```json
{ "tool": "get_skill_instructions", "arguments": { "skillId": "smart-token" } }
```
## Skills
| Skill | Category | What it covers |
| --- | --- | --- |
| [Address List](address-list.md) | Token type | On-chain address list where membership = owning x1 of token ID 1. |
| [Auction](auction.md) | Token type | Single-item auction with intent-based bidding. |
| [Bounty](bounty.md) | Token type | Escrow-based bounty with verifier arbitration. |
| [Credit Token](credit-token.md) | Token type | Increment-only, non-transferable credit token purchased with any ICS20 denom. |
| [Crowdfund](crowdfund.md) | Token type | On-chain crowdfunding with goal tracking via mustOwnTokens. |
| [Custom 2FA](custom-2fa.md) | Token type | Two-factor authentication for transfers using a secondary approval address |
| [Fungible Token](fungible-token.md) | Token type | Simple fungible token with fixed or unlimited supply and configurable mint/transfer approvals |
| [Liquidity Pools](liquidity-pools.md) | Token type | Liquidity pool standard with the "Liquidity Pools" protocol standard tag, used for tradable assets that can be swapped on a DEX |
| [NFT Collection](nft-collection.md) | Token type | Non-fungible token collection with unique token IDs, metadata URIs, and badge-based ownership |
| [Payment obligations and reusable links](payment-obligations.md) | Token type | Versioned invoice obligations, payer groups, installments, partial targets, split payouts and reusable payment links built from native approvals. |
| [Payment Protocol](payment-protocol.md) | Token type | Invoices, escrows, bounties, milestones, and multi-party agreements using coinTransfer-based approvals or IBC-backed smart token escrow |
| [PaymentRequest](payment-request.md) | Token type | Agent-initiated payment request with no escrow. |
| [Prediction Market](prediction-market.md) | Token type | Binary prediction market with YES/NO outcome tokens, liquidity pool trading, and vote-based settlement |
| [Products](product-catalog.md) | Token type | Multi-product storefront with per-product pricing, supply limits, and optional burn-on-purchase. |
| [Quest](quest.md) | Token type | Quest/reward collection: users complete criteria and claim a badge + coin payout |
| [Smart Token](smart-token.md) | Token type | IBC-backed smart token with 1:1 backing and two required approvals (backing + unbacking) |
| [Subscription](subscription.md) | Token type | Time-based subscription token with recurring payment approvals and auto-deletion on expiry |
| [Tradable NFTs](tradable.md) | Standard | NFT marketplace standard enabling peer-to-peer transfers with the "NFTMarketplace" standard tag and NFTPricingDenom |
| [Burnable](burnable.md) | Approval | Allow token holders to burn tokens by sending them to the burn address, permanently removing them from circulation |
| [Minting](minting.md) | Approval | Mint approval patterns including public mint, whitelist mint, creator-only mint, payment-gated mint, and escrow payouts |
| [Multi-Sig / Voting](multi-sig-voting.md) | Approval | Require weighted quorum voting from multiple parties before transfers can proceed (multi-sig, governance, etc.) |
| [Auto-Mint](auto-mint.md) | Feature | Mint and distribute tokens to recipients at collection creation time using MsgTransferTokens |
| [BB-402 Token-Gated Access](bb-402.md) | Feature | Token-gated access protocol where ownership of specific badges grants API/resource access |
| [Transferability & Update Rules](immutability.md) | Advanced | Lock collection permissions to make properties permanently immutable or permanently permitted |
## Related
- [MCP builder tools](../mcp-tools.md)
- [Claude Code plugin](../claude-code-plugin.md)
## File: agents/skills/address-list.md
---
description: "On-chain address list where membership = owning x1 of token ID 1. Manager can add/remove addresses."
---
# Address List
Instruction text for agents that use the Address List skill, loaded by `bb dev skills address-list` 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 address-list skill and build me one: on-chain address list where membership = owning x1 of token ID 1. Manager can add/remove addresses. 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 address-list` prints the same instructions.
## Summary
Required standards: ["Address List"]
- validTokenIds: must be exactly [{ "start": "1", "end": "1" }]
- Two collection approvals required with exact approvalIds (frontend depends on these):
1. "manager-add": fromListId "Mint", toListId "All", initiatedByListId = creator. Mints token to add address.
2. "manager-remove": fromListId "!Mint", toListId burn address (bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv), initiatedByListId = creator. Burns token to remove address.
- Both approvals must have overridesFromOutgoingApprovals: true
- No peer-to-peer transfer approval, only manager can modify the list
- Standard is "Address List" (not "Non-Transferable")
- Use per-field tools (set_standards, add_approval, set_permissions, set_invariants)
## Instructions
## Address List Configuration
An address list collection represents membership as token ownership: owning x1 of token ID 1 = being on the list. The manager controls membership by minting (adding) and burning (removing) tokens.
### Preferred path: presets (two short tool calls)
Both approvals are fully canonical, the only param that varies is the manager/creator address. Use the presets:
```ts
add_preset_approval({ presetId: "address-list.manager-add", params: { creatorAddress: "bb1..." } })
add_preset_approval({ presetId: "address-list.manager-remove", params: { creatorAddress: "bb1..." } })
```
`list_presets({skill: "address-list"})` enumerates them. For non-canonical variants (e.g. a committee instead of a single manager) fall back to raw `add_approval`.
### Critical: Exact Approval IDs Required
The frontend identifies address list approvals by their exact approvalIds. Using different IDs will break the UI.
- Approval 1: approvalId must be **"manager-add"**
- Approval 2: approvalId must be **"manager-remove"**
### Required Structure
1. **Standards**: ["Address List"] (not "Non-Transferable")
2. **validTokenIds**: [{ "start": "1", "end": "1" }]
3. **Burn address**: bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv (ETH null address in bb1 format)
### Manager-Add Approval (mint to add)
```json
{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "bb1creator...",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "ipfs://METADATA_APPROVAL_manager-add",
"customData": "",
"approvalId": "manager-add",
"approvalCriteria": {
"overridesFromOutgoingApprovals": true
},
"version": "0"
}
```
### Manager-Remove Approval (forceful burn to remove)
`fromListId` must be **"!Mint"** (All except Mint). Using "All" is rejected by the chain with "Mint address cannot be included in address list with other addresses", the Mint slot can only appear in a list by itself.
```json
{
"fromListId": "!Mint",
"toListId": "bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv",
"initiatedByListId": "bb1creator...",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "ipfs://METADATA_APPROVAL_manager-remove",
"customData": "",
"approvalId": "manager-remove",
"approvalCriteria": {
"overridesFromOutgoingApprovals": true
},
"version": "0"
}
```
### Invariants
Do not set `noForcefulPostMintTransfers: true` on address-list collections. That invariant would block manager-remove from burning tokens (since its `overridesFromOutgoingApprovals: true` is only chain-allowed when `fromListId` is exactly "Mint"). The manager must be able to forcibly burn a list member's token, so leave that invariant off. Other default invariants (e.g. `noCustomOwnershipTimes`) are fine.
### Default Balances
```json
{
"balances": [],
"outgoingApprovals": [],
"incomingApprovals": [],
"autoApproveAllIncomingTransfers": true,
"autoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedIncomingTransfers": true,
"userPermissions": {}
}
```
### Permissions
Lock approvals and token IDs:
- canUpdateCollectionApprovals: frozen
- canUpdateValidTokenIds: frozen
- canUpdateManager: frozen (typically)
## Common Mistakes
- Don't use approvalId other than "manager-add" and "manager-remove", the frontend depends on these exact strings.
- Don't use standard "Non-Transferable", address lists use "Address List".
- Don't add a peer-to-peer transfer approval, only the manager should modify the list.
- Don't forget overridesFromOutgoingApprovals: true on both approvals.
## File: agents/skills/auction.md
---
description: "Single-item auction with intent-based bidding. Seller mints NFT directly to the winning bidder during the accept window."
---
# Auction
Instruction text for agents that use the Auction skill, loaded by `bb dev skills auction` 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 auction skill and build me one: single-item auction with intent-based bidding. Seller mints NFT directly to the winning bidder during the accept window. 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 auction` prints the same instructions.
## Summary
Required standards: ["Auction"]
- 1 token ID (the auctioned item)
- 2 collection-level approvals: mint-to-winner (combines mint + accept), burn (cleanup)
- Mint-to-winner: seller mints NFT directly to winning bidder during accept window (bidDeadline → bidDeadline + acceptWindow)
- No separate mint-at-creation step, token doesn't exist until seller accepts a bid
- Burn: anyone can burn token to burn address (permanent cleanup)
- Bidding via user-level incoming approval intents (not collection approvals)
- Bids must have transferTimes valid through end of accept window (not just bid deadline)
- initiatedByListId on mint-to-winner = seller address (only seller can accept)
- maxNumTransfers = 1 on all approvals (one-shot)
- overridesToIncomingApprovals: false on mint-to-winner (bidder's incoming approval handles payment)
- Burn approval has no override flags, relies on defaultBalances autoApproveSelfInitiatedOutgoingTransfers + burn destination
- noForcefulPostMintTransfers: true in invariants (permanently locks out forceful transfers post-mint)
- After settlement, the mint-to-winner approval is auto-deleted via afterOneUse, protocol validators treat a missing mint-to-winner as a valid post-settlement state, not an error
- All permissions frozen after creation
- Don't use coinTransfers on collection approvals, payment happens via intent matching
- Don't set initiatedByListId to "All" on mint-to-winner, must be seller
- Don't set transferTimes to forever, must be bounded to accept window
- Don't put override flags on the burn approval, auction has noForcefulPostMintTransfers: true, so non-mint override flags would be invariant violations
- Do use autoDeletionOptions.afterOneUse: true on mint-to-winner
## Instructions
## Auction Configuration
### Mental Model
A single-item auction where the seller creates a collection, bidders place intent-based bids, and the seller mints the NFT directly to the winning bidder during the accept window. There is no separate mint-then-transfer flow, mint + accept happen in one action. The collection has no coinTransfers, payment is handled entirely through the bidder's user-level approval (intent matching).
### Collection Structure
- Token ID 1 = The auctioned item
- Standard: "Auction"
- validTokenIds: [{ start: "1", end: "1" }]
- invariants: `{ noCustomOwnershipTimes: true, maxSupplyPerId: "0", noForcefulPostMintTransfers: true, disablePoolCreation: true }`
- `noForcefulPostMintTransfers: true` locks the collection so no non-mint approval can ever use `overridesFromOutgoingApprovals` or `overridesToIncomingApprovals`, the burn approval below relies on `defaultBalances` auto-approve flags instead
- All permissions frozen after creation
### Bidding Mechanism
Bidders set user-level incoming approvals on their own accounts that say "I will pay X coins for token 1 from this collection." The seller then accepts the best bid by minting the token directly to the winning bidder during the accept window. The bidder's incoming approval handles the coin payment side via intent matching.
Bids must have transferTimes that stay valid through the end of the accept window (not just the bid deadline), so the seller can match them during the entire accept period.
### Preferred path: presets (two short tool calls)
```ts
add_preset_approval({
presetId: "auction.mint-to-winner",
params: {
sellerAddress: "bb1...",
bidDeadlineMs: "",
acceptWindowEndMs: ""
}
})
add_preset_approval({ presetId: "auction.burn", params: {} })
```
### 2 Approvals
#### 1. Mint-to-Winner (seller mints NFT directly to winning bidder during accept window)
```json
{
"approvalId": "auction-mint-to-winner",
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "", "end": "" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": false,
"coinTransfers": [],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }]
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": { "overallMaxNumTransfers": "1" },
"autoDeletionOptions": { "afterOneUse": true, "afterOverallMaxNumTransfers": true }
}
}
```
> **Critical:** `overridesToIncomingApprovals: false`: the bidder's incoming approval (intent) must be checked so the coin payment side executes. `transferTimes` are bounded to the accept window only, the seller cannot mint before the bid deadline. `toListId: "All"` allows minting to any address (the winning bidder).
#### 2. Burn (cleanup: anyone can burn the token)
```json
{
"approvalId": "auction-burn",
"fromListId": "!Mint",
"toListId": "",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
```
> **No `approvalCriteria` overrides:** because the invariants lock `noForcefulPostMintTransfers: true`, the burn approval must not set `overridesFromOutgoingApprovals` or `overridesToIncomingApprovals`. It relies on `defaultBalances.autoApproveSelfInitiatedOutgoingTransfers: true` for the outgoing side and on the burn destination for the incoming side.
### Auction Flow
1. **Create**: Seller creates auction collection with 2 approvals. No token is minted yet.
2. **Bid**: Bidders place intent-based bids (user-level incoming approvals with coin payment offers). Bids must have transferTimes valid through end of accept window.
3. **Accept**: After bid deadline, seller mints token 1 directly to the winning bidder's address. The bidder's incoming approval triggers coin payment via intent matching. One action: mint + payment.
4. **Cleanup**: If unsold, burn approval allows cleanup.
### Creation Flow (Tool Calls)
1. `set_valid_token_ids`: set [{ start: "1", end: "1" }]
2. `set_standards`: set ["Auction"]
3. `set_invariants`: set `{ noCustomOwnershipTimes: true, maxSupplyPerId: "0", noForcefulPostMintTransfers: true, disablePoolCreation: true }`
4. `add_approval` x2: mint-to-winner, burn
5. `set_collection_metadata`: auction title, description, image
6. `set_token_metadata`: token 1 metadata (the item being auctioned)
7. `set_permissions`: preset "fully-immutable"
8. `validate_transaction`: verify structure
9. `simulate_transaction`: dry run
### Common Mistakes
- Don't add coinTransfers to collection approvals, payment flows through intent matching, not collection-level coin transfers
- Don't set initiatedByListId to "All" on mint-to-winner, only the seller can accept bids
- Don't set transferTimes to forever, must be bounded to accept window (bidDeadline → bidDeadline + acceptWindow)
- Don't set overridesToIncomingApprovals to true on mint-to-winner, must be false so the bidder's incoming approval (payment intent) is checked
- Don't add override flags to the burn approval, the invariant `noForcefulPostMintTransfers: true` rejects `overridesFromOutgoingApprovals` or `overridesToIncomingApprovals` on any non-mint approval
- Don't worry if the mint-to-winner approval is absent after settlement, `autoDeletionOptions.afterOneUse: true` removes it after the first successful mint, and the protocol validator accepts that as a valid post-settlement state
- Don't create a separate mint-to-seller approval, the token should not exist until the seller accepts a bid
- Don't forget autoDeletionOptions on mint-to-winner, without afterOneUse: true, the seller could mint to multiple bidders
- Don't forget that bids must have transferTimes valid through the end of the accept window, not just the bid deadline
## File: agents/skills/auto-mint.md
---
description: "Mint and distribute tokens to recipients at collection creation time using MsgTransferTokens"
---
# Auto-Mint
Instruction text for agents that use the Auto-Mint skill, loaded by `bb dev skills auto-mint` 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 auto-mint skill and build me one: mint and distribute tokens to recipients at collection creation time using MsgTransferTokens. 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 auto-mint` prints the same instructions.
## Summary
Post-creation minting: adds MsgTransferTokens messages to the transaction so tokens are distributed immediately after collection creation.
- Transaction can contain MsgUniversalUpdateCollection plus one or more MsgTransferTokens messages
- All transfer messages use collectionId: "0" to reference the just-created collection
- prioritizedApprovals must always be specified (use [] if none needed)
- from: "Mint" for minting new tokens, bb1... address for peer-to-peer transfers
- The signing user (creator) is the initiator, collection must have an approval allowing this
- All numbers as strings, "1" not 1
- Prefer predeterminedBalances with x0 increments over maxNumTransfers for better frontend UX
When to use:
- User asks to mint tokens to themselves or others at creation time
- Initial distribution or pre-allocation of tokens
- Manager-only collections where the manager should receive all tokens immediately
When not to use:
- Public mint collections (users mint later via approval)
- Subscription collections (users mint on subscribe)
- Smart tokens (users deposit IBC coins to mint)
- Any collection where minting happens post-creation through approvals
## Instructions
## Auto-Mint: Post-Creation Transfers
A transaction can contain the collection creation message (MsgUniversalUpdateCollection) plus one or more MsgTransferTokens messages. All use collectionId: "0" to reference the just-created collection.
### MsgTransferTokens Structure
```json
{
"typeUrl": "/tokenization.MsgTransferTokens",
"value": {
"creator": "bb1...",
"collectionId": "0",
"transfers": [{
"from": "Mint",
"toAddresses": ["bb1recipientaddress..."],
"balances": [{
"amount": "1",
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}],
"prioritizedApprovals": [{
"approvalId": "the-mint-approval-id",
"approvalLevel": "collection",
"approverAddress": "",
"version": "0"
}],
"onlyCheckPrioritizedCollectionApprovals": false,
"onlyCheckPrioritizedIncomingApprovals": false,
"onlyCheckPrioritizedOutgoingApprovals": false,
"memo": ""
}]
}
}
```
### When to add a transfer message
- User asks to mint tokens to themselves or others at creation time
- User wants initial distribution or pre-allocation of tokens
- Manager-only collections where the manager should receive all tokens immediately
- Any scenario where tokens should exist in wallets right after collection creation
### When not to add a transfer message
- Public mint collections (users mint later via the approval)
- Subscription collections (users mint on subscribe)
- Smart tokens (users deposit IBC coins to mint)
- Any collection where minting happens post-creation through approvals
### Critical transfer rules
1. **prioritizedApprovals must be specified**, even if empty []. Match the approvalId to one of the collection's collectionApprovals.
2. **from: "Mint"** for minting new tokens. Use a bb1... address for peer-to-peer transfers.
3. **The signing user (creator) is the initiator**, the collection must have an approval that allows this address as initiatedBy.
4. **collectionId: "0"** is auto-set, it references the collection created by message[0] in the same transaction.
5. **All numbers as strings**, "1" not 1.
6. **Prefer predeterminedBalances for one-time or fixed-use approvals**. Use incrementedBalances with x0 increments (incrementTokenIdsBy: "0", incrementOwnershipTimesBy: "0") instead of relying solely on maxNumTransfers. The frontend auto-detects predeterminedBalances and shows users the exact tokens they will receive, providing much better UX. Avoid manualBalances; incrementedBalances with x0 increments is preferred.
### Time-Dependent Ownership
For expiring tokens, calculate timestamps:
- Current time: use get_current_timestamp tool (milliseconds since epoch)
- Example: 5 minutes from now = current timestamp + (5 * 60 * 1000)
```json
{
"ownershipTimes": [{
"start": "1706000000000",
"end": "1706000300000"
}]
}
```
### Session-based patch operations
- add_transfer: { op: "add_transfer", transfer: { transfers: [...] } }, appends a MsgTransferTokens to the transaction
- remove_transfer: { op: "remove_transfer", index: 0 }, removes transfer message by index (0-based among transfer messages)
- update_transfer: { op: "update_transfer", index: 0, changes: {...} }, deep-merges changes into the transfer message
### Common mistakes
- Don't forget to add prioritizedApprovals in MsgTransferTokens, even if empty ([]), this field must be present or the transfer fails.
- Don't forget that the collection must have a mint approval that allows the creator as initiatedBy.
- Don't add transfer messages for subscription, smart token, or public mint collections, minting happens post-creation through approvals.
## File: agents/skills/bb-402.md
---
description: "Token-gated access protocol where ownership of specific badges grants API/resource access"
---
# BB-402 Token-Gated Access
Instruction text for agents that use the BB-402 Token-Gated Access skill, loaded by `bb dev skills bb-402` 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 bb-402 skill and build me one: token-gated access protocol where ownership of specific badges grants API/resource access. 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 bb-402` prints the same instructions.
## Summary
Protocol for token-gated access to APIs/resources using HTTP 402 Payment Required.
- Flow: client requests resource -> server returns 402 + required badge criteria -> client proves ownership -> server validates via BitBadges API
- ownershipRequirements: use $and for "must have all", $or for "must have any"
- mustOwnAmounts: { start: 1, end: 1 } = must own at least 1
- mustOwnAmounts: { start: 0, end: 0 } = must not own (exclusion)
- Tiered access: different token IDs = different access levels
- Time-bounded access: combine ownershipTimes with subscription tokens
- Server-side verification: BitBadgesApi.verifyOwnership() or Blockin sign-in
## Instructions
## BB-402 Token-Gated Access Protocol
BB-402 is a protocol for token-gated access to APIs and digital resources. It uses HTTP 402 Payment Required responses to signal that badge ownership is needed.
### How It Works
1. Client requests a protected resource
2. Server responds with HTTP 402 + required badge criteria
3. Client proves badge ownership (signs a challenge or presents proof)
4. Server validates ownership via BitBadges API and grants access
### Design Patterns
#### Pattern 1: Simple Badge Gate
Require ownership of a specific badge to access a resource.
```json
{
"ownershipRequirements": {
"$and": [{
"assets": [{
"chain": "BitBadges",
"collectionId": 123,
"assetIds": [{ "start": 1, "end": 1 }],
"mustOwnAmounts": { "start": 1, "end": 1 },
"ownershipTimes": []
}]
}]
}
}
```
#### Pattern 2: Tiered Access
Different badge IDs = different access levels.
- Token ID 1 = Basic access
- Token ID 2 = Premium access
- Token ID 3 = Admin access
#### Pattern 3: Time-Bounded Access
Use ownershipTimes to restrict access to users who own the badge during specific periods. Combine with subscription tokens for recurring access.
#### Pattern 4: Multi-Collection Gate
Require badges from multiple collections using $and/$or logic.
### Implementation Steps
1. **Create the gate badge collection**: Use NFT, fungible, or subscription patterns
2. **Configure ownership requirements**: Define what badges grant what access
3. **Server integration**: Use BitBadges API to verify ownership:
- `BitBadgesApi.verifyOwnership()` for programmatic checks
- Blockin sign-in for session-based authentication
4. **Client integration**: Handle 402 responses, present proof of ownership
### BB-402 Gotchas
- Badge ownership checks are point-in-time, consider caching strategies
- For subscription-based access, check ownershipTimes overlap with current time
- Use $and for "must have all", $or for "must have any"
- mustOwnAmounts: { start: 0, end: 0 } means must not own (exclusion)
- mustOwnAmounts: { start: 1, end: 1 } means must own at least 1
## File: agents/skills/bounty.md
---
description: "Escrow-based bounty with verifier arbitration. Submitter escrows coins, verifier accepts (pays recipient) or denies (refunds submitter). Expires if no decision."
---
# Bounty
Instruction text for agents that use the Bounty skill, loaded by `bb dev skills bounty` 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 bounty skill and build me one: escrow-based bounty with verifier arbitration. Submitter escrows coins, verifier accepts (pays recipient) or denies (refunds submitter). Expires if no decision. 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 bounty` prints the same instructions.
## Summary
Required standards: ["Bounty"]
- 1 token ID (vehicle for approval engine, minted directly to burn)
- 3 collection-level approvals: accept, deny, expire
- Each approval: Mint → burn 1x token ID 1, triggers coinTransfer as side effect
- Verifier decides outcome via MsgCastVote
- Escrow pre-funded at creation via mintEscrowCoinsToTransfer
- Fixed bounty amount, no amount scaling
- All approvals maxNumTransfers = 1 (one-shot)
- All permissions frozen after creation
- Expiration enforced via transferTimes windows
## Instructions
## Bounty Standard
### Mental Model
A bounty is an escrow-based agreement between three parties:
- **Submitter**: Creates the bounty, deposits funds into escrow
- **Recipient**: Receives the payout if the verifier accepts
- **Verifier**: Decides whether to accept or deny the bounty
The escrow is funded upfront at collection creation via `mintEscrowCoinsToTransfer`. Each resolution path (accept/deny/expire) is a single approval that mints 1x token ID 1 from Mint → burn address. The token is just a vehicle, the real action is the coinTransfer side effect that pays out from escrow. The verifier votes to unlock accept or deny. If no vote before expiration, anyone can trigger expire to refund the submitter.
### Token Structure
- Token ID 1 = Bounty token (vehicle for approval engine)
- validTokenIds: [{ start: "1", end: "1" }]
- 1 alias path: `ubounty` → token ID 1, symbol BOUNTY, 1 decimal, 1:1 conversion
### 3 Required Approvals
All 3 approvals share the same structure: Mint → burn address, 1x token ID 1. The token is just a vehicle to trigger the approval engine's coinTransfer. Each approval has maxNumTransfers = 1 (one-shot).
#### Preferred path: presets (three short tool calls)
All three approvals are fully canonical, bounty.accept, bounty.deny, bounty.expire cover the shape. The agent still generates the approvalId + proposalId suffixes (via `generate_unique_id` or similar) and passes them as params.
```ts
add_preset_approval({
presetId: "bounty.accept",
params: { approvalId, proposalId, payoutTo: recipient, verifierAddress, denom, amount, expirationMs }
})
add_preset_approval({
presetId: "bounty.deny",
params: { approvalId, proposalId, payoutTo: submitter, verifierAddress, denom, amount, expirationMs }
})
add_preset_approval({
presetId: "bounty.expire",
params: { approvalId, refundTo: submitter, denom, amount, expirationMs }
})
```
`list_presets({skill: "bounty"})` lists params. For non-standard variants (multi-verifier quorum, variable amounts), use raw `add_approval`.
#### 1. Accept (bounty-accept-*)
Verifier votes accept → mint-to-burn → coins to recipient.
Key fields:
- fromListId: "Mint"
- toListId: burn address (bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv)
- initiatedByListId: "All"
- coinTransfers: [{ to: recipientAddress, overrideFromWithApproverAddress: true, overrideToWithInitiator: false, coins: [{ denom, amount: depositAmount }] }]
- predeterminedBalances.incrementedBalances:
- startBalances: [{ amount: "1", tokenIds: [{ start: "1", end: "1" }], ownershipTimes: fullRange }]
- allowAmountScaling: false, maxScalingMultiplier: "0"
- votingChallenges: [{ proposalId: "bounty-accept-*", quorumThreshold: "100", voters: [{ address: verifierAddress, weight: "1" }] }]
- transferTimes: [{ start: "1", end: expirationTimestamp }]
- maxNumTransfers.overallMaxNumTransfers: "1"
- overridesFromOutgoingApprovals: true
- overridesToIncomingApprovals: true
#### 2. Deny (bounty-deny-*)
Verifier votes deny → mint-to-burn → coins to submitter.
Same as Accept but:
- coinTransfers.to: submitterAddress (refund)
- votingChallenges proposalId: "bounty-deny-*"
#### 3. Expire (bounty-expire-*)
After expiration → mint-to-burn → coins to submitter. No verifier vote needed.
Same as Deny but:
- No votingChallenges (time-gated only)
- transferTimes: [{ start: expirationTimestamp + 1, end: "18446744073709551615" }]
### Settlement Flow
1. Verifier sends MsgCastVote to unlock accept or deny:
- collection_id: collectionId
- approval_level: "collection"
- approval_id: accept or deny approval ID
- proposal_id: matching proposal ID
- yes_weight: "100"
2. After vote, anyone triggers the payout:
- MsgTransferTokens from Mint to burn address (1x token ID 1)
- prioritizedApprovals: [{ approvalId, approvalLevel: "collection" }]
- Coin transfer executes automatically (escrow → recipient or submitter)
3. For expiration (no vote needed):
- After expirationTimestamp, anyone can call MsgTransferTokens with the expire approval
- Coins return to submitter automatically
### Key Differences from Prediction Markets
- Token is just a vehicle (minted directly to burn), nobody holds it
- 3 approvals, not 7 (no separate mint/redeem/transfer/push)
- Escrow pre-funded at creation (mintEscrowCoinsToTransfer)
- Fixed amount, no allowAmountScaling
- Fixed payout addresses (hardcoded in coinTransfers.to), not overrideToWithInitiator
- All approvals maxNumTransfers = 1 (one-shot)
- Expiration via transferTimes windowing (accept/deny before, expire after)
### Creation Flow (Tool Calls)
1. Use per-field tools to initialize the collection
2. `set_valid_token_ids`: set [{ start: "1", end: "1" }]
3. `set_standards`: set ["Bounty"]
4. `set_invariants`: set { noCustomOwnershipTimes: true, disablePoolCreation: true }
5. `set_mint_escrow_coins`: fund escrow with bounty amount
6. `add_approval` x3: add accept, deny, expire approvals
7. `add_alias_path`: ubounty alias (symbol BOUNTY, 1 decimal, 1:1 token ID 1)
8. `set_permissions`: freeze all permissions
9. `set_collection_metadata`: name, description, image
10. `set_token_metadata`: token 1 metadata
11. `validate_transaction`: verify structure
12. `simulate_transaction`: dry run
### Permissions
All permissions must be frozen (permanentlyForbiddenTimes: fullRange):
- canDeleteCollection
- canArchiveCollection
- canUpdateStandards
- canUpdateCustomData
- canUpdateManager
- canUpdateCollectionMetadata
- canUpdateValidTokenIds
- canUpdateTokenMetadata
- canUpdateCollectionApprovals
- canAddMoreAliasPaths
- canAddMoreCosmosCoinWrapperPaths
#### Common Mistakes
- Don't use allowAmountScaling: bounty amount is fixed at creation time
- Don't use overrideToWithInitiator: use hardcoded addresses in coinTransfers.to
- Don't set maxNumTransfers > 1, each approval is one-shot
- Don't use fromListId "!Mint", all 3 approvals mint from "Mint" to burn address
- Don't omit manualBalances: [] in predeterminedBalances
- Don't omit fields in orderCalculationMethod, include all boolean fields
- Don't make expiration transferTimes overlap with accept/deny transferTimes
- Don't forget set_mint_escrow_coins: without it, the escrow is empty and payouts fail
#### Advanced: Self-Referencing with mustOwnTokens
For bounties that require the verifier or submitter to hold a token from this collection (e.g., a reputation badge), use collectionId "0" in mustOwnTokens. The chain resolves "0" to the current collection ID at runtime, which is especially useful at creation time when the real ID is not yet known.
## File: agents/skills/burnable.md
---
description: "Allow token holders to burn tokens by sending them to the burn address, permanently removing them from circulation"
---
# Burnable
Instruction text for agents that use the Burnable skill, loaded by `bb dev skills burnable` 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 burnable skill and build me one: allow token holders to burn tokens by sending them to the burn address, permanently removing them from circulation. 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 burnable` prints the same instructions.
## Summary
Allows holders to permanently destroy tokens by sending to burn address.
- Burn address: bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv (ETH null address in BitBadges format)
- Approval structure: fromListId: "!Mint", toListId: burn address
- overridesToIncomingApprovals: true (burn address has no user-level incoming approvals)
- approvalId: "burnable-approval" (standard ID used by frontend to detect burnability)
- All amounts/transfers set to "0" (unlimited)
- Additive: sits alongside other collection approvals
- Do not use with: credit tokens (increment-only), soulbound tokens, subscription tokens
## Instructions
## Burnable Tokens
### Concept
A burnable approval allows any token holder to permanently destroy their tokens by transferring them to the burn address. This is useful for deflationary tokens, redemption systems, or any scenario where tokens should be removable from circulation.
### Burn Address
The burn address is the ETH null address (0x0000000000000000000000000000000000000000) converted to BitBadges format:
`bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv`
Tokens sent to this address are effectively destroyed, no one controls the private key.
### Required Approval Structure
Add this approval to `collectionApprovals`:
```json
{
"fromListId": "!Mint",
"toListId": "bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "burnable-approval",
"uri": "",
"customData": "",
"version": "0",
"approvalCriteria": {
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [],
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false
},
"orderCalculationMethod": { "useOverallNumTransfers": false, "usePerToAddressNumTransfers": false, "usePerFromAddressNumTransfers": false, "usePerInitiatedByAddressNumTransfers": false, "useMerkleChallengeLeafIndex": false, "challengeTrackerId": "" }
},
"approvalAmounts": { "overallApprovalAmount": "0", "perToAddressApprovalAmount": "0", "perFromAddressApprovalAmount": "0", "perInitiatedByAddressApprovalAmount": "0", "amountTrackerId": "", "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" } },
"maxNumTransfers": { "overallMaxNumTransfers": "0", "perToAddressMaxNumTransfers": "0", "perFromAddressMaxNumTransfers": "0", "perInitiatedByAddressMaxNumTransfers": "0", "amountTrackerId": "", "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" } },
"coinTransfers": [],
"merkleChallenges": [],
"mustOwnTokens": [],
"overridesFromOutgoingApprovals": false,
"overridesToIncomingApprovals": true,
"mustPrioritize": false
}
}
```
### Key Fields Explained
- **fromListId: "!Mint"**: any holder (everyone except the Mint address) can burn
- **toListId: "bb1qqqqqqq..."**: destination is the burn address (a single-address list)
- **initiatedByListId: "All"**: anyone can initiate the burn (typically the holder themselves)
- **overridesToIncomingApprovals: true**: the burn address has no user-level incoming approvals, so this must override
- **approvalId: "burnable-approval"**: standard ID used by the frontend to detect burnability
- All amounts/transfers set to "0" (unlimited)
### Combining with Other Approvals
The burnable approval is additive, it sits alongside other collection approvals (mint approvals, transferable approvals, etc.). Order matters: place it after mint approvals but the system will match based on from/to addresses.
### When not to Use
- **Credit tokens**: These are increment-only by design; burning defeats the purpose
- **Soulbound tokens**: If tokens should be permanently bound to an address, don't add a burn approval
- **Subscription tokens**: Typically managed by the issuer, not burned by holders
## File: agents/skills/credit-token.md
---
description: "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 --units ` 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: "",
paymentRecipient: "",
tokensPerUnit: ""
}
})
```
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": "", "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": "",
"coins": [{ "amount": "1", "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",
"conversion": {
"sideA": { "amount": "1" },
"sideB": [{ "amount": "1", "ownershipTimes": [{"start":"1","end":"18446744073709551615"}], "tokenIds": [{"start":"1","end":"1"}] }]
},
"symbol": "",
"denomUnits": [{ "decimals": "6", "symbol": "", "isDefaultDisplay": true, "metadata": { "uri": "ipfs://METADATA_ALIAS__UNIT", "customData": "" } }],
"metadata": { "uri": "ipfs://METADATA_ALIAS_u", "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.
## File: agents/skills/crowdfund.md
---
description: "On-chain crowdfunding with goal tracking via mustOwnTokens. Contributors deposit funds, receive refund tokens. Crowdfunder withdraws if goal met, contributors refund if not."
---
# Crowdfund
Instruction text for agents that use the Crowdfund skill, loaded by `bb dev skills crowdfund` 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 crowdfund skill and build me one: on-chain crowdfunding with goal tracking via mustOwnTokens. Contributors deposit funds, receive refund tokens. Crowdfunder withdraws if goal met, contributors refund if not. 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 crowdfund` prints the same instructions.
## Summary
Required standards: ["Crowdfund"]
- 2 token IDs: Token 1 = Refund token (contributor holds), Token 2 = Progress token (crowdfunder accumulates)
- 4 collection-level approvals: deposit-refund, deposit-progress, success (withdraw), refund
- Contributors deposit coins → receive Token 1 (refund token). Paired approval mints Token 2 to crowdfunder (progress tracking).
- Success: crowdfunder withdraws if mustOwnTokens confirms they hold >= goal of Token 2 (collectionId: 0 = self-reference)
- Refund: after deadline, contributors burn Token 1 → escrow pays them back (only if goal not met via mustOwnTokens check)
- allowAmountScaling: true on all 4 approvals (contributors choose deposit size, everything scales proportionally)
- maxScalingMultiplier: MAX_UINT for unrestricted scaling
- Deposit coinTransfer.to = "Mint" (auto-resolves to escrow)
- requireToEqualsInitiatedBy: true on deposit-refund (contributor receives their own refund token)
- invariants: `noForcefulPostMintTransfers: true`: the refund approval (non-mint) must not set `overridesFromOutgoingApprovals` or `overridesToIncomingApprovals` (both must be false or omitted). It relies on `defaultBalances.autoApproveSelfInitiatedOutgoingTransfers: true` for the outgoing side and on the burn destination for the incoming side. The deposit-refund / deposit-progress / success approvals are Mint-side and keep `overridesFromOutgoingApprovals: true` as the chain requires, with `overridesToIncomingApprovals: false`
- All permissions frozen after creation
- Don't use votingChallenges: goal tracking is via mustOwnTokens, not voting
- Don't forget allowAmountScaling on all 4 approvals
- Don't set overrideFromWithApproverAddress on deposit (contributor pays, not escrow)
- Do set overrideFromWithApproverAddress: true on success and refund (escrow pays out)
- Do set overrideToWithInitiator: true on refund (contributor receives their own refund)
- Do use collectionId: "0" in mustOwnTokens for self-reference
## Instructions
## Crowdfund Configuration
### Mental Model
On-chain crowdfunding with automatic goal tracking. Contributors deposit coins and receive refund tokens. A progress token tracks total raised. If the goal is met, the crowdfunder withdraws all funds. If not, contributors burn their refund tokens to get deposits back.
### Collection Structure
- Token ID 1 = Refund token (contributor holds: burn to refund)
- Token ID 2 = Progress token (crowdfunder accumulates: tracks total raised)
- Standard: "Crowdfund"
- validTokenIds: [{ start: "1", end: "2" }]
- invariants: { noCustomOwnershipTimes: true }
- All permissions frozen after creation
### 4 Required Approvals
### Preferred path: presets (four short tool calls)
```ts
add_preset_approval({ presetId: "crowdfund.deposit-refund", params: { denom, deadlineMs, crowdfunderAddress } })
add_preset_approval({ presetId: "crowdfund.deposit-progress", params: { denom, deadlineMs, crowdfunderAddress } })
add_preset_approval({ presetId: "crowdfund.success-withdraw", params: { denom, deadlineMs, crowdfunderAddress, goalAmount } })
add_preset_approval({ presetId: "crowdfund.refund", params: { denom, deadlineMs, crowdfunderAddress, goalAmount } })
```
#### 1. Deposit-Refund (contributor pays coins → receives Token 1)
```json
{
"approvalId": "deposit-refund",
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"requireToEqualsInitiatedBy": true,
"coinTransfers": [{
"to": "Mint",
"overrideFromWithApproverAddress": false,
"overrideToWithInitiator": false,
"coins": [{ "amount": "1", "denom": "" }]
}],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"allowAmountScaling": true,
"maxScalingMultiplier": "18446744073709551615",
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": { "overallMaxNumTransfers": "0" }
}
}
```
> **Critical:** `requireToEqualsInitiatedBy: true` ensures the contributor receives their own refund token. `allowAmountScaling: true` lets contributors choose their deposit size, the coin payment and token amount scale together.
#### 2. Deposit-Progress (paired: mints Token 2 to crowdfunder, no coinTransfer)
```json
{
"approvalId": "deposit-progress",
"fromListId": "Mint",
"toListId": "",
"initiatedByListId": "All",
"tokenIds": [{ "start": "2", "end": "2" }],
"transferTimes": [{ "start": "1", "end": "" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "2", "end": "2" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"allowAmountScaling": true,
"maxScalingMultiplier": "18446744073709551615",
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": { "overallMaxNumTransfers": "0" }
}
}
```
> **toListId** is the crowdfunder's specific address (not "All"). No coinTransfer, this is the paired counterpart to deposit-refund.
#### 3. Success / Withdraw (crowdfunder withdraws if goal met)
```json
{
"approvalId": "success-withdraw",
"fromListId": "Mint",
"toListId": "",
"initiatedByListId": "",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [{
"to": "",
"overrideFromWithApproverAddress": true,
"overrideToWithInitiator": false,
"coins": [{ "amount": "1", "denom": "" }]
}],
"mustOwnTokens": [{
"collectionId": "0",
"tokenIds": [{ "start": "2", "end": "2" }],
"amountRange": { "start": "", "end": "18446744073709551615" },
"ownershipCheckParty": ""
}],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"allowAmountScaling": true,
"maxScalingMultiplier": "18446744073709551615",
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": { "overallMaxNumTransfers": "1" }
}
}
```
> **mustOwnTokens with collectionId: "0"** = self-reference. Checks that the crowdfunder owns >= goal amount of Token 2 (progress token). Only available after deadline.
#### 4. Refund (contributor burns Token 1 → gets deposit back, only if goal not met)
```json
{
"approvalId": "refund",
"fromListId": "!Mint",
"toListId": "",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [{
"to": "",
"overrideFromWithApproverAddress": true,
"overrideToWithInitiator": true,
"coins": [{ "amount": "1", "denom": "" }]
}],
"mustOwnTokens": [{
"collectionId": "0",
"tokenIds": [{ "start": "2", "end": "2" }],
"amountRange": { "start": "0", "end": "" },
"ownershipCheckParty": ""
}],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"allowAmountScaling": true,
"maxScalingMultiplier": "18446744073709551615",
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": {
"overallMaxNumTransfers": "18446744073709551615"
}
}
}
```
> Refund uses overrideFromWithApproverAddress: true (escrow pays) + overrideToWithInitiator: true (contributor receives). mustOwnTokens checks crowdfunder has less than goal of Token 2 (amountRange.end = goal - 1). allowAmountScaling: true so refund scales with deposit size. maxNumTransfers = MAX_UINT (needs non-zero with overrideFromWithApproverAddress).
### Creation Flow (Tool Calls)
1. `set_valid_token_ids`: set [{ start: "1", end: "2" }]
2. `set_standards`: set ["Crowdfund"]
3. `set_invariants`: set { noCustomOwnershipTimes: true }
4. `add_approval` x4: deposit-refund, deposit-progress, success, refund
5. `set_collection_metadata`: name, description, image
6. `set_token_metadata` x2: Token 1 (Refund), Token 2 (Progress)
7. `set_permissions`: preset "fully-immutable"
8. `validate_transaction`: verify structure
9. `simulate_transaction`: dry run
### Common Mistakes
- Don't forget allowAmountScaling: true on all 4 approvals, without it, all deposits are fixed at 1 base unit
- Don't use votingChallenges: goal tracking uses mustOwnTokens, not voting
- Don't forget maxScalingMultiplier: MAX_UINT, without it, scaling is capped at 0 (no scaling)
- Don't set overrideFromWithApproverAddress on deposit-refund or deposit-progress (contributor pays, not escrow)
- Don't forget requireToEqualsInitiatedBy: true on deposit-refund
- Don't forget the paired deposit-progress approval, it tracks total raised
- Don't set collectionId to the actual collection ID in mustOwnTokens, use "0" for self-reference
- Don't forget that success transferTimes must start after the deadline (deadline + 1)
- Don't forget that refund mustOwnTokens amountRange.end = goal - 1 (strictly less than goal)
- Don't set maxNumTransfers = 0 on refund approval, overrideFromWithApproverAddress requires non-zero
## File: agents/skills/custom-2fa.md
---
description: "Two-factor authentication for transfers using a secondary approval address"
---
# Custom 2FA
Instruction text for agents that use the Custom 2FA skill, loaded by `bb dev skills custom-2fa` 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 custom-2fa skill and build me one: two-factor authentication for transfers using a secondary approval address. 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 custom-2fa` prints the same instructions.
## Summary
Required standards: ["Custom-2FA"]
- autoDeletionOptions.allowPurgeIfExpired: must be true
- Approval name must contain "Custom 2FA"
- Use time-dependent ownershipTimes in MsgTransferTokens (not forever)
- Calculate timestamps: current time + expiration duration (milliseconds since epoch)
- Tokens automatically expire and can be purged after expiration
## Instructions
## Custom-2FA Configuration
When creating a Custom-2FA collection, follow these requirements:
### Preferred path: preset (one short tool call)
The mint approval is canonical, use `custom-2fa.mint`:
```ts
add_preset_approval({ presetId: "custom-2fa.mint", params: { managerAddress: "bb1..." } })
```
The actual token expiration is set per-mint via the MsgTransferTokens `ownershipTimes` window (e.g. now → now + 5*60*1000 ms). The approval itself just enables the Mint path with auto-purge.
### Required Structure
1. **Standards**: must include "Custom-2FA"
- "standards": ["Custom-2FA"]
2. **Approval Requirements**:
- autoDeletionOptions.allowPurgeIfExpired: must be true
- This allows expired tokens to be automatically purged
- Approval name must contain "Custom 2FA"
3. **Time-Dependent Ownership**: Use time-dependent ownershipTimes in MsgTransferTokens
- Calculate timestamps: current time + expiration duration
- Example: 5 minutes = Date.now() + (5 * 60 * 1000)
### Complete Example
```json
{
"messages": [
{
"typeUrl": "/tokenization.MsgUniversalUpdateCollection",
"value": {
"standards": ["Custom-2FA"],
"collectionApprovals": [{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "bb1manager...",
"approvalId": "2fa-mint",
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"autoDeletionOptions": { "allowPurgeIfExpired": true }
}
}]
}
},
{
"typeUrl": "/tokenization.MsgTransferTokens",
"value": {
"collectionId": "0",
"transfers": [{
"from": "Mint",
"toAddresses": ["bb1recipient..."],
"balances": [{
"amount": "1",
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1706000000000", "end": "1706000300000" }]
}]
}]
}
}
]
}
```
### 2FA-Specific Gotchas
- Must set allowPurgeIfExpired: true
- Use time-dependent ownershipTimes in transfers (not forever)
- Calculate expiration timestamps correctly (milliseconds since epoch)
- Tokens automatically expire and can be purged after expiration
## File: agents/skills/fungible-token.md
---
description: "Simple fungible token with fixed or unlimited supply and configurable mint/transfer approvals"
---
# Fungible Token
Instruction text for agents that use the Fungible Token skill, loaded by `bb dev skills fungible-token` 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 fungible-token skill and build me one: simple fungible token with fixed or unlimited supply and configurable mint/transfer approvals. 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 fungible-token` prints the same instructions.
## Summary
Required standards: ["Fungible Tokens"]
- validTokenIds: must be exactly [{ "start": "1", "end": "1" }] (single token ID)
- All tokens share the same token ID (1), making them interchangeable
- Amount field in transfers determines quantity
- Token metadata must reference token ID 1
- Ownership times typically forever: [{ "start": "1", "end": "18446744073709551615" }]
## Instructions
## Fungible Token Configuration
When creating a fungible token collection, you must follow these requirements:
### Required Configuration
1. **validTokenIds**: Set to exactly [{ "start": "1", "end": "1" }]
- This ensures only token ID 1 is valid
- This is the standard pattern for fungible tokens
2. **Standards**: Include "Fungible Tokens" in the standards array
- Example: "standards": ["Fungible Tokens"]
3. **Token Metadata**: Each tokenMetadata entry must reference token ID 1
- Example: "tokenIds": [{ "start": "1", "end": "1" }]
### Supply Tracking with approvalAmounts
Fungible tokens use approvalAmounts (not predeterminedBalances) to track minting supply:
```json
{
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"approvalAmounts": {
"overallApprovalAmount": "1000000",
"perInitiatedByAddressApprovalAmount": "1000",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"amountTrackerId": "mint-tracker",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
}
```
- overallApprovalAmount: total supply cap (use "0" for unlimited)
- perInitiatedByAddressApprovalAmount: max per user (use "0" for unlimited)
- Important: predeterminedBalances and approvalAmounts are incompatible, fungible tokens use approvalAmounts, not predeterminedBalances
### Pattern-Specific Gotchas
- All tokens share the same token ID (1), making them interchangeable
- Amount field in transfers determines quantity (e.g., "amount": "100" means 100 tokens)
- Ownership times are typically forever: [{ "start": "1", "end": "18446744073709551615" }]
## Common Mistakes
- Don't use predeterminedBalances for fungible tokens, use approvalAmounts instead. They are incompatible.
- Don't use numbers instead of strings for amounts, use "1000" not 1000. All numeric values must be string-encoded.
- Don't forget autoApproveAllIncomingTransfers: true in defaultBalances, without it, recipients cannot receive tokens on public-mint collections.
- Don't forget overridesFromOutgoingApprovals: true on Mint approvals, required for all fromListId: "Mint" approvals.
- Don't use multiple token IDs, fungible tokens must use exactly one token ID [{ "start": "1", "end": "1" }].
### Example Structure
```json
{
"updateValidTokenIds": true,
"validTokenIds": [{ "start": "1", "end": "1" }],
"updateStandards": true,
"standards": ["Fungible Tokens"],
"updateTokenMetadata": true,
"tokenMetadata": [{
"uri": "ipfs://...",
"customData": "",
"tokenIds": [{ "start": "1", "end": "1" }]
}]
}
```
## File: agents/skills/immutability.md
---
description: "Lock collection permissions to make properties permanently immutable or permanently permitted"
---
# Transferability & Update Rules
Instruction text for agents that use the Transferability & Update Rules skill, loaded by `bb dev skills immutability` 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 immutability skill and build me one: lock collection permissions to make properties permanently immutable or permanently permitted. 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 immutability` prints the same instructions.
## Summary
Controls whether collection properties can be changed after creation.
- Two states: FROZEN (permanentlyForbiddenTimes: FOREVER) or NEUTRAL (empty [])
- NEUTRAL [] = manager can update now and can freeze it later. Use this for editable fields.
- FROZEN = permanent and irreversible. Use for fields that should never change.
- Avoid permanentlyPermittedTimes: it permanently prevents locking. Almost never needed.
- canUpdateCollectionApprovals: controls transfer rule mutability
- Security: If manager can update Mint approvals, they can mint unlimited tokens
- Default to frozen unless user requests updatable
- List IDs in permissions: only use reserved IDs ("All", "Mint", "!Mint", direct "bb1..." addresses)
- permanentlyForbiddenTimes: [{ "start": "1", "end": "18446744073709551615" }] = frozen forever
## Instructions
## Transferability & Update Rules Configuration
When configuring collection permissions for transferability and update rules, you must follow these critical requirements:
### Critical Permission Rules
**canUpdateCollectionApprovals**:
- **Critical**: Controls whether transfer rules (approvals) can be changed after creation
- **Security risk**: If the manager can update approvals from the "Mint" address, they can mint any amount
- **Default**: Should be **forbidden (frozen)** for collections where transfer rules should be locked
- **Format**: Uses CollectionApprovalPermission format (see below)
### CollectionApprovalPermission Format
```json
{
"fromListId": "All",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "All",
"permanentlyPermittedTimes": [],
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
```
**Key Fields:**
- **List IDs** (fromListId, toListId, initiatedByListId): Use only reserved list IDs:
- "All": Any address
- "Mint": Mint address
- "!Mint": Everything except Mint
- "bb1...": Direct address
- "!bb1...": Everything except the specific address
- "bb1abc:bb1xyz": Colon-separated addresses
- **Do not use**: Custom list IDs, only reserved IDs or direct addresses
- **approvalId**: "All" to restrict all approvals, or a specific approvalId string
### Example: Immutable Transfer Rules (All Frozen)
```json
{
"collectionPermissions": {
"canUpdateCollectionApprovals": [{
"fromListId": "All",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "All",
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }],
"permanentlyPermittedTimes": []
}]
}
}
```
### Example: Restricting Only Mint Approvals
```json
{
"canUpdateCollectionApprovals": [{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "All",
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }],
"permanentlyPermittedTimes": []
}]
}
```
### Example: Restricting Specific Approval ID
```json
{
"canUpdateCollectionApprovals": [{
"fromListId": "All",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "mint-approval",
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }],
"permanentlyPermittedTimes": []
}]
}
```
### Empty Permission Arrays: critical
**Important**: If a permission entry has both `permanentlyPermittedTimes` and `permanentlyForbiddenTimes` as empty arrays, the entire permission entry is redundant and should be replaced with an empty array.
**For ActionPermission** (canDeleteCollection, canArchiveCollection, etc.):
- Wrong: `"canArchiveCollection": [{ "permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [] }]`
- Correct: `"canArchiveCollection": []`
**For TokenIdsActionPermission** (canUpdateTokenMetadata, canUpdateValidTokenIds):
- Wrong: `"canUpdateTokenMetadata": [{ "tokenIds": [...], "permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [] }]`
- Correct: `"canUpdateTokenMetadata": []`
**For CollectionApprovalPermission** (canUpdateCollectionApprovals):
- If both time arrays are empty, use empty array: `"canUpdateCollectionApprovals": []`
### Permission Presets
Three common permission configurations:
**1. Fully Immutable**: Everything frozen. Nothing can change after creation.
- All permissions set to permanentlyForbiddenTimes: FOREVER
- Use when: the collection should never change
**2. Manager Controlled**: Manager can change everything except delete.
- canDeleteCollection: frozen
- Everything else: NEUTRAL [] (editable now, can be frozen later)
- Use when: the manager needs full control (issuer-controlled tokens, evolving collections)
**3. Locked Approvals (recommended default)**: Approvals and supply frozen, metadata editable.
- canDeleteCollection: frozen
- canUpdateStandards: frozen
- canUpdateManager: frozen
- canUpdateValidTokenIds: frozen
- canUpdateCollectionApprovals: frozen
- canUpdateCollectionMetadata: NEUTRAL []
- canUpdateTokenMetadata: NEUTRAL []
- canArchiveCollection: NEUTRAL []
- canUpdateCustomData: NEUTRAL []
- Use when: supply and rules should be immutable but metadata needs updates
**Understanding permission states:**
- Empty array [] = NEUTRAL (manager can update now and can lock it later, preserves maximum flexibility)
- permanentlyForbiddenTimes: FOREVER = FROZEN (can never be changed, permanent and irreversible)
- permanentlyPermittedTimes: FOREVER = PERMANENTLY ALLOWED (can never be frozen, almost never needed)
**Important**: For editable fields, always use NEUTRAL (empty []) instead of permanentlyPermittedTimes. Neutral gives the same current behavior (manager can update) but preserves the option to freeze it later. Only use permanentlyPermittedTimes if the user explicitly requests a guarantee that a field can never be locked.
### Security Considerations
- **Mint Transfer Rules**: If canUpdateCollectionApprovals is allowed for Mint, the manager could mint unlimited tokens
- **Post-Mint Transfer Rules**: If post-mint transfer rules can be updated, the manager could change transferability
- **Best Practice**: Default to locked (frozen) transfer rules unless user explicitly requests updatable rules
- Only allow updates for dynamic collections where the user explicitly requests flexibility
## Common Mistakes
- Don't use custom list IDs in permissions, only reserved IDs: "All", "Mint", "!Mint", or direct bb1... addresses.
- Don't leave both permanentlyPermittedTimes and permanentlyForbiddenTimes as empty arrays in a permission entry, this is redundant. Replace the entire entry with an empty array [].
- Don't forget that unfrozen Mint approval permissions means the manager can mint unlimited tokens, freeze canUpdateCollectionApprovals for Mint if supply should be fixed.
- Don't confuse empty permission array [] (neutral/unset) with a frozen permission, empty means the field is still updatable.
## File: agents/skills/liquidity-pools.md
---
description: "Liquidity pool standard with the \"Liquidity Pools\" protocol standard tag, used for tradable assets that can be swapped on a DEX"
---
# Liquidity Pools
Instruction text for agents that use the Liquidity Pools skill, loaded by `bb dev skills liquidity-pools` 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 liquidity-pools skill and build me one: liquidity pool standard with the "Liquidity Pools" protocol standard tag, used for tradable assets that can be swapped on a DEX. 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 liquidity-pools` prints the same instructions.
## Summary
Required standards: ["Liquidity Pools"]
- Must set invariants.disablePoolCreation: false
- Must configure at least one alias path (required for liquidity pools to function)
- Merkle challenges are not compatible with liquidity pools
- Enables decentralized exchange (DEX) trading interfaces
## Instructions
## Liquidity Pools Configuration
When enabling liquidity pools for a collection, follow these requirements:
### Required Structure
1. **Standards**: must include "Liquidity Pools"
- "standards": ["Liquidity Pools"]
2. **Invariants**: must set disablePoolCreation to false
```json
{
"standards": ["Liquidity Pools"],
"invariants": {
"disablePoolCreation": false
}
}
```
3. **Alias Paths**: must configure at least one alias path
- This is required for liquidity pools to function
- Use the alias path configuration provided in the skill config
```json
{
"aliasPathsToAdd": [{
"denom": "uvatom",
"symbol": "uvatom",
"conversion": {
"sideA": { "amount": "1" },
"sideB": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }]
},
"denomUnits": [{
"decimals": "6",
"symbol": "vATOM",
"isDefaultDisplay": true,
"metadata": { "uri": "ipfs://METADATA_ALIAS_uvatom_UNIT", "customData": "" }
}],
"metadata": { "uri": "ipfs://METADATA_ALIAS_uvatom", "customData": "" }
}]
}
```
### Liquidity Pools Gotchas
- disablePoolCreation must be false (not true)
- Must configure at least one alias path (required for liquidity pools)
- PathMetadata (on every aliasPath and denomUnit) is only `{ uri, customData }`. Put the token logo in the off-chain JSON at the placeholder URI and register a matching entry in `metadataPlaceholders`, never put `image` on the proto.
- Merkle challenges are not compatible with liquidity pools
- This enables decentralized exchange (DEX) trading interfaces
## File: agents/skills/minting.md
---
description: "Mint approval patterns including public mint, whitelist mint, creator-only mint, payment-gated mint, and escrow payouts"
---
# Minting
Instruction text for agents that use the Minting skill, loaded by `bb dev skills minting` 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 minting skill and build me one: mint approval patterns including public mint, whitelist mint, creator-only mint, payment-gated mint, and escrow payouts. 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 minting` prints the same instructions.
## Summary
Required fields for all minting approvals:
- fromListId: "Mint"
- overridesFromOutgoingApprovals: true (required for all Mint approvals)
- autoApproveAllIncomingTransfers: true in defaultBalances (for public-mint collections)
- predeterminedBalances vs approvalAmounts: incompatible, use one or the other
- orderCalculationMethod: must have exactly one method set to true (default: useOverallNumTransfers)
- coinTransfers override flags: false for standard payments, true for escrow payouts
- Mint escrow: overrideFromWithApproverAddress: true + overrideToWithInitiator: true (pays the minter from the escrow address)
- amountTrackerId: required when using maxNumTransfers or approvalAmounts
## Instructions
## Minting Configuration
When configuring minting approvals, you create collection approvals with fromListId: "Mint" that allow tokens to be minted from the Mint address.
### Core Structure
All minting approvals must have:
- **fromListId**: "Mint" (required for all minting operations)
- **overridesFromOutgoingApprovals**: true (required for all Mint approvals)
- **toListId**: Typically "All" or specific address list
- **initiatedByListId**: Who can initiate the mint (typically "All" for public mints)
### 1. Payments Per Mint
Use `coinTransfers` in approvalCriteria to require payment:
```json
{
"approvalCriteria": {
"coinTransfers": [{
"to": "bb1creator...",
"coins": [{ "denom": "ubadge", "amount": "5000000000" }],
"overrideFromWithApproverAddress": false,
"overrideToWithInitiator": false
}]
}
}
```
**Important:**
- `overrideFromWithApproverAddress`: false (standard for mint payments)
- `overrideToWithInitiator`: false (standard for mint payments)
- Payment recipient (`to`) should be the creator or approver address
### 2. Incremented Token IDs
Use `predeterminedBalances.incrementedBalances` to automatically increment token IDs:
```json
{
"approvalCriteria": {
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{
"amount": "1",
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}],
"incrementTokenIdsBy": "1",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false
},
"orderCalculationMethod": {
"useOverallNumTransfers": true,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
},
"manualBalances": []
}
}
}
```
**Critical: orderCalculationMethod Rule**
- When using `predeterminedBalances`, the `orderCalculationMethod` must have exactly one method set to `true`
- Default: `useOverallNumTransfers: true` (sequential across all mints)
- Cannot have zero methods true, cannot have multiple methods true
### 3. Auto-Deletions
Use `autoDeletionOptions` to automatically delete approvals after use:
```json
{
"approvalCriteria": {
"autoDeletionOptions": {
"afterOneUse": true,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
}
}
}
```
### 4. Transfer Limits (Max Num Transfers)
Use `maxNumTransfers` to limit how many times minting can occur:
```json
{
"approvalCriteria": {
"maxNumTransfers": {
"overallMaxNumTransfers": "100",
"perInitiatedByAddressMaxNumTransfers": "1",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"amountTrackerId": "mint-tracker-id",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
}
```
### 5. Appending Minting Approvals After Creation
To allow minting approvals to be added after collection creation:
- Collection must have appropriate permissions (canUpdateCollectionApprovals not frozen for Mint)
- Approval can be added via separate MsgUniversalUpdateCollection transaction
### Mint Escrow (Free Mints with Payout)
The **Mint Escrow Address** is a special reserved address generated from the collection ID that holds Cosmos native funds. Use `mintEscrowCoinsToTransfer` to fund it during collection creation:
```json
{
"collectionId": "0",
"mintEscrowCoinsToTransfer": [{ "denom": "ubadge", "amount": "10000000000" }],
"collectionApprovals": [{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"approvalId": "free-mint",
"approvalCriteria": {
"coinTransfers": [{
"to": "bb1user...",
"coins": [{ "denom": "ubadge", "amount": "1000000000" }],
"overrideFromWithApproverAddress": true,
"overrideToWithInitiator": true
}],
"overridesFromOutgoingApprovals": true
}
}]
}
```
Key escrow rules:
- **overrideFromWithApproverAddress: true**: uses mint escrow as the payer
- **overrideToWithInitiator: true**: pays the user who initiated the mint
- Escrow address has no private key, only collection approvals can transfer from it
### Complete Example: Public Mint with Payment and Incremented IDs
```json
{
"collectionApprovals": [{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"approvalId": "public-mint-5-badge",
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"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": "1",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false
},
"orderCalculationMethod": {
"useOverallNumTransfers": true,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
},
"manualBalances": []
},
"maxNumTransfers": {
"overallMaxNumTransfers": "1000",
"perInitiatedByAddressMaxNumTransfers": "1",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"amountTrackerId": "public-mint-tracker",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
}]
}
```
### Minting Gotchas
- **Must have overridesFromOutgoingApprovals: true** (required for all Mint approvals)
- **coinTransfers override flags**: Should be false for standard payments, true for escrow payouts
- **predeterminedBalances vs approvalAmounts**: These are incompatible, use one or the other
- **orderCalculationMethod**: must have exactly one method set to true
- **amountTrackerId**: Required when using maxNumTransfers or approvalAmounts
- **autoApproveAllIncomingTransfers**: Must be true in defaultBalances for public-mint collections
## Common Mistakes
- Don't use numbers instead of strings for amounts, use "1000" not 1000. All numeric values in BitBadges JSON must be string-encoded.
- Don't forget overridesFromOutgoingApprovals: true on Mint approvals, without it, the Mint address cannot send tokens and minting silently fails.
- Don't forget autoApproveAllIncomingTransfers: true in defaultBalances for public-mint collections, otherwise recipients cannot receive minted tokens.
- Don't forget to add prioritizedApprovals in MsgTransferTokens, even if empty ([]), this field must be present or the transfer fails.
- Don't use predeterminedBalances and approvalAmounts together, they are incompatible. Use one or the other.
- Don't set multiple methods to true in orderCalculationMethod, exactly one must be true (default: useOverallNumTransfers).
## File: agents/skills/multi-sig-voting.md
---
description: "Require weighted quorum voting from multiple parties before transfers can proceed (multi-sig, governance, etc.)"
---
# Multi-Sig / Voting
Instruction text for agents that use the Multi-Sig / Voting skill, loaded by `bb dev skills multi-sig-voting` 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 multi-sig-voting skill and build me one: require weighted quorum voting from multiple parties before transfers can proceed (multi-sig, governance, etc.). 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 multi-sig-voting` prints the same instructions.
## Summary
Enables multi-signature-like approval via votingChallenges[] in approvalCriteria.
- Each voter has an address and a weight
- quorumThreshold: percentage (0-100) of total possible weight that must vote "yes"
- Voters cast votes via MsgCastVote with yesWeight (0-100%)
- Non-voting voters count as 0% yes; threshold is % of all voters' total weight, not just those who voted
- Votes can be updated (re-casting overwrites previous vote)
- proposalId: unique identifier: changing it resets the vote tracker
- v29: resetAfterExecution (bool): automatically resets all votes after quorum is met and transfer executes, enabling recurring multi-sig
- v29: delayAfterQuorum (Uint, ms): enforces a waiting period after quorum before the transfer can execute (e.g., timelock)
- Common patterns: unanimous (threshold: "100", equal weights), majority (threshold: "51"), weighted governance
## Instructions
## Multi-Sig / Voting Challenges
### Concept
Voting challenges enable multi-signature-like approval where multiple parties must vote before a transfer can proceed. Each voter has a configurable weight, and a quorum threshold (percentage of total weight) must be met. This is configured via the `votingChallenges[]` field in `approvalCriteria`.
### How It Works
1. An approval has `votingChallenges` with a list of voters, weights, and a quorum threshold
2. Voters cast votes using `MsgCastVote` with a `yesWeight` (0-100%)
3. When a transfer is attempted, the system checks if the quorum threshold is met
4. Threshold is calculated as a percentage of **total possible weight** (all voters), not just those who voted
5. Non-voting voters count as 0% yes
### Structure
```json
{
"votingChallenges": [
{
"proposalId": "proposal-1",
"quorumThreshold": "50",
"voters": [
{ "address": "bb1alice...", "weight": "100" },
{ "address": "bb1bob...", "weight": "200" },
{ "address": "bb1charlie...", "weight": "50" }
],
"resetAfterExecution": false,
"delayAfterQuorum": "0",
"uri": "",
"customData": ""
}
]
}
```
### Key Fields
- **proposalId**: Unique identifier for tracking votes. Changing it resets the vote tracker.
- **quorumThreshold**: Percentage (0-100) of total possible weight that must vote "yes"
- **voters**: List of voter addresses with their weights
- **yesWeight** (in MsgCastVote): Percentage (0-100%) allocated to "yes"; remainder goes to "no"
- **resetAfterExecution** (v29): If true, all votes are automatically reset after the quorum is met and the transfer executes. Enables recurring multi-sig without rotating proposalIds.
- **delayAfterQuorum** (v29): Delay in milliseconds after quorum is reached before the transfer can execute. Acts as a timelock, gives voters time to change their vote or raise objections.
### Common Patterns
#### Multi-Sig (Unanimous)
All parties must approve. Set `quorumThreshold: "100"` with equal weights:
```json
{ "quorumThreshold": "100", "voters": [
{ "address": "bb1a...", "weight": "1" },
{ "address": "bb1b...", "weight": "1" },
{ "address": "bb1c...", "weight": "1" }
]}
```
#### Majority Vote
Require >50% approval:
```json
{ "quorumThreshold": "51", "voters": [
{ "address": "bb1a...", "weight": "1" },
{ "address": "bb1b...", "weight": "1" },
{ "address": "bb1c...", "weight": "1" }
]}
```
#### Weighted Governance
Different stakeholders have different voting power:
```json
{ "quorumThreshold": "66", "voters": [
{ "address": "bb1founder...", "weight": "1000" },
{ "address": "bb1investor...", "weight": "500" },
{ "address": "bb1community...", "weight": "100" }
]}
```
### Threshold Calculation Example
- Voter A: weight 100, votes 100% yes → contributes 100
- Voter B: weight 200, votes 50% yes → contributes 100
- Voter C: weight 50, doesn't vote → contributes 0
- Total possible weight: 350
- Total yes weight: 200
- Percentage: (200 × 100) / 350 = 57.14%
- If quorumThreshold is 50 → **satisfied**
### Important Notes
- Votes are cast via `MsgCastVote`, a separate transaction from the transfer itself
- Votes can be updated (re-casting overwrites the previous vote)
- Vote keys are scoped: `collectionId-approverAddress-approvalLevel-approvalId-proposalId-voterAddress`
- Set realistic thresholds: high thresholds with many voters may be hard to meet if voters abstain
- **Vote reset behavior**: By default, vote state does not reset after quorum is met (one-time transfers). With v29's `resetAfterExecution: true`, votes are automatically cleared after a successful transfer, enabling recurring multi-sig workflows without rotating proposalIds. Use `delayAfterQuorum` to add a timelock between quorum and execution.
- For full documentation, see the BitBadges docs on voting challenges
## File: agents/skills/nft-collection.md
---
description: "Non-fungible token collection with unique token IDs, metadata URIs, and badge-based ownership"
---
# NFT Collection
Instruction text for agents that use the NFT Collection skill, loaded by `bb dev skills nft-collection` 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 nft-collection skill and build me one: non-fungible token collection with unique token IDs, metadata URIs, and badge-based ownership. 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 nft-collection` prints the same instructions.
## Summary
Required standards: ["NFTs"]
- For tradable NFTs: ["NFTs", "NFTMarketplace", "NFTPricingDenom:ubadge"]
- validTokenIds: set to the range of unique token IDs (e.g. [{ "start": "1", "end": "100" }])
- Each token ID represents a unique NFT; amount in transfers is typically "1"
- Use {id} placeholder in tokenMetadata URI for per-token metadata (e.g. "ipfs://QmHash/{id}")
- Mint approvals must have overridesFromOutgoingApprovals: true
- Ownership times are usually forever for NFTs
## Instructions
## NFT Collection Configuration
When creating an NFT collection, follow this pattern:
### Required Configuration
1. **Standards**: Include "NFTs" in the standards array
- Example: "standards": ["NFTs"]
- For tradable NFTs: "standards": ["NFTs", "NFTMarketplace", "NFTPricingDenom:ubadge"]
2. **validTokenIds**: Set to the range of unique token IDs
- Example for 100 NFTs: [{ "start": "1", "end": "100" }]
- Each token ID represents a unique NFT
3. **Token Metadata**: Each tokenMetadata entry must include tokenIds matching the range
- Use {id} placeholder for per-token metadata URIs
### Pattern Example
```json
{
"updateValidTokenIds": true,
"validTokenIds": [{ "start": "1", "end": "100" }],
"updateCollectionMetadata": true,
"collectionMetadata": {
"uri": "ipfs://QmCollectionMetadata",
"customData": ""
},
"updateTokenMetadata": true,
"tokenMetadata": [{
"uri": "ipfs://QmTokenMetadata/{id}",
"customData": "",
"tokenIds": [{ "start": "1", "end": "100" }]
}],
"updateCollectionApprovals": true,
"collectionApprovals": [{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "bb1creator...",
"approvalId": "manager-mint",
"tokenIds": [{ "start": "1", "end": "100" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true
}
}],
"updateStandards": true,
"standards": ["NFTs"]
}
```
### Sequential Minting with predeterminedBalances
For NFTs, use predeterminedBalances with incrementTokenIdsBy: "1" to mint tokens sequentially (token 1, then 2, then 3, etc.):
```json
{
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"incrementTokenIdsBy": "1",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false
},
"orderCalculationMethod": {
"useOverallNumTransfers": true,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
}
},
"maxNumTransfers": {
"overallMaxNumTransfers": "100",
"perInitiatedByAddressMaxNumTransfers": "1",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"amountTrackerId": "nft-mint-tracker",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
}
```
Key: incrementTokenIdsBy: "1" means each mint gets the next sequential token ID. Use maxNumTransfers to cap total mints and per-user mints.
Important: predeterminedBalances and approvalAmounts are incompatible, use one or the other. NFTs use predeterminedBalances (not approvalAmounts).
### NFT-Specific Gotchas
- Each token ID is unique and represents a distinct NFT
- Amount in transfers is typically "1" (one NFT per transfer)
- Ownership times are usually forever for NFTs
- Mint approvals must have overridesFromOutgoingApprovals: true
- Use {id} in metadata URIs for per-token metadata
## Common Mistakes
- Don't reuse token IDs across editions without understanding ownership times, each token ID is unique and represents a distinct NFT.
- Don't forget tokenIds in canUpdateTokenMetadata permission structure, the permission must specify which token ID ranges it covers.
- Don't use {id} in metadata name, description, or image fields, the {id} placeholder only works in the URI string itself (e.g. "ipfs://abc/{id}").
- Don't forget overridesFromOutgoingApprovals: true on Mint approvals, required for all minting operations.
- Don't use custom list IDs, only reserved IDs are valid: "All", "Mint", "!Mint", "AllWithoutMint", or direct bb1... addresses.
## File: agents/skills/payment-obligations.md
---
description: "Versioned invoice obligations, payer groups, installments, partial targets, split payouts and reusable payment links built from native approvals."
---
# Payment obligations and reusable links
Instruction text for agents that use the Payment obligations and reusable links skill, loaded by `bb dev skills payment-obligations` 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 payment-obligations skill and build me one: versioned invoice obligations, payer groups, installments, partial targets, split payouts and reusable payment links built from native approvals. 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 payment-obligations` prints the same instructions.
## Summary
Use build_payment_request_v2 for PaymentRequestV2 invoices or PaymentLinkV1 reusable links. Every obligation has an independent on-chain approval and tracker. Keep recurring consent and charging in the existing Subscriptions standard.
## Instructions
## Payment obligations
Use the build_payment_request_v2 tool or SDK buildPaymentRequestV2 with version: 2, kind: invoice or payment-link, and obligations. Metadata requires uri or name/image/description. All amounts are positive base-unit integer strings, never display-unit floating point numbers. Times are inclusive Unix milliseconds.
CLI discovery is offline and never signs, simulates or deploys:
- `bb build payment-request-v2 --list-examples` lists specific, anyone, one, all, threshold, installments, partial, target and link.
- `bb build payment-request-v2 --schema` exposes the structural tool schema, limits and its cross-field validation limitations. The SDK runtime terms schema remains authoritative.
- `bb build payment-request-v2 --example all | jq '.data' > payment.json` creates complete editable parameters. Replace demo addresses, amounts, metadata and dates before use. The first finite window ends 30 days after generation; installments add a second future window, and links have no cutoff.
- `bb build payment-request-v2 --json payment.json --json-only` builds the edited terms. JSON also accepts inline input or `-` for stdin. `bb pay-requests build-v2` is an alias; `bb pay-requests build` remains the legacy builder.
- Metadata flags explicitly override JSON metadata: `--uri` selects hosted mode; any of `--name/--image/--description` switches to inline mode and retains other inline JSON values, with all three required. Do not combine URI and inline flags. No metadata flags means JSON metadata is preserved.
- Discovery flags cannot be mixed with JSON build inputs, simulation or signing/deployment flags. Output stays in the normal envelope; extract `data` to reuse an example as builder input.
- Pay an existing collection with `bb pay-requests pay --creator --obligation --units `. Units are payout quanta, not coin display amounts. Obligation may be omitted only for single-obligation collections. Current chain state and simulation still determine whether a payment can execute.
Each obligation has id, payer, payouts, startTime and endTime. payer is {kind: anyone} or {kind: addresses, addresses: [...]}. Anyone excludes all payout recipients on chain to prevent self-payment. Named rosters are immutable inline lists with unique canonical bb1 addresses.
- Specific or one-of-list: one obligation, one eligible address or a roster, default requiredPayments 1.
- All-of-list with custom shares: one independent obligation per payer. Do not use total amount alone as proof that everyone paid.
- K-of-N with equal payment terms: one roster obligation, requiredPayments K, distinctPayers true. The shared overall cap and per-initiator cap are enforced on chain.
- Installments: separate obligations with their own windows and optional informational dueAt. Windows enforce when each installment may be paid; these are not conditional milestone approvals.
- Partial payments/shared targets: partial: {targetUnits: N}; each payout.amount is the base-unit amount per quantum. A transfer of U receipt units transfers U times EVERY payout and consumes U of the cumulative target. maxScalingMultiplier is only a per-transaction cap; overallApprovalAmount enforces the cumulative target. Never round split payouts silently.
- Multiple payouts: all are charged atomically by the same approval. Multiple denominations mean all listed currencies are due, not a choice between currencies.
- Reusable links: kind payment-link; omit requiredPayments, partial and distinctPayers. Each use is a separate receipt event, with an unlimited tracked payment count until the hard cutoff.
The builder freezes terms, roster, approvals, collection invariants and conversion paths. Never hand-edit customData without regenerating approvals. extractPaymentRequestV2Details validates the actual on-chain shape against the declared terms before returning details. Indexers must use the full configured tracker identity, retain per-obligation progress separately from lifecycle, and report unknown when evidence is missing. Never sum mixed denomination amounts or use one paid flag for a collection of obligations.
These direct payments are final transfers with no escrow and no cancellation or refund branch. Unsupported fields are rejected. Refusable legacy PaymentRequest deny is only a recorded refusal: its independent counter does not disable pay. Escrow/refundable pooled funding, conditional release, alternative-currency settlement and cancellable requests need a separately verified state machine; do not simulate them with metadata flags or unrelated trackers. Subscriptions already provide recurring user consent, period counters and incoming approvals: reuse that standard rather than treating a reusable payment link as automatic billing.
Before publication, review and validate the generated collection and simulate actual payments. The SDK/tool builder emits an unsigned message. CLI build emits unsigned output unless explicit signing/deployment flags are supplied; CLI discovery never invokes those paths.
## File: agents/skills/payment-protocol.md
---
description: "Invoices, escrows, bounties, milestones, and multi-party agreements using coinTransfer-based approvals or IBC-backed smart token escrow"
---
# Payment Protocol
Instruction text for agents that use the Payment Protocol skill, loaded by `bb dev skills payment-protocol` 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 payment-protocol skill and build me one: invoices, escrows, bounties, milestones, and multi-party agreements using coinTransfer-based approvals or IBC-backed smart token escrow. 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 payment-protocol` prints the same instructions.
## Summary
Build invoices, milestones, bounties, escrow agreements, or any payment flow.
Two approaches:
- **Approach 1 (coinTransfer-based)**: Simple one-shot payments. Each approval = one invoice/milestone with coinTransfers.
- Standards: ["ListView:Milestones"] or ["ListView:Invoice Requests"] or ["ListView:Bounties"]
- Each approval: fromListId "Mint", coinTransfers for payment, overridesFromOutgoingApprovals: true
- ListView incompatible with: Subscriptions, Smart Tokens, Custom 2FA, Liquidity Pools, Tradable NFTs
- **Approach 2 (Escrow)**: Funds held in IBC-backed smart token until conditions are met.
- Standards: ["Smart Token"]
- USDC/ATOM backed 1:1 into single token ID. Approvals control deposit, release, refund, dispute, timeout.
- Typically 6-12+ approvals modeling the full lifecycle of a multi-party agreement.
- All permissions permanently locked, no one can change rules after deployment.
Key design: each approval = one conditional branch. Not all get used, they define what can happen.
- Lock canUpdateCollectionApprovals for immutable terms
- Initiator pays gas; for mint-based, the payer initiates
## Instructions
## Payment Protocol
Build invoices, milestones, bounties, escrow agreements, or any payment flow.
### Which approach to use
**Default to Approach 2 (Smart Token Escrow)** unless the request is clearly a simple one-shot payment with no hold/release/refund logic. Smart token escrow is the superset, it can do everything coinTransfers can, plus escrow, conditional release, refunds, multi-party deposits, and dispute resolution. When in doubt, use Approach 2.
Use Approach 1 only for simple scenarios like: "create an invoice for 10 BADGE" or "milestone list where payer pays on completion", where funds move immediately at transfer time with no hold period.
### Approach 1: coinTransfer-Based (Simple Payments)
Each approval is an invoice/milestone with coinTransfers. Uses the **ListView** display standard. Only for simple one-shot payments, no escrow, no hold-and-release, no refunds.
**Preferred path: preset per line item.** Each invoice / milestone / bounty entry has a canonical shape, use `payment-protocol.invoice`:
```ts
add_preset_approval({
presetId: "payment-protocol.invoice",
params: {
approvalId: "invoice-1",
payerAddress: "bb1...",
payeeAddress: "bb1...",
amount: "",
denom: ""
}
})
```
Call once per line item. For escrow / hold-and-release (Approach 2) use the smart-token skill.
**Required:**
- Standards: ["ListView:Milestones"] or ["ListView:Invoice Requests"] or ["ListView:Bounties"]
- Each item = one collection-level approval with:
- fromListId: "Mint" (for new tokens) or specific address (for transfers)
- coinTransfers: payment amount and recipient
- overridesFromOutgoingApprovals: true (if fromListId is "Mint")
**Invoice/Milestone Example:**
```json
{
"collectionApprovals": [{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "bb1payer...",
"approvalId": "milestone-1",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"coinTransfers": [{
"to": "bb1payee...",
"coins": [{ "denom": "ubadge", "amount": "10000000000" }],
"overrideFromWithApproverAddress": false,
"overrideToWithInitiator": false
}],
"overridesFromOutgoingApprovals": true,
"maxNumTransfers": {
"overallMaxNumTransfers": "1",
"amountTrackerId": "milestone-1-tracker",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
}],
"standards": ["ListView:Milestones"]
}
```
**ListView incompatibility**: ListView is incompatible with Subscriptions, Smart Tokens, Custom 2FA, Liquidity Pools, Tradable NFTs.
### Approach 2: Escrow (Smart Token)
Use a USDC/ATOM-backed smart token for hold-and-release escrow. Funds are deposited (backed) and released via approval-controlled transfers, then withdrawn (unbacked) for the underlying ICS20 coins.
**When to use escrow vs coinTransfers:**
- coinTransfers: One-shot payment at transfer time (simpler)
- Escrow: Funds held until conditions met, refundable, multi-party (more complex but trustless)
**Architecture: Three Phases**
```text
PHASE 1 - DEPOSITS: Parties back ICS20 coins into the smart token (backing approvals)
PHASE 2 - RESOLUTION: Approvals control who can move tokens to whom, gated by conditions
PHASE 3 - WITHDRAWAL: Winners unback tokens to receive ICS20 coins
```
**Required structure:**
- Standards: ["Smart Token"]
- Invariants: cosmosCoinBackedPath with 1:1 conversion
- Alias path for display
- validTokenIds: [{ "start": "1", "end": "1" }] (single token, amount-capped approvals for logical buckets)
- All permissions permanently locked
**Escrow approval categories:**
1. **Backing approvals (deposits)**: each depositing party gets a separate backing approval with amount caps
```json
{
"fromListId": "bb1backingAddress...",
"toListId": "bb1posterAddress...",
"initiatedByListId": "bb1posterAddress...",
"approvalId": "poster-backing",
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"mustPrioritize": true,
"allowBackedMinting": true,
"maxNumTransfers": { "overallMaxNumTransfers": "1", "amountTrackerId": "poster-backing-tracker", "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" } }
}
}
```
2. **Release approvals (payment paths)**: control how funds move. Gate with:
- Simple sign-off: initiatedByListId = poster address
- Verifier-gated: votingChallenges with quorum threshold
- mustOwnBadges: require deposit verification or credentials
- approvalAmounts: cap how much each approval can move
3. **Timeout approvals (fallbacks)**: use transferTimes to gate when fallbacks activate
```json
{
"approvalId": "timeout-release",
"transferTimes": [{ "start": "DEADLINE_MS", "end": "18446744073709551615" }],
"approvalCriteria": { "overridesFromOutgoingApprovals": true }
}
```
DEADLINE_MS = creation timestamp + timeout hours * 3600000
4. **Verifier fee approvals**: flat fee regardless of decision (neutral incentive). Two approvals: one gated by approve vote, one by deny vote.
5. **Deposit return/forfeit**: worker reclaims on success, poster takes on timeout. Natural mutex via balance depletion.
6. **Unbacking approval**: standard smart token unbacking. Anyone holding tokens can burn for ICS20 coins.
### Multi-Approval Lifecycle Design
Payment protocols typically have **6-12+ approvals** modeling every possible flow. Not all get used in a single transaction, they define the complete lifecycle of what can happen.
**Example: Freelancer escrow with 8 approvals**
1. `poster-deposit`: poster backs USDC into escrow
2. `worker-deposit`: worker deposits a bond (optional)
3. `release-on-completion`: poster releases funds to worker
4. `timeout-refund`: poster reclaims if worker ghosts (time-gated)
5. `dispute-resolution`: arbitrator releases funds (vote-gated)
6. `worker-bond-return`: worker reclaims bond on success
7. `worker-bond-forfeit`: poster takes bond on timeout
8. `penalty-fee`: arbitrator fee on dispute
Happy path uses #1, #3, #6. Dispute uses #1, #2, #5, #7, #8. Timeout uses #1, #4. The approvals define all possibilities.
**Common patterns:**
- **Pattern A: Simple Trust**: poster-backing + poster-release + timeout-release + unbacking (4 approvals)
- **Pattern B: Verified Third-Party**: + approve-release + deny-refund + verifier-fees (7+ approvals)
- **Pattern C: Mutual Deposit + Vote**: both parties deposit, 2-of-3 vote gates release (8+ approvals)
- **Pattern D: Milestones**: poster backs total budget, separate release approval per milestone (N+3 approvals)
- **Pattern E: Bounty**: poster backs bounty, award approval with toListId "All" + overallMaxNumTransfers "1" (4 approvals)
### How conditional branching works
All approvals are effectively **OR logic**, any approval can be satisfied as long as its criteria match. The chain doesn't enforce "if A then B" directly. To implement conditional flows, get creative with criteria composition:
- **Balance depletion as mutex**: approve-release and deny-refund target the same tokens. Once one fires, balance depletes and the other can't execute. Natural mutual exclusion.
- **mustOwnTokens for state gating**: mint soulbound tokens (from this or another collection) to represent state transitions, then require them via mustOwnTokens on downstream approvals. E.g., mint a "work-completed" badge, then the release approval requires holding it. Use collectionId "0" to self-reference this collection, the chain resolves it at runtime, avoiding the need to hardcode the collection ID.
- **transferTimes for temporal gating**: only allow certain approvals after a deadline passes.
- **votingChallenges for human decisions**: gate approvals behind explicit votes from designated parties.
- **Amount caps for partial flows**: use approvalAmounts to limit how much each branch can move, preventing over-claiming.
The primitives (mustOwnTokens, transferTimes, votingChallenges, amount caps, balance depletion) combine to model complex conditional logic even though each approval is independently satisfiable.
### Decision Tree
1. **Default to Smart Token Escrow (Approach 2)** unless clearly a simple one-shot payment. Escrow handles everything, payments, holds, refunds, disputes, multi-party flows. Only use coinTransfers (Approach 1) for trivially simple invoices with no hold period.
2. **Permission locking**: For agreements, lock `canUpdateCollectionApprovals` so terms are immutable.
3. **Who pays gas?**: The initiator pays gas. For mint-based, the payer initiates.
4. **Multiple currencies**: A single collection can accept different IBC denoms in different approvals.
5. **Refunds**: For coinTransfer-based, refunds require a separate approval. For escrow, use timeout-refund approvals.
6. **Timeouts**: Every escrow path must have a timeout fallback, without them, funds can be locked forever.
## Common Mistakes
- Don't use multiple token IDs for different "buckets" in escrow, use a single token ID with amount-capped approvals instead.
- Don't forget timeout fallbacks on every path, funds can be locked forever if a party ghosts.
- Don't make verifier fee conditional on outcome, use flat fee with two vote-gated fee approvals for neutral incentives.
- Don't forget to lock all permissions for escrow, any unlocked permission lets someone change the rules.
- Approval trackers include approvalId in their identity. Reusing amountTrackerId across different approvals does not share a counter; use one approval for a shared cap or a separately enforced state-token mechanism.
- Don't forget mustOwnBadges for deposit verification, this is how you on-chain gate releases on deposits.
## File: agents/skills/payment-request.md
---
description: "Agent-initiated payment request with no escrow. The agent (or any address) creates a collection requesting payment from a targeted human payer. The payer approves and pays from their own wallet in a single action. Inverse of Bounty."
---
# PaymentRequest
Instruction text for agents that use the PaymentRequest skill, loaded by `bb dev skills payment-request` 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 payment-request skill and build me one: agent-initiated payment request with no escrow. The agent (or any address) creates a collection requesting payment from a targeted human payer. The payer approves and pays from their own wallet in a single action. Inverse of Bounty. 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 payment-request` prints the same instructions.
## Summary
Required standards: ["PaymentRequest"]
- 1 token ID (vehicle for approval engine, minted directly to burn)
- Specific payer: 2 approvals (pay, deny). Public payer All: exactly 1 approval (pay only).
- Each approval: Mint → burn 1x token ID 1
- Pay approval triggers a coinTransfer from the payer's wallet to the recipient
- No mintEscrowCoinsToTransfer: payment debits the payer's wallet at execution time
- Approval gating via initiatedByListId scoped to the payer (no votingChallenges)
- Fixed payment amount, no amount scaling
- Both approvals maxNumTransfers = 1 (one-shot)
- All permissions frozen after creation
- Expiration is implicit: both approvals share transferTimes [1, expirationMs]; no separate expire approval (no escrow to refund)
## Instructions
## PaymentRequest Standard
### Mental Model
PaymentRequest is the **inverse of Bounty**: instead of an escrow-based reward where the submitter pre-funds and the verifier votes, this is an agent-initiated payment request where the targeted human payer approves and pays from their own wallet in one action.
Two parties:
- **Requester** (agent or merchant): Creates the collection, specifies payer + amount + recipient
- **Payer** (human): Sees the request, decides to approve+pay or deny
There's no escrow. The payment doesn't move until the payer signs the approval. Funds debit directly from the payer's wallet at execution because the pay approval uses `overrideFromWithApproverAddress: false`, the chain default routes the coinTransfer's "from" to the initiator (the payer, scoped via `initiatedByListId`).
This is the on-chain equivalent of Stripe Link's spend-request flow: the agent presents a payable artifact with rationale, the human approves, the credential settles. Mirror the rationale-bound, single-use, expiry-gated pattern, but with chain rails instead of card rails.
### Token Structure
- Token ID 1 = PaymentRequest token (vehicle for approval engine)
- validTokenIds: [{ start: "1", end: "1" }]
- No alias path needed (1-of-1 receipt-style token)
### Required Approvals
For a public invoice, use payer "All" and create only the pay approval. There is no denial or cancellation action; the request stays pending until paid or expired. Do not add a deny approval, even one restricted to the requester. The pay-only shape is validated by the SDK and indexer.
The following pay+deny pair applies only to specific-payer requests.
Both approvals share: Mint → burn address, 1x token ID 1, maxNumTransfers = 1, overridesFromOutgoingApprovals=true, overridesToIncomingApprovals=true. No votingChallenges (gating is via initiatedByListId, not voting). Both are time-gated to `[1, expirationTimestamp]`, once that window closes, neither can fire and the request is implicitly expired (no separate expire approval is needed because there's no escrow to refund).
#### Preferred path: presets (omit the deny call when payer is "All")
```ts
add_preset_approval({
presetId: "payment-request.pay",
params: { approvalId, payer, recipient, denom, amount, expirationMs }
})
add_preset_approval({
presetId: "payment-request.deny",
params: { approvalId, payer, expirationMs }
})
```
`list_presets({skill: "payment-request"})` lists legacy params. For payer groups, installments, partial targets, splits and reusable links, use `build_payment_request_v2` and the payment-obligations skill; do not create unverified variants with raw approvals.
#### 1. Pay (payment-request-pay-*)
Payer approves → mint-to-burn → coins move from payer to recipient.
Key fields:
- fromListId: "Mint"
- toListId: burn address (bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv)
- initiatedByListId: payer's bb1... address, or "All" for a public pay-only request
- coinTransfers: [{ to: recipientAddress, overrideFromWithApproverAddress: false, overrideToWithInitiator: false, coins: [{ denom, amount }] }]
- **Critical**: `overrideFromWithApproverAddress` must be false. The chain default routes "from" to the initiator (the payer). Setting true would attempt to debit a non-existent escrow → tx fails.
- transferTimes: [{ start: "1", end: expirationTimestamp }]
- maxNumTransfers.overallMaxNumTransfers: "1"
#### 2. Deny (payment-request-deny-*, specific payer only)
Payer rejects → mint-to-burn → no coin transfer. Records denial state for indexers/UIs.
Same as Pay but:
- No coinTransfers (or empty array)
- Same initiatedByListId (payer)
- Same transferTimes (concurrent with pay)
### Settlement Flow
1. Agent (or merchant) creates the collection, no escrow funded.
2. Payer sees the request in their dashboard / wallet (rationale, line items, amount).
3. Payer either:
- **Approves+pays**: signs MsgTransferTokens from Mint → burn (1x token ID 1) with prioritizedApprovals targeting the pay approval. Coins debit from their wallet to the recipient automatically.
- **Denies**: signs MsgTransferTokens targeting the deny approval. No coins move.
4. If the payer does neither before `expirationTimestamp`, both approvals become un-fireable (transferTimes window closed). Clients display the request as expired by comparing current time to `transferTimes[0].end`.
### Key Differences from Bounty
- **No mintEscrowCoinsToTransfer** at the collection level
- **Pay approval uses overrideFromWithApproverAddress: false** (Bounty uses true)
- **No votingChallenges**: gating is via initiatedByListId scoped to payer
- **Deny has no coinTransfers**, no funds need to be returned (no escrow to refund)
- **No expire approval**: Bounty needs one to refund escrow, but there's no escrow here. Expiration is implicit via the shared `transferTimes[0].end`.
- **1 public pay approval, or 2 specific-payer approvals**, instead of Bounty's 3
- Same mint-to-burn vehicle, same frozen permissions
### Creation Flow (Tool Calls)
1. Use per-field tools to initialize the collection
2. `set_valid_token_ids`: set [{ start: "1", end: "1" }]
3. `set_standards`: set ["PaymentRequest"]
4. `set_invariants`: set { noCustomOwnershipTimes: true, disablePoolCreation: true, noForcefulPostMintTransfers: true }
5. **Do not** call set_mint_escrow_coins, there's no escrow
6. `add_preset_approval`: pay only for All; pay + deny for a specific payer (or `add_approval` for raw)
7. `set_permissions`: freeze all permissions
8. `set_collection_metadata`: name + the rationale (≥100 chars recommended; mirror Stripe Link's bar)
9. `set_token_metadata`: token 1 metadata
10. `validate_transaction`: verify structure (verifyPaymentRequest enforces the no-escrow invariants)
11. `simulate_transaction`: dry run
### Permissions
All permissions must be frozen (same set as Bounty).
### Common Mistakes
- **Don't set overrideFromWithApproverAddress=true on the pay approval**, that's the Bounty pattern. PaymentRequest needs false so the chain debits the payer (initiator), not a non-existent escrow.
- **Don't add votingChallenges**: PaymentRequest gating is via initiatedByListId, not voting. Voting is a Bounty construct.
- **Don't fund mintEscrowCoinsToTransfer**: there is no escrow. The payer pays at execution time.
- **Don't add a deny approval to an All-payer request**, public invoices have only a pay approval. Specific-payer invoices keep both approvals scoped to that payer.
- **Don't add a third "expire" approval**. Bounty needs one to refund escrow. PaymentRequest has no escrow, so an expire branch would just be a no-op marker. Validator rejects collections with more than 2 approvals.
- **Don't forget the rationale** in collection metadata description, it's what the human reads to decide.
- **Don't use overrideToWithInitiator** on the coinTransfer, the recipient is hardcoded.
### Relationship to the Invoices Standard
The existing `Invoices` standard validates a single payer-as-initiator approval, useful as a building block, but it has no deny branch and no targeted-payer scoping. PaymentRequest is a more constrained, agent-payments-specific subset: same payment direction (initiator → address), but with an explicit pay+deny pair for specific payers, or a pay-only public invoice that remains open until paid or expired. Consumers that want any payer-initiated payment can match `Invoices`; consumers that want the agent-payments artifact specifically should match `PaymentRequest`.
## File: agents/skills/prediction-market.md
---
description: "Binary prediction market with YES/NO outcome tokens, liquidity pool trading, and vote-based settlement"
---
# Prediction Market
Instruction text for agents that use the Prediction Market skill, loaded by `bb dev skills prediction-market` 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 prediction-market skill and build me one: binary prediction market with YES/NO outcome tokens, liquidity pool trading, and vote-based settlement. 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 prediction-market` prints the same instructions.
## Summary
Required standards: ["Prediction Market"]
- Use bb build prediction-market; YES token 1 and NO token 2 are collateralized pairs.
- Deposit 1 collateral base unit to mint 1 YES and 1 NO base unit; pair redemption reverses this.
- Winner burns 1 raw unit for 1 collateral base unit. Push burns 2 raw units for 1 collateral base unit.
- New markets support repeated consuming redemptions. Frozen legacy markets may allow only one claim per initiator.
- The verifier controls mutable voting challenges. This is not an immutable, mutually exclusive outcome oracle.
- Quote before proposing; never silently discard odd push dust or burn losing positions.
- Preserve frozen permissions, no-forceful-transfer invariant and escrow-funded payout routing.
## Instructions
## Prediction Markets
### CLI First
Use the canonical builder rather than hand-writing approval ratios:
~~~sh
bb build prediction-market --verifier "$VERIFIER" --denom USDC --name "Example market" --description "Define the event, evidence and resolver policy" --image "ipfs://image" > market.json
bb check market.json
bb deploy market.json --browser
bb prediction-markets show "$COLLECTION_ID"
bb prediction-markets deposit "$COLLECTION_ID" --creator "$HOLDER" --amount 1000000 > deposit.json
bb deploy deposit.json --browser
~~~
Creation returns seven frozen approvals: paired mint, transferable, pair redeem, YES winner, NO winner, push YES and push NO. YES is token 1 and NO token 2. Alias paths uyes/uno expose display units; amounts supplied to deposit, quote and redeem are always integer base units. For six-decimal collateral, 1000000 base units equals one display coin. Payout amounts exclude network and protocol fees.
### Exact Redemptions
~~~sh
bb prediction-markets quote "$COLLECTION_ID" --state yes-wins --yes-amount 250000 --yes-balance 1000000
bb prediction-markets redeem "$COLLECTION_ID" --creator "$HOLDER" --state yes-wins --yes-amount 250000 > redeem.json
bb deploy redeem.json --browser
bb prediction-markets quote "$COLLECTION_ID" --state push --yes-balance 5 --no-balance 3
bb prediction-markets redeem "$COLLECTION_ID" --creator "$HOLDER" --state active --pair-amount 1 > pair.json
~~~
Explicit --yes-amount / --no-amount selects a partial redemption. It must match a whole payout lot and fit the supplied balance. An explicit side amount of 0 skips that side, retaining its full balance. Pair amounts must be positive. Without an explicit amount, a supplied balance selects the maximum complete lots. The push example burns 4 YES and 2 NO, receives 3 collateral base units and retains one raw unit of each side. Explicit --yes-amount 5 for push fails; it is never rounded. Retained complementary YES/NO dust can be paired and redeemed for one collateral unit. A single unmatched raw unit cannot pay half a collateral base unit and remains transferable.
Winner redemption leaves losing positions untouched. Pair redemption is an independently available consuming route; the canonical frozen recipe does not stop pair deposits or pair redemptions at resolution. Do not describe this as an enforced trading deadline.
New settlements have an overall MAX_UINT64 transfer-count ceiling with no per-initiator limit. Repeated and later-acquired positions can redeem while the selected approval remains eligible and escrow is funded. Existing deployed approvals are never rewritten. Legacy per-initiator one-shot approvals may be exhausted after an earlier partial claim; quote reports the policy and unknown eligibility when tracker state has not been loaded. Current balances, votes, tracker counts, approval windows, escrow liquidity and fees still require fresh validation/simulation before signing.
### Verifier Trust
~~~sh
bb prediction-markets resolve "$COLLECTION_ID" --creator "$VERIFIER" --outcome yes > resolution.json
bb deploy resolution.json --browser
~~~
YES/NO emit one vote; push emits votes for both push approvals. Independent voting challenges authorize payouts. They do not supply an immutable single terminal outcome or cross-approval exclusion. The resolver must follow the stated event policy; do not claim trustless finality or guaranteed collateral solvency under inconsistent resolver decisions. Frozen collection permissions do not freeze votes. A stronger finality model requires a separate proven primitive or oracle design.
### SDK and MCP
quotePredictionMarketRedemption(collection, request) returns exact payout, burn legs, retained positions, observed approval IDs/versions, policy and eligibility caveats. buildPredictionMarketRedeemTx with collection uses the same quote and refuses a known exhausted allowance. The lower-level overload cannot discover deployed terms; prefer the collection-aware path.
MCP standard_prediction_markets_quote and standard_prediction_markets_redeem mirror CLI arguments and only return reads or unsigned proposals. Use the CLI for browser signing. Approval presets remain available for advanced composition, with the same conserving ratios and amount scaling; run validation on the complete collection after composing.
### Checks
Never infer finality from a deadline, an indexed status or a successful earlier vote alone. Do not invent an approval ID or silently fall back to an unrelated transfer approval when a payout approval is missing. Use current observed approval versions. Do not convert base-unit balances through floating-point numbers. Unknown state is not zero balance or an unused allowance.
## File: agents/skills/product-catalog.md
---
description: "Multi-product storefront with per-product pricing, supply limits, and optional burn-on-purchase. Each product is a separate token ID."
---
# Products
Instruction text for agents that use the Products skill, loaded by `bb dev skills product-catalog` 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 product-catalog skill and build me one: multi-product storefront with per-product pricing, supply limits, and optional burn-on-purchase. Each product is a separate token ID. 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 product-catalog` prints the same instructions.
## Summary
Required standards: ["Products"]
- N token IDs (one per product), starting at 1
- N+1 approvals: 1 purchase approval per product + 1 optional burn approval
- Each purchase approval: fromListId="Mint", toListId="All" (or burn address if burn-on-purchase), 1 coinTransfer paying the store address
- Payment goes directly to store address (not to escrow): overrideFromWithApproverAddress: false
- Each product has independent price, supply limit (maxNumTransfers), and burn-on-purchase toggle
- predeterminedBalances.incrementedBalances.startBalances: 1x of that product's token ID
- Optional burn approval: !Mint → burn address, no coinTransfers
- invariants: { noCustomOwnershipTimes: true }
- All permissions frozen after creation
- Don't use overrideFromWithApproverAddress: payment goes directly to store, not from escrow
- Don't use allowAmountScaling: fixed price per item
- Don't use votingChallenges, merkleChallenges, or mustOwnTokens
- Do use unique approvalId per product (e.g. "product-purchase-1", "product-purchase-2")
- Do set maxNumTransfers to supply limit (0 = unlimited)
## Instructions
## Products Configuration
### Mental Model
A multi-product storefront where each product is a separate token ID. Buyers pay coins to mint a product token. Each product has its own price, supply limit, and optional burn-on-purchase setting. Payment goes directly to the store owner's address (not escrow).
### Collection Structure
- Token IDs 1..N (one per product)
- Standard: "Products"
- validTokenIds: [{ start: "1", end: "" }]
- invariants: { noCustomOwnershipTimes: true }
- All permissions frozen after creation
### Approval Structure
Each product gets its own purchase approval. There's also an optional global burn approval.
### Preferred path: CLI
Use `bb build product-catalog --json catalog.json --creator --output-file proposal.json`.
Inspect `bb dev capabilities build_product_catalog` for the current input schema. Each products entry accepts
name, price (display units), denom, maxSupply, burn and metadata. Optional product.storeAddress
overrides the catalog's default storeAddress for that product only; BitBadges and EVM account
addresses are accepted. One purchase still pays exactly one recipient.
Use `bb products show ` to inspect the deployed catalog, then
`bb products purchase --token-id --creator ` to build a purchase.
Review, simulate and explicitly request browser signing as documented by the CLI lifecycle.
### Catalog lifecycle
Catalogs created by this builder freeze products, prices, recipients, supply caps and metadata.
Publish a new collection for new terms. Do not rebuild an existing catalog using this creation
builder: changing token IDs, approval IDs or tracker IDs can detach receipts and purchase counts.
The frontend catalog form is creation-only; its reorder/delete actions apply only to unsigned drafts
and move each product's metadata with its terms. Custom mutable collections require an advanced
update that preserves identities and obeys their existing permissions.
Payments are direct to the seller, not held for conditional release. Burning a product receipt
does not refund payment, restock the product or prove delivery. Cancellations, refunds and timed
licenses are not built-in catalog actions; they need a separately reviewed design before creation.
### Advanced path: presets (one call per product + optional burn)
Call `products.purchase` once per product (productIndex 1..N). Add `products.burn` once if you need the global burn/redeem:
```ts
add_preset_approval({
presetId: "products.purchase",
params: {
productIndex: 1,
storeAddress: "bb1...",
priceAmount: "",
denom: "",
supplyLimit: "0", // "0" = unlimited
burnOnPurchase: false // true = token minted straight to burn (receipt-style)
}
})
// Optional:
add_preset_approval({ presetId: "products.burn", params: { numProducts: 3 } })
```
#### Purchase Approval (per product)
```json
{
"approvalId": "product-purchase-1",
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [{
"to": "",
"overrideFromWithApproverAddress": false,
"overrideToWithInitiator": false,
"coins": [{ "amount": "", "denom": "" }]
}],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"allowAmountScaling": false,
"maxScalingMultiplier": "0"
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": { "overallMaxNumTransfers": "" }
}
}
```
> For burn-on-purchase products, set toListId to the burn address instead of "All". The buyer never holds the token, it's minted directly to burn, and the purchase receipt is the transaction itself.
#### Burn Approval (optional, for all products)
```json
{
"approvalId": "product-burn",
"fromListId": "!Mint",
"toListId": "",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [],
"maxNumTransfers": { "overallMaxNumTransfers": "0" }
}
}
```
### Creation Flow (Tool Calls)
1. `set_valid_token_ids`: set [{ start: "1", end: "" }]
2. `set_standards`: set ["Products"]
3. `set_invariants`: set { noCustomOwnershipTimes: true }
4. `add_approval` xN: one purchase approval per product
5. `add_approval`: optional burn approval
6. `set_collection_metadata`: store name, description, image
7. `set_token_metadata` xN: metadata for each product
8. `set_permissions`: preset "fully-immutable"
9. `validate_transaction`: verify structure
10. `simulate_transaction`: dry run
### Common Mistakes
- Don't use overrideFromWithApproverAddress on purchase approvals, payment goes directly to the store address, not from escrow
- Don't use allowAmountScaling: each purchase is exactly 1 item at fixed price
- Don't use a single approval for multiple products, each product needs its own approval with its own tokenIds, price, and supply limit
- Don't forget unique approvalIds, duplicate IDs will cause the chain to reject the transaction
- Don't set maxNumTransfers > 0 on the burn approval, burns should be unlimited
- Don't use votingChallenges, merkleChallenges, or mustOwnTokens, purchases are open to all
- Don't forget to set toListId to burn address for burn-on-purchase products
## File: agents/skills/quest.md
---
description: "Quest/reward collection: users complete criteria and claim a badge + coin payout"
---
# Quest
Instruction text for agents that use the Quest skill, loaded by `bb dev skills quest` 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 quest skill and build me one: quest/reward collection: users complete criteria and claim a badge + coin payout. 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 quest` prints the same instructions.
## Summary
Required standards: ["Quests"]
- Single token only: validTokenIds = [{start: "1", end: "1"}]
- Quest approval must be properly gated, typically via an off-chain claim (merkle challenge with claimConfig), but can also use on-chain criteria (mustOwnTokens, dynamicStoreChallenges, evmQueryChallenges, votingChallenges)
- Coin transfers with overrideFromWithApproverAddress: true + overrideToWithInitiator: true
- predeterminedBalances: amount 1, no increments, no recurring, no duration
- Escrow funded upfront via set_mint_escrow_coins (rewardAmount * maxClaims)
- invariants.noCustomOwnershipTimes: true
- Permissions: use "locked-approvals" preset (recommended)
- Default balances: empty balances, all auto-approve flags true
## Instructions
## Quest Configuration
### Mental Model
A quest collection rewards users for completing criteria. Users receive a quest badge (token 1) + coin payout.
The quest approval must be properly gated so that only eligible users can claim. Gating options:
- **Off-chain claim (most common)**: A merkle challenge with claimConfig containing plugins (password, codes, whitelist, etc.). The claim is verified off-chain by BitBadges, and a merkle proof is issued for on-chain redemption.
- **On-chain criteria**: mustOwnTokens (require holding specific tokens/badges), dynamicStoreChallenges, evmQueryChallenges, votingChallenges, these are checked directly on-chain during the transfer.
- **Both**: Combine off-chain claims with on-chain criteria for layered verification.
Choose the gating approach based on the user's request. If they mention passwords, codes, or whitelists, use an off-chain claim. If they mention token ownership or on-chain conditions, use the corresponding on-chain criteria.
### Build Steps (call all in parallel in one round)
1. `set_standards` → `["Quests"]`
2. `set_valid_token_ids` → `[{ "start": "1", "end": "1" }]`
3. `set_invariants` → `{ "noCustomOwnershipTimes": true }`
4. `set_permissions` → `{ "preset": "locked-approvals" }`
5. `set_default_balances` → empty balances, all auto-approve true
6. `set_collection_metadata` / `set_token_metadata`: descriptive content
7. `add_approval`: the quest approval (see exact structure below)
8. **`set_mint_escrow_coins`**: required for coin rewards. Amount = rewardPerClaim × maxClaims.
### Quest Approval (add_approval)
Use approvalId `"quest-approval"`. The exact approvalCriteria structure:
```json
{
"approvalId": "quest-approval",
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"tokenIds": [{"start":"1","end":"1"}],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"maxNumTransfers": {
"overallMaxNumTransfers": ""
},
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [{ "amount": "1", "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" }
},
"orderCalculationMethod": {
"useOverallNumTransfers": true,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
}
},
"coinTransfers": [{
"to": "",
"overrideFromWithApproverAddress": true,
"overrideToWithInitiator": true,
"coins": [{ "amount": "", "denom": "" }]
}]
}
}
```
**Gating**: add one OR more of these to approvalCriteria based on the user's request:
- **Off-chain claim**: `"merkleChallenges": [{ "root": "", "expectedProofLength": "0", "maxUsesPerLeaf": "1", "uri": "", "customData": "", "useCreatorAddressAsLeaf": false, "claimConfig": { "approach": "in-site", "label": "...", "plugins": [...] } }]`
- **Token ownership**: `"mustOwnTokens": [{ "collectionId": "...", "amountRange": {"start":"1","end":"18446744073709551615"}, ... }]`. Use collectionId "0" to self-reference this collection (e.g., require holding token 1 from this quest collection itself).
- **Dynamic store**: `"dynamicStoreChallenges": [...]`
- **EVM query**: `"evmQueryChallenges": [...]`
Off-chain claims are the most common for quests (passwords, codes, whitelists). On-chain criteria can be combined with or used instead of claims.
### Escrow Funding (required)
Call `set_mint_escrow_coins` in the same round as the other tools. Example for 10 ATOM reward × 50 claims:
```ts
set_mint_escrow_coins({ coins: [{ denom: "ibc/A4DB...", amount: "500000000" }] })
```
Without this, the escrow has no funds and claims will fail.
### Common Mistakes
- Don't add extra fields to coinTransfers, the only fields are: to, overrideFromWithApproverAddress, overrideToWithInitiator, coins. No startTime, no other fields.
- Don't omit `manualBalances: []` in predeterminedBalances, SDK crashes without it
- Don't omit fields in orderCalculationMethod, include all boolean fields
- Don't forget `set_mint_escrow_coins`: without it, the escrow is empty and rewards can't be paid
- Don't set maxUsesPerLeaf to anything other than "1", each user claims once
- Don't set allowOverrideTimestamp: true, quests require false
- Don't set useCreatorAddressAsLeaf: true, quests require false
## File: agents/skills/smart-token.md
---
description: "IBC-backed smart token with 1:1 backing and two required approvals (backing + unbacking)"
---
# Smart Token
Instruction text for agents that use the Smart Token skill, loaded by `bb dev skills smart-token` 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 smart-token skill and build me one: iBC-backed smart token with 1:1 backing and two required approvals (backing + unbacking). 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 smart-token` prints the same instructions.
## Summary
Required standards: ["Smart Token"]
- Must include cosmosCoinBackedPath in invariants with conversion sideA/sideB
- Must configure at least one alias path (decimals must match IBC denom decimals)
- Must create two required collection approvals (backing + unbacking). Transferable approval is common but optional:
1. Backing approval (required): fromListId = backing address, allowBackedMinting: true, mustPrioritize: true
2. Transferable approval (optional: include for wrapped assets, omit for vaults/escrows): fromListId = "!Mint", toListId = "All"
3. Unbacking approval (required): toListId = backing address, allowBackedMinting: true, mustPrioritize: true
- Do not use fromListId: "Mint", tokens are created via IBC backing, not traditional minting
- Backing addresses are protocol-controlled with auto-set approvals, overridesFromOutgoingApprovals is irrelevant for backing/unbacking (leave unset or false)
- noForcefulPostMintTransfers invariant should be true, smart tokens do not need forceful transfer overrides
- Unbacking fromListId uses "!Mint:backingAddress" syntax (excludes both Mint and backing address, meaning only regular holders can unback)
- Backing address is deterministic, use generate_backing_address tool
- Optional: Add "AI Agent Vault" to standards for AI Prompt tab (display-only)
- Alias path: symbol = base unit (e.g. "uvatom"), denomUnits = display units with decimals > 0 only, each denomUnit must have PathMetadata with a placeholder uri
- PathMetadata only has `{ uri, customData }`, do not include image/name/description fields. Set metadata.uri to a placeholder like `"ipfs://METADATA_ALIAS_"` and register the image/name/description in the metadataPlaceholders sidecar keyed by that URI
## Instructions
## Smart Token Configuration
### Mental Model: Three Phases
Think about smart tokens in three distinct phases:
1. **Phase 1: Deposits (Backing)**: Users send IBC coins (e.g. USDC) to the backing address and receive wrapped tokens 1:1. This is the on-ramp.
2. **Phase 2: Transferability (While Backed)**: While tokens exist in the wrapped silo, can users transfer them peer-to-peer? This is the key design decision, transferable for wrapped assets, non-transferable for vaults/escrows.
3. **Phase 3: Withdrawals (Unbacking)**: Users send wrapped tokens back to the backing address and receive their IBC coins 1:1. This is the off-ramp. Rate limits, 2FA, and other controls go here.
Each phase maps to at least one collection approval. Design each phase independently, they are orthogonal concerns.
### Required Structure
1. **Standards**: must include "Smart Token"
- "standards": ["Smart Token"]
2. **Invariants**: must include cosmosCoinBackedPath
```json
{
"standards": ["Smart Token"],
"invariants": {
"cosmosCoinBackedPath": {
"conversion": {
"sideA": {
"amount": "1",
"denom": "ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701"
},
"sideB": [{
"amount": "1",
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}]
}
}
}
}
```
3. **Alias Paths**: must configure at least one alias path
- The alias path decimals must match the IBC denom's decimals
- This is required for Smart Tokens to function properly
- Use the alias path configuration provided in the skill config
- Alias path denom and symbol must only contain a-zA-Z, _, {, }, and - characters. Never use the raw IBC denom (ibc/...) as the alias path denom, create a new symbol like "wuusdc" or "uwrapped"
- Do not reuse reserved symbols (USDC, ATOM, BADGE, etc.), always prefix with "w" or similar (e.g., wUSDC, wATOM)
### Approval System
Smart Tokens require two approvals (backing + unbacking). A third transferable approval is common for wrapped assets but optional for vaults/escrows:
#### 1. Backing Approval (for backing tokens)
This approval allows tokens to be sent from the IBC backing address (backing the tokens).
```json
{
"fromListId": "bb1backingaddress...",
"toListId": "!bb1backingaddress...",
"initiatedByListId": "All",
"approvalId": "smart-token-backing",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"mustPrioritize": true,
"allowBackedMinting": true
}
}
```
Note: Backing addresses are protocol-controlled with auto-set approvals. overridesFromOutgoingApprovals is irrelevant and can be omitted.
#### 2. Transferable Approval (optional: for peer-to-peer transfers)
This approval allows tokens to be transferred between users (non-backing addresses). Include for wrapped assets where holders need to trade/transfer. Omit for simple deposit/withdraw vaults or escrows where tokens should not move between users.
```json
{
"fromListId": "!Mint",
"toListId": "All",
"initiatedByListId": "All",
"approvalId": "transferable-approval",
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
```
#### 3. Unbacking Approval (for unbacking tokens)
This approval allows tokens to be sent to the IBC backing address (unbacking the tokens).
The `!Mint:bb1backingaddress...` fromListId uses colon-separated exclude syntax, it means "everyone except Mint and the backing address", so only regular holders can unback.
```json
{
"fromListId": "!Mint:bb1backingaddress...",
"toListId": "bb1backingaddress...",
"initiatedByListId": "All",
"approvalId": "smart-token-unbacking",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"mustPrioritize": true,
"allowBackedMinting": true
}
}
```
### Multi-Message Deposit/Withdraw Pattern
For safety, backing approvals enforce that the initiator must be the recipient (deposit) and the initiator must be the sender (withdraw). To deposit to or withdraw for another address, use a multi-message transaction:
#### Deposit to Another Address (2 msgs in one tx)
1. **MsgTransferTokens**: Deposit to self (from: backingAddress, to: self)
2. **MsgTransferTokens**: Transfer from self to target (from: self, to: targetAddress), uses the transferable approval
#### Withdraw for Another Address (2 msgs in one tx)
1. **MsgTransferTokens**: Withdraw to self (from: self, to: backingAddress)
2. **MsgSend** (cosmos bank): Send equivalent IBC coins from self to target address
### Optional: AI Agent Vault Standard
Add "AI Agent Vault" to the standards array to enable an AI Prompt tab in the frontend. This is **display-only** and has no impact on on-chain logic.
```json
{
"standards": ["Smart Token", "AI Agent Vault"]
}
```
### Optional: DEX Tradability (Liquidity Pools)
Most smart tokens are not tradable on DEX, only add this if the user explicitly requests trading/DEX functionality:
- Add "Liquidity Pools" to standards
- Set invariants.disablePoolCreation to **false** (override the safe default)
- Must have at least one alias path configured
- Must have a transferable approval (peer-to-peer transfers required for trading)
### IBC Backed Minting Rules
- **Backing/unbacking approvals**: Backing addresses are protocol-controlled with auto-set approvals, overridesFromOutgoingApprovals is **irrelevant** (leave unset or false). noForcefulPostMintTransfers should be true.
- **Unbacking fromListId**: Use `!Mint:backingAddress` syntax, colon-separated addresses with `!` prefix means "everyone except Mint and backing address" (only regular holders can unback)
- **Use allowBackedMinting: true** for IBC backed operations
- **Use mustPrioritize: true** (required, not compatible with auto-scan)
- **Backing Address**: Use the backing address as fromListId (not "Mint"): generated deterministically from the IBC denom via generate_backing_address tool
### Alias Path Configuration
Must configure at least one alias path. Structure:
```json
{
"aliasPathsToAdd": [{
"denom": "uvatom",
"symbol": "uvatom",
"conversion": {
"sideA": { "amount": "1" },
"sideB": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }]
},
"denomUnits": [{
"decimals": "6",
"symbol": "vATOM",
"isDefaultDisplay": true,
"metadata": { "uri": "ipfs://METADATA_ALIAS_uvatom_UNIT", "customData": "" }
}],
"metadata": { "uri": "ipfs://METADATA_ALIAS_uvatom", "customData": "" }
}]
}
```
Rules:
- symbol = base unit symbol (e.g., "uvatom")
- denomUnits = display units with decimals > 0 only (base decimals 0 is implicit)
- Each denomUnit and the path itself must have PathMetadata of the form `{ uri, customData }`. Use a placeholder URI like `"ipfs://METADATA_ALIAS_"` / `"ipfs://METADATA_ALIAS__UNIT"`.
- isDefaultDisplay: true for the primary display unit
- **Critical**: PathMetadata has exactly two fields, `uri` and `customData`. Never add `image`, `name`, or `description` here. Register the name, description, and image for each placeholder URI in the `metadataPlaceholders` sidecar returned alongside the transaction, the metadata auto-apply flow uploads the off-chain JSON and substitutes the placeholder URIs with real IPFS URIs after deploy.
### Cosmos Coin Wrapper (Optional)
For wrapping native Cosmos SDK coins, use `allowSpecialWrapping: true` and `cosmosCoinWrapperPathsToAdd`:
```json
{
"cosmosCoinWrapperPathsToAdd": [{
"denom": "uatom",
"symbol": "uatom",
"conversion": {
"sideA": { "amount": "1" },
"sideB": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }]
},
"denomUnits": [{ "decimals": "6", "symbol": "ATOM", "isDefaultDisplay": true, "metadata": { "uri": "ipfs://METADATA_WRAPPER_uatom_UNIT", "customData": "" } }],
"metadata": { "uri": "ipfs://METADATA_WRAPPER_uatom", "customData": "" },
"allowOverrideWithAnyValidToken": false
}]
}
```
### Optional: Unbacking Withdraw Limits
Add approvalAmounts to the unbacking approval to enforce daily or total withdraw limits.
**Critical: approvalAmounts are in base units, not display units.** The value you write is in the alias denom's base units (e.g. `uusdc`, `uatom`), not the display unit (USDC, ATOM). You must convert by multiplying the user's stated amount by 10^decimals.
For a 6-decimal denom (USDC, ATOM, vUSDC, etc.):
| User says (display) | Write as (base units) |
|-------------------------|-----------------------|
| 1 USDC / day | "1000000" |
| 100 USDC / day | "100000000" |
| **1000 USDC / day** | **"1000000000"** (nine zeros, not six) |
| 10000 USDC / day | "10000000000" |
Formula: `base_units = display_amount * 10^decimals`. For 6 decimals, that's **display_amount followed by six zeros**. Always sanity-check: 1 display unit = 10^decimals base units, so "1000000" (six zeros) = 1 display unit, not 1000. A common mistake is to compute "1000 × 10^6 = 1000000" (wrong; it's 1,000,000,000).
```json
{
"approvalCriteria": {
"mustPrioritize": true,
"allowBackedMinting": true,
"overridesFromOutgoingApprovals": false,
"approvalAmounts": {
"overallApprovalAmount": "0",
"perFromAddressApprovalAmount": "1000000000",
"perToAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "daily-withdraw-limit",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "86400000" }
}
}
}
```
The example above encodes a **1000 USDC/day** limit (1000 × 10^6 = 1,000,000,000 base units).
- **Daily limit**: Use perFromAddressApprovalAmount with resetTimeIntervals.intervalLength: "86400000" (24 hours in ms)
- **Total limit**: Use overallApprovalAmount with intervalLength: "0" (no reset)
- amountTrackerId must be unique per approval
- **When refining**: if the user reports "rate is X currently" and the on-chain value is already in base units, compare apples to apples, convert their stated X to base units via the formula above before concluding whether the current value matches.
### Optional: 2FA on Unbacking
Require ownership of a 2FA token to withdraw. Add mustOwnTokens to the unbacking approval:
```json
{
"approvalCriteria": {
"mustPrioritize": true,
"allowBackedMinting": true,
"overridesFromOutgoingApprovals": false,
"mustOwnTokens": [{
"collectionId": "74",
"amountRange": { "start": "1", "end": "18446744073709551615" },
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"overrideWithCurrentTime": true,
"mustSatisfyForAllAssets": false,
"ownershipCheckParty": "initiator"
}]
}
}
```
- collectionId: the 2FA collection ID
- overrideWithCurrentTime: true ensures the check uses the current time (important for expiring 2FA tokens)
- ownershipCheckParty: "initiator" checks the person initiating the withdrawal
### Metadata Guidance
- Collection, token, alias path, and denomUnit metadata proto fields are all shaped as `{ uri, customData }`. Use placeholder URIs (`ipfs://METADATA_COLLECTION`, `ipfs://METADATA_TOKEN_`, `ipfs://METADATA_ALIAS_`, etc.) and register the real names, descriptions, and images in the `metadataPlaceholders` sidecar keyed by those URIs. The auto-apply flow uploads the off-chain JSON and substitutes the placeholder URIs with real uploaded URIs after deploy.
- Do not use lazy placeholder names like "Backing Approval", write real user-facing descriptions inside the metadataPlaceholders entries explaining what each approval / collection / token does.
### Key Rules Summary
1. **No fromListId: "Mint" approvals**: Tokens are created via IBC backing, not traditional minting
2. **Use allowBackedMinting: true** in both backing and unbacking approvals
3. **Use mustPrioritize: true** (required for IBC backed operations)
4. **overridesFromOutgoingApprovals**: leave unset or false on both backing and unbacking approvals. The backing address is protocol-controlled but we still manage its approvals at the protocol level, treat it like any regular user whose outgoing approvals are externally managed. Don't set `overridesFromOutgoingApprovals: true` as a "best practice", adding overrides where they aren't needed is actively worse than omitting them.
5. **Unbacking fromListId**: Use `!Mint:backingAddress` syntax, excludes both Mint and backing address so only regular holders can send tokens back
6. **Must create backing + unbacking approvals**. Transferable approval is common but optional (omit for vaults/escrows)
7. **Must configure alias path** with matching decimals
8. The backing address is deterministic, use the one from generate_backing_address tool
## Common Mistakes
- Don't use fromListId: "Mint", smart tokens are created via IBC backing, not traditional minting. Use the backing address as fromListId instead.
- Don't forget mustPrioritize: true on backing and unbacking approvals, without it, the chain cannot match the approval and the transfer fails.
- Don't assume all three approvals are required, the transferable approval is optional. Omit it for vaults or escrows where tokens should not move between users.
- Don't use "All" as fromListId or toListId for backing/unbacking operations, use the exact deterministic backing address from generate_backing_address.
- Don't use number types for amounts or IDs, all values must be strings ("1" not 1, "18446744073709551615" not Number.MAX_SAFE_INTEGER).
- Don't forget to configure an alias path, smart tokens will not function without one, and the alias decimals must match the IBC denom decimals.
## File: agents/skills/subscription.md
---
description: "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: "",
paymentAmount: "",
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" }],
"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.
## File: agents/skills/tradable.md
---
description: "NFT marketplace standard enabling peer-to-peer transfers with the \"NFTMarketplace\" standard tag and NFTPricingDenom"
---
# Tradable NFTs
Instruction text for agents that use the Tradable NFTs skill, loaded by `bb dev skills tradable` 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 tradable skill and build me one: nFT marketplace standard enabling peer-to-peer transfers with the "NFTMarketplace" standard tag and NFTPricingDenom. 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 tradable` prints the same instructions.
## Summary
Required standards: ["NFTMarketplace", "NFTs", "NFTPricingDenom:ubadge"]
- Must include all three standards together
- NFTPricingDenom format: "NFTPricingDenom:" (sets pricing denomination for orderbook)
- Must include a free transfer approval: fromListId: "!Mint", toListId: "All", initiatedByListId: "All", approvalId: "transferable-approval"
- Enables orderbook/marketplace integration
- Typically used with NFT collections
- Note: Legacy names "Tradable" and "DefaultDisplayCurrency" are still accepted for existing collections
## Instructions
## Tradable NFTs Configuration
When enabling trading for NFTs, follow these requirements:
### Preferred path: preset (one short tool call)
The free-transfer approval is fully canonical (takes no params):
```ts
add_preset_approval({ presetId: "tradable.transferable", params: {} })
```
### Required Structure
1. **Standards**: must include "NFTMarketplace", "NFTs", and "NFTPricingDenom:ubadge"
- "standards": ["NFTMarketplace", "NFTs", "NFTPricingDenom:ubadge"]
- NFTPricingDenom sets the pricing denomination for orderbook display
- Replace "ubadge" with your desired currency denom if different
2. **Free Transfer Approval**: Include a default collection approval for peer-to-peer transfers
```json
{
"fromListId": "!Mint",
"toListId": "All",
"initiatedByListId": "All",
"approvalId": "transferable-approval",
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
```
### Complete Example
```json
{
"updateStandards": true,
"standards": ["NFTMarketplace", "NFTs", "NFTPricingDenom:ubadge"]
}
```
### Tradable Gotchas
- Must include all three standards together
- NFTPricingDenom format: "NFTPricingDenom:denom"
- This enables orderbook/marketplace integration
- Typically used with NFT collections
- The currency denom determines how prices are displayed in marketplaces
- Legacy names "Tradable" and "DefaultDisplayCurrency" still work for existing collections
## File: agents/spending-authorization.md
---
description: "Give an AI agent a delegate wallet with chain-enforced spending limits: daily caps, time windows, recipient and token allowlists, one-transaction revocation."
---
# Spending Authorization
An outgoing approval on the controller's account lets a delegate agent wallet move a bounded amount of the controller's tokens. The chain enforces the bound; no server or dashboard setting is involved.
In this example alice (`bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d`) is the controller, the agent key is `bb18cad7xxsk3drvwdxeasc3wqn2plftpzq2tsrsr`, and bob (`bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue`) is the only allowed recipient. The window runs from `1788739200000` (2026-09-06) for 90 days. The full `MsgSetOutgoingApproval`, with every `approvalCriteria` field spelled out, is:
```json fold=20-41,58-75
{
"messages": [
{
"typeUrl": "/tokenization.MsgSetOutgoingApproval",
"value": {
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "2",
"approval": {
"approvalId": "agent-daily-budget",
"toListId": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"initiatedByListId": "bb18cad7xxsk3drvwdxeasc3wqn2plftpzq2tsrsr",
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"transferTimes": [{ "start": "1788739200000", "end": "1796515200000" }],
"uri": "",
"customData": "",
"version": "0",
"approvalCriteria": {
"merkleChallenges": [],
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [],
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false,
"allowAmountScaling": false,
"maxScalingMultiplier": "0"
},
"orderCalculationMethod": {
"useOverallNumTransfers": false,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
}
},
"approvalAmounts": {
"overallApprovalAmount": "100",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "agent-daily-budget",
"resetTimeIntervals": { "startTime": "1788739200000", "intervalLength": "86400000" }
},
"maxNumTransfers": {
"overallMaxNumTransfers": "20",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"perInitiatedByAddressMaxNumTransfers": "0",
"amountTrackerId": "agent-daily-budget",
"resetTimeIntervals": { "startTime": "1788739200000", "intervalLength": "86400000" }
},
"coinTransfers": [],
"requireToEqualsInitiatedBy": false,
"requireToDoesNotEqualInitiatedBy": false,
"autoDeletionOptions": {
"afterOneUse": false,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
},
"mustOwnTokens": [],
"dynamicStoreChallenges": [],
"ethSignatureChallenges": [],
"recipientChecks": { "mustBeEvmContract": false, "mustNotBeEvmContract": false, "mustBeLiquidityPool": false, "mustNotBeLiquidityPool": false },
"initiatorChecks": { "mustBeEvmContract": false, "mustNotBeEvmContract": false, "mustBeLiquidityPool": false, "mustNotBeLiquidityPool": false },
"altTimeChecks": { "offlineHours": [], "offlineDays": [] },
"mustPrioritize": false,
"votingChallenges": [],
"evmQueryChallenges": []
}
}
}
}
]
}
```
```bash
# Controller signs. The agent key never touches this step.
bb check ./approval.json --depth structural
bb tx tokenization set-outgoing-approval 2 "$(jq -c .messages[0].value.approval ./approval.json)" \
--from alice --chain-id bitbadges-1 \
--node https://rpc.bitbadges.io:443 --gas auto --gas-prices 10ubadge
```
This approval, on collection `2`, says: the agent at `bb18cad7xxsk3drvwdxeasc3wqn2plftpzq2tsrsr` may move token ID `1` from the controller to `bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue`, at most 100 units and 20 transfers per 24-hour window starting at `startTime`, and only between the two `transferTimes` timestamps. Transfers outside these conditions cannot use this approval. Other collection or account approvals may authorize other transfers; inspect the complete policy before treating this as the agent's only authority.
## Why App-Layer Limits Are Not Enough
A limit enforced only by an application server depends on that server correctly applying the policy on every request. A policy edit or compromised server can change that behavior. An on-chain outgoing approval instead exposes the policy and its tracker state for independent inspection. When comparing wallet providers, check where their particular policy is enforced; implementations differ.
Application checks can complement on-chain restrictions. Choose the enforcement boundary based on which keys and services you trust.
## How It Works
The delegation lives in the controller's outgoing approvals for this collection on-chain. The collection's own approvals must also permit the transfer. The controller keeps the main wallet; the agent uses a separate keypair. Inspect all approvals and any collection-level overrides before concluding that this outgoing approval is the agent's only authority.
| Building block | Field | What it enforces |
| --- | --- | --- |
| Delegate wallet | `initiatedByListId` | Only the agent address can initiate transfers under this approval |
| Recipient allowlist | `toListId` | One address, or an address list ID for several. See [Address Lists](../token-standard/concepts/address-lists.md) |
| Token allowlist | `tokenIds` | Which token IDs the agent can move |
| Valid time window | `transferTimes` | Unix millisecond range in which transfers are allowed. Outside it the chain refuses the transaction |
| Daily cap | `approvalAmounts.overallApprovalAmount` + `resetTimeIntervals` | Cumulative amount per interval. `intervalLength: "86400000"` is one day; use `3600000` for hourly or `2592000000` for 30 days. The tally resets to zero on the first transfer of each new interval |
| Transfer count cap | `maxNumTransfers.overallMaxNumTransfers` + `resetTimeIntervals` | Number of transfers per interval, same reset rule |
| Revocation | `MsgDeleteOutgoingApproval` | Removes the approval. After successful inclusion, this approval no longer grants spending authority |
The tally mechanics (tracker IDs, `overall` versus per-address trackers, reset timing) are on [Approval Trackers](../token-standard/approval-criteria/approval-trackers.md). The full approval interface is on [Approval Criteria](../token-standard/approval-criteria/README.md).
The collection-level approval must also allow the transfer. A standard "transferable" collection approval with `overridesFromOutgoingApprovals: false` defers to the sender's outgoing approvals, which is what makes this pattern work. A collection approval that overrides outgoing approvals bypasses the agent's limits.
## Agent Side: Spend Within the Envelope
The agent signs a `MsgTransferTokens` with itself as `creator` and the controller as `from`, and points at the outgoing approval:
```json
{
"messages": [
{
"typeUrl": "/tokenization.MsgTransferTokens",
"value": {
"creator": "bb18cad7xxsk3drvwdxeasc3wqn2plftpzq2tsrsr",
"collectionId": "2",
"transfers": [
{
"from": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"toAddresses": ["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"],
"balances": [
{
"amount": "5",
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
],
"memo": "",
"merkleProofs": [],
"ethSignatureProofs": [],
"prioritizedApprovals": [
{
"approvalId": "agent-daily-budget",
"approvalLevel": "outgoing",
"approverAddress": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"version": "0"
}
],
"onlyCheckPrioritizedCollectionApprovals": false,
"onlyCheckPrioritizedIncomingApprovals": false,
"onlyCheckPrioritizedOutgoingApprovals": true
}
]
}
}
]
}
```
```bash
bb check ./transfer.json --depth structural
bb tx tokenization transfer-tokens ./transfer.json \
--from agent-wallet --chain-id bitbadges-1 \
--node https://rpc.bitbadges.io:443 --gas auto --gas-prices 10ubadge
```
`bb check --depth structural` on either file prints:
```json
{
"ok": true,
"data": {
"valid": true,
"issues": []
},
"warnings": [],
"error": null
}
```
`version` must match the approval's current version; the chain increments it on every edit, which also invalidates stale agent configs after the controller changes the rules. The 101st unit in a day, the 21st transfer, a transfer to another recipient, or a transfer after the window ends fail when the chain executes the transfer. Structural validation alone does not evaluate live balances and approval trackers.
## Revoke
```bash
bb tx tokenization delete-outgoing-approval 2 agent-daily-budget \
--from alice --chain-id bitbadges-1 \
--node https://rpc.bitbadges.io:443 --gas auto --gas-prices 10ubadge
```
The same message as JSON, for the SDK or the review-and-sign flow:
```json
{
"messages": [
{
"typeUrl": "/tokenization.MsgDeleteOutgoingApproval",
"value": {
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "2",
"approvalId": "agent-daily-budget"
}
}
]
}
```
Revocation takes effect when the transaction succeeds on-chain. Transfers ordered before it can still execute, and any other approvals granted to the agent remain in force. To tighten instead of revoke, send `MsgSetOutgoingApproval` again with the same `approvalId` and new limits; the version increments and the agent's next transfer must reference it.
Keep `userPermissions.canUpdateOutgoingApprovals` neutral (`[]`) on the controller account so revocation stays possible. A controller that freezes that permission cannot revoke.
## Audit Trail
Every transfer the agent makes is a chain transaction with a timestamp, the approval ID it used, the amounts, and the recipients. The running tally is queryable:
```bash
# args: collectionId approvalLevel approverAddress approvalId amountTrackerId trackerType approvedAddress
bb query tokenization approvals-trackers 2 outgoing bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d \
agent-daily-budget agent-daily-budget overall "" \
--node https://rpc.bitbadges.io:443 --output json
```
After two transfers of 5 units in the current window the response reads (synthesized; the fixture approval is not on mainnet):
```json
{
"tracker": {
"numTransfers": "2",
"amounts": [
{
"amount": "10",
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
],
"lastUpdatedAt": "1788750000000"
}
}
```
An auditor, regulator, or security team can verify the whole spending history without asking any platform for data. Query reference: [Get approval tracker](../token-standard/queries/get-approval-tracker.md).
## Native Coins and USDC
Outgoing approvals govern collection-token transfers, not arbitrary native bank transfers. Holding USDC as a backed Smart Token can put conditions on moving or redeeming its receipt tokens, subject to the collection's approvals. A recipient allowlist on those receipts does **not** constrain the recipient of a later `MsgSend` after redemption. The redeemed coins are in the withdrawing wallet and outside that approval's control.
An atomic redemption plus bank payment avoids a successful withdrawal followed by a failed payment, but atomicity alone does not enforce a merchant allowlist on the bank payment. A compromised agent with authority to redeem can choose another transaction. Do not advertise receipt-token limits as a general USDC spending policy.
Use [Main-Wallet Payment Requests](main-wallet-payments.md) when the human must approve each exact USDC recipient and amount. Use an existing [Smart Token or Vault](../guides/smart-tokens-and-vaults.md) when the requirement is explicitly a redemption policy. [Choose Agent Spending Authority](spending-models.md) compares these with a separately funded agent wallet.
## Related
- [MsgSetOutgoingApproval](../token-standard/messages/msg-set-outgoing-approval.md)
- [MsgDeleteOutgoingApproval](../token-standard/messages/msg-delete-outgoing-approval.md)
- [MsgTransferTokens](../token-standard/messages/msg-transfer-tokens.md)
- [Approval Trackers](../token-standard/approval-criteria/approval-trackers.md)
## File: agents/spending-models.md
---
description: "Choose human-approved payments, a separately funded agent wallet, or existing token approvals by the authority an agent actually needs."
---
# Choose Agent Spending Authority
Start with the action a creator wants to authorize. An agent that prepares a payment does not need custody. An agent that must pay while the creator is away needs explicit spending authority and a clearly bounded exposure.
## Choose the Smallest Authority
| Creator's need | Existing flow | Enforcement and limit |
| --- | --- | --- |
| Approve each purchase from my wallet | [Main-Wallet Payment Requests](main-wallet-payments.md) | Human reviews and signs each exact transaction; no standing allowance is needed |
| Let an agent operate unattended with a small amount | Separate agent-owned wallet, funded explicitly | Key can spend the wallet's balance and exercise any other authority granted to it; funding is an exposure decision |
| Delegate specific collection-token transfers | [Outgoing spending approval](spending-authorization.md) | Chain checks the permitted token, recipient, initiator, time and tracker; inspect alternative approval paths |
| Limit how quickly backing coins leave a vault | Existing [Smart Token / Vault](../guides/smart-tokens-and-vaults.md) | Collection redemption rules constrain withdrawal; redeemed coins are outside those rules |
| Pay a known provider repeatedly | [Subscriptions](../standards/subscriptions.md) | Explicit recurring terms and consent, with separate cancellation and already-purchased access |
The first flow is the default. The agent resolves the invoice or recipient, prepares unsigned messages, and opens the existing browser review. The creator sees the network, asset, amount, recipient and fees before signing. This is already an end-to-end workflow; it does not require an Agent Vault collection.
## Unattended Wallets
A separate funded wallet is often enough for a small autonomous experiment. Budget for fees, keep the main wallet key out of the agent's environment, and give the agent only the funds and roles needed for its task. Do not grant it a collection manager role or unrelated approvals merely because it is called an “agent wallet.”
A 10 USDC balance is not a 10 USDC lifetime policy: future deposits, refunds, incoming transfers and other delegated permissions can increase its authority. Automatic top-ups can make total exposure much larger. Reconcile confirmed payments before replenishing, and decide explicitly whether the agent may retain refunds or receive more funds.
Skills, prompts and shell hooks help an agent follow the intended process. They are not a security boundary against a compromised process that can read the signing key. A separate signer that independently validates a policy is a different architecture; it must control key access, validate every signing path and define recovery. Do not promise those guarantees from an agent prompt.
## Existing Vaults Have a Specific Job
Use a vault when on-chain redemption limits are themselves valuable, such as controlling the release cadence of a larger operating reserve. Inspect the exact approval configuration: tracker scope, reset boundary, permitted initiators, alternative withdrawal paths, account approvals and recovery authority. A fixed daily interval can allow one budget immediately before reset and another immediately after it; it is not a rolling 24-hour cap.
A withdrawal cap is not a purchase cap. The agent may accumulate withdrawn coins over several periods, then spend them together. Neither a receipt-transfer recipient restriction nor a multisig activation vote binds a later native bank payment to a merchant. Once coins are withdrawn, a vault cannot claw them back.
Recovery is not automatically a pause, refund or revocation. Read the actual recovery approvals: moving receipts to a recovery address is different from redeeming them, and may still require withdrawal conditions. Revocation or recovery becomes effective when included on-chain; a competing spend ordered first can succeed. Do not assume the manager can change an immutable vault after creation.
## Payment Completion and Service Delivery
For every model, store the business operation reference and transaction hash, confirm execution on the intended network, and reconcile indexed invoice state before retrying. A browser timeout or missing callback is an unknown outcome, not evidence that no payment occurred. See [request recovery](main-wallet-payments.md#recover-a-request).
A payment proves a transfer, not service delivery. A merchant refund is a separate payment unless an explicit refund mechanism was agreed. Agents and providers must agree how to identify the original payment and who receives the refund. Replenishing a vault does not erase consumed withdrawal trackers or establish a safe retry.
## Why There Is No Additional Agent Vault Standard Here
A new label would not improve the human-reviewed payment flow. The proposed combination of designated agent, withdrawal cap, activation votes and recovery uses existing approval primitives and creates additional custody and maintenance obligations. It also does not enforce where withdrawn native coins are spent.
Use the existing flows above. A future design should begin with a concrete unmet guarantee—such as independently enforced native-coin recipient and lifetime limits—and demonstrate that every signing and withdrawal path preserves it. A more elaborate vault UI or an “agent” label is not such a guarantee.
## File: api/README.md
---
description: "The BitBadges API in one page. Base URL, API keys, credits and pricing, rate limits, number types, errors, and the refresh queue."
---
# BitBadges API
The BitBadges API is the hosted REST service at `https://api.bitbadges.io` that indexes the chain and adds off-chain features (claims, sign in, metadata, search). Use it from any backend with an API key.
This page is the introduction to the API tab: keys, credits, limits, number types, errors, and the refresh queue. The other pages on this tab go deeper on one topic each, and the [API reference](/api-reference) is the interactive list of every route.
## Which API do I want?
There are two, and they answer different questions.
| | BitBadges API (this tab) | Chain API |
| --- | --- | --- |
| What it is | The hosted indexer at `https://api.bitbadges.io` | The chain's own LCD gateway at `https://lcd.bitbadges.io` |
| Answers | What the chain means, plus everything off-chain | What one node holds in state right now |
| Gives you | Collections with metadata resolved, search, activity and history, balances by address, claims and plugins, Sign In with BitBadges, address lists, the refresh queue | Module queries straight from state: `x/tokenization`, `x/gamm`, `x/poolmanager`, `x/sendmanager`, plus the standard Cosmos SDK and IBC endpoints |
| Needs a key | Yes | No |
| Reference | [API reference](/api-reference) | [Chain API reference](/chain-api-reference) |
Use this API for almost everything. It reads the chain for you, joins in the off-chain pieces the chain never sees, and returns one object per question. Use the Chain API when you need raw module state with no indexer in the path, when you are running against your own node, or when you are writing a chain-side integration. See [Chain](../chain/README.md) for nodes, modules, and the LCD.
## API Keys
1. Sign in at [https://bitbadges.io/developer](https://bitbadges.io/developer) and open the **API Keys** tab.
2. Create a key. Send it in the `x-api-key` header on every request.
3. Top up credits in the same tab.
Select read-only routes are public without a key and are rate limited per IP. Everything else requires a key. A route that needs a key answers `401` with `{ "errorMessage": "Unauthorized request. This route is only accessible with an API key. To get an API key, visit https://bitbadges.io/developer and go to API Keys." }` when the key is missing.
The SDK also reads `BITBADGES_API_KEY` from the environment when `apiKey` is not passed. The CLI reads the same variable (see [CLI api](../cli/api.md)).
## What Is on This Tab
| Page | Read it when |
| --- | --- |
| [Pagination and Views](pagination-and-views.md) | A response returns `bookmark` and `hasMore`, or you fetch a `views` object. |
| [Swaps](swaps.md) | You want a swap estimate and the messages to execute it. |
| [Claims](claims/README.md) | You gate a mint or an app on off-chain criteria. Concepts, then [endpoints](claims/endpoints.md), [Plugins](claims/plugins.md), and [Dynamic Stores](claims/dynamic-stores.md). |
| [Sign In with BitBadges](sign-in/README.md) | You want users to prove address ownership or grant your app API scopes. |
| [API Reference](/api-reference) | You need the exact method, path, body, and response of a route. |
## Example
```bash
curl -X POST https://api.bitbadges.io/api/v0/collections \
-H "Content-Type: application/json" \
-H "x-api-key: $BITBADGES_API_KEY" \
-d '{ "collectionsToFetch": [ { "collectionId": "1" } ] }'
```
```ts
import { BigIntify, BitBadgesAPI } from 'bitbadges';
const BitBadgesApi = new BitBadgesAPI({
apiKey: process.env.BITBADGES_API_KEY,
convertFunction: BigIntify, // or Numberify, Stringify
apiUrl: 'https://api.bitbadges.io' // default when omitted
});
const res = await BitBadgesApi.getCollections({ collectionsToFetch: [{ collectionId: '1' }] });
const collection = res.collections[0];
if (!collection) throw new Error('Collection not found');
console.log(collection.manager); // bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d
```
```bash
bb api tokens get-collection 1
```
The response for the Demo NFTs collection (synthesized from the SDK types; numbers arrive as strings):
```json fold=10-17,33-56,66-117,119-167,169-180
{
"collections": [
{
"_docId": "1",
"collectionId": "1",
"createdBy": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"manager": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"createdBlock": "4182003",
"createdTimestamp": "1788652800000",
"updateHistory": [
{
"txHash": "E5B4C3A6E5B1F3B9F0F4C1F2B7A6D5C4E3F2A1B0C9D8E7F6A5B4C3D2E1F0A9B8",
"block": "4182003",
"blockTimestamp": "1788652800000",
"timestamp": "1788652800000"
}
],
"collectionMetadata": {
"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"customData": ""
},
"tokenMetadata": [
{
"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/{id}.json",
"tokenIds": [{ "start": "1", "end": "100" }],
"customData": ""
}
],
"customData": "",
"standards": ["NFTs"],
"isArchived": false,
"validTokenIds": [{ "start": "1", "end": "100" }],
"mintEscrowAddress": "bb1v9jxgu33kfsgr5mkaa4z0ry6s2acpah4yh6yqfdnzkx3l8vmqzmpcssqaqaen9t",
"cosmosCoinWrapperPaths": [],
"aliasPaths": [],
"invariants": {
"noCustomOwnershipTimes": true,
"maxSupplyPerId": "1",
"noForcefulPostMintTransfers": true,
"disablePoolCreation": false
},
"defaultBalances": {
"balances": [],
"incomingApprovals": [],
"outgoingApprovals": [],
"userPermissions": {
"canUpdateOutgoingApprovals": [],
"canUpdateIncomingApprovals": [],
"canUpdateAutoApproveSelfInitiatedOutgoingTransfers": [],
"canUpdateAutoApproveSelfInitiatedIncomingTransfers": [],
"canUpdateAutoApproveAllIncomingTransfers": []
},
"autoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedIncomingTransfers": true,
"autoApproveAllIncomingTransfers": false
},
"collectionApprovals": [
{
"approvalId": "mint-to-alice",
"fromListId": "Mint",
"toListId": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"initiatedByListId": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "100" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "",
"customData": "",
"fromList": { "listId": "Mint", "addresses": ["Mint"], "whitelist": true, "uri": "", "customData": "" },
"toList": {
"listId": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"addresses": ["bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"],
"whitelist": true,
"uri": "",
"customData": ""
},
"initiatedByList": {
"listId": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"addresses": ["bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"],
"whitelist": true,
"uri": "",
"customData": ""
},
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": false,
"requireToEqualsInitiatedBy": false,
"requireFromEqualsInitiatedBy": false,
"requireToDoesNotEqualInitiatedBy": false,
"requireFromDoesNotEqualInitiatedBy": false,
"coinTransfers": [],
"merkleChallenges": [],
"mustOwnTokens": [],
"dynamicStoreChallenges": [],
"ethSignatureChallenges": [],
"maxNumTransfers": {
"overallMaxNumTransfers": "0",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"perInitiatedByAddressMaxNumTransfers": "0",
"amountTrackerId": "mint-to-alice",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"approvalAmounts": {
"overallApprovalAmount": "0",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "mint-to-alice",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"autoDeletionOptions": {
"afterOneUse": false,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
}
}
},
{
"approvalId": "transferable",
"fromListId": "!Mint",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "100" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "",
"customData": "",
"fromList": { "listId": "!Mint", "addresses": ["Mint"], "whitelist": false, "uri": "", "customData": "" },
"toList": { "listId": "All", "addresses": [], "whitelist": false, "uri": "", "customData": "" },
"initiatedByList": { "listId": "All", "addresses": [], "whitelist": false, "uri": "", "customData": "" },
"approvalCriteria": {
"overridesFromOutgoingApprovals": false,
"overridesToIncomingApprovals": false,
"requireToEqualsInitiatedBy": false,
"requireFromEqualsInitiatedBy": false,
"requireToDoesNotEqualInitiatedBy": false,
"requireFromDoesNotEqualInitiatedBy": false,
"coinTransfers": [],
"merkleChallenges": [],
"mustOwnTokens": [],
"dynamicStoreChallenges": [],
"ethSignatureChallenges": [],
"maxNumTransfers": {
"overallMaxNumTransfers": "0",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"perInitiatedByAddressMaxNumTransfers": "0",
"amountTrackerId": "transferable",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"approvalAmounts": {
"overallApprovalAmount": "0",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "transferable",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"autoDeletionOptions": {
"afterOneUse": false,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
}
}
}
],
"collectionPermissions": {
"canDeleteCollection": [],
"canArchiveCollection": [],
"canUpdateStandards": [],
"canUpdateCustomData": [],
"canUpdateManager": [],
"canUpdateCollectionMetadata": [],
"canUpdateValidTokenIds": [],
"canUpdateTokenMetadata": [],
"canUpdateCollectionApprovals": [],
"canAddMoreAliasPaths": []
},
"activity": [],
"owners": [],
"challengeTrackers": [],
"approvalTrackers": [],
"listings": [],
"claims": [],
"views": {}
}
]
}
```
## Credits and Pricing
API credits (on-chain symbol `APITOKEN`) meter API calls. Every request debits one credit from the account that owns the key.
| Item | Value |
| --- | --- |
| API request | 1 credit per request, every route |
| Exchange rate | 1 USDC = 100,000 credits, so 100,000 requests cost about $1 |
| Tiers, subscriptions, card on file | None |
| Refunds | None. Credits are non-refundable. |
| Transfers | None. Credits are soulbound to the account that buys them. |
| Expiry | None |
To top up: open [https://bitbadges.io/developer](https://bitbadges.io/developer), **API Keys** tab, enter a USDC amount, confirm the on-chain transaction. The balance updates once the transfer confirms. The same card shows the current balance and a low-balance warning.
Credits pay for API requests. The site no longer runs an AI builder of its own, so nothing here is charged per model call. Build tokens with your own AI instead: see [Agents setup](../agents/setup.md).
### Balance
```bash
curl https://api.bitbadges.io/api/v0/credits/balance \
-H "Authorization: Bearer $BITBADGES_ACCESS_TOKEN"
```
```json
{
"onChainTotal": 100000,
"used": 423,
"remaining": 99577,
"decimals": 6
}
```
The balance route needs a signed-in session with the `Full Access` scope. It is served with website-only CORS, so call it from a server, not a browser on another origin. `onChainTotal`, `used`, and `remaining` are display `APITOKEN` (1 credit = 1 request). `decimals` is the on-chain base-unit scale (`base = display * 10^decimals`). The off-chain `used` counter is only exposed to the account owner. The `402` response below is the simplest way to read it without a session.
### Out of Credits (402)
When the balance is zero, every request answers `402 Payment Required`:
```json
{
"error": "Insufficient credits",
"errorMessage": "Insufficient API credits. Top up at /developer?tab=apiKeys.",
"topUpUrl": "/developer?tab=apiKeys",
"onChainTotal": 100,
"used": 100,
"remaining": 0,
"decimals": 6
}
```
The key stays valid. Catch the `402`, prompt the account owner to top up, and retry once the balance confirms on-chain.
## Rate Limits and Size Limits
| Limit | Value |
| --- | --- |
| Requests per account (all keys combined) | 10,000 per minute. Answers `429` with `{ "errorMessage": "Exceeded rate limit. Too many requests." }` |
| Requests without a key (public routes) | 10 per 10 seconds per IP |
| Metadata URIs per request | 250 |
| Account lookups per request | 250 |
| Collection fetches per request | 250 |
| IPFS uploads | 100 MB total per address |
| Collection size | Limited functionality above JavaScript `Number.MAX_SAFE_INTEGER` |
| External fetch timeout (metadata URIs, plugin endpoints, hooks) | 10 seconds |
| Failed fetch retry | Exponential backoff: `delay = 1 hour * 2^attempts` |
| Manual metadata refresh | Once per 5 minutes per collection |
The per-account limit exists to stop runaway loops. Contact BitBadges if you need a higher ceiling. Limits can change.
## Number Types
Responses stringify numbers to avoid precision loss. Convert them yourself (bigint is the safe choice) or let the SDK do it through `convertFunction`. See [SDK types](../sdk/types.md).
## Route Naming
This documentation often shows the SDK call. The raw HTTP route is the same name under `/api/v0`:
```ts
const res = await BitBadgesApi.getClaim('claim_demo_01');
```
```bash
curl https://api.bitbadges.io/api/v0/claim/claim_demo_01 -H "x-api-key: $BITBADGES_API_KEY"
```
Use the [API reference](/api-reference) for the exact method, path, and body of each route.
## Errors
Errors return a JSON body with `errorMessage`:
```json
{ "errorMessage": "Collection not found" }
```
Common codes: `400` invalid payload, `401` missing or invalid key or session, `402` no credits, `404` not found, `429` rate limited, `500` server error. The SDK throws on any non-2xx response.
## Authorization and Scopes
Most apps only read public data and need no user authorization. To act on behalf of a user (complete claims, read private claim data, manage claims), use [Sign In with BitBadges](sign-in/README.md). It is a standard OAuth 2.0 flow. Request scopes in the authorization URL and send the access token as `Authorization: Bearer `. The [API reference](/api-reference) lists the scope each route needs.
## Refresh Queue
The API fetches anything behind a source URI (metadata, off-chain balances) through a load-balanced queue, then caches the result until the next refresh. New metadata can take a moment to populate.
Refreshes trigger automatically on-chain events such as collection creation or a URI change. You can also trigger one manually, subject to the cooldown above:
```ts
await BitBadgesApi.refreshMetadata('1');
const status = await BitBadgesApi.getRefreshStatus('1');
console.log(status.inQueue); // true until the queue drains
```
```bash
curl -X POST https://api.bitbadges.io/api/v0/collection/1/refresh -H "x-api-key: $BITBADGES_API_KEY"
curl https://api.bitbadges.io/api/v0/collection/1/refreshStatus -H "x-api-key: $BITBADGES_API_KEY"
```
The status route returns whether the collection is still queued, up to 20 queue documents that failed, and the last refresh request:
```json
{
"inQueue": true,
"errorDocs": [
{
"_docId": "1-ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/7.json",
"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/7.json",
"collectionId": "1",
"loadBalanceId": "3",
"refreshRequestTime": "1788739200000",
"numRetries": "2",
"lastFetchedAt": "1788746400000",
"nextFetchTime": "1788753600000",
"error": "Request timed out after 10000 ms"
}
],
"refreshDoc": {
"_docId": "1",
"collectionId": "1",
"refreshRequestTime": "1788739200000"
}
}
```
Failed fetches retry with the backoff in the limits table. On the site, a collection page under **Actions** then **Refresh** shows the same status and any error documents.
## Testnet
A testnet API exists at `https://api.bitbadges.io/testnet` with the same routes under the `/testnet` prefix. It is a separate service: keys, credits, and data do not carry over. Testnet is offline at the time of writing. See [Testnet](../chain/testnet.md) for status.
## Related
- [API reference](/api-reference)
- [SDK](../sdk/README.md)
- [CLI api](../cli/api.md)
- [Sign In with BitBadges](sign-in/README.md)
## File: api/claims/README.md
---
description: "How claims work. Plugins, indexed versus on-demand, claim numbers, success logic, sign-in modes, gating an on-chain approval, and the trust model."
---
# Claims
A claim is an off-chain criteria check hosted by BitBadges: meet the criteria, receive the reward. Claims run airdrops, whitelist mints, code redemptions, attendance rewards, and any flow where a user must qualify before receiving tokens or access. No contract or frontend code is required.
See the [API reference](/api-reference) for every route's request and response schema.
## Example
A claim with three plugin instances: `numUses` caps the claim at 100 successes, and either of two whitelists is enough to pass.
```ts
import { BigIntify, BitBadgesAPI } from 'bitbadges';
const BitBadgesApi = new BitBadgesAPI({ apiKey: process.env.BITBADGES_API_KEY, convertFunction: BigIntify }); // key from https://bitbadges.io/developer
// Create it: POST /api/v0/claims (needs a session with the Manage Claims scope)
await BitBadgesApi.createClaims({
claims: [
{
claimId: 'claim_demo_01',
collectionId: '1',
plugins: [
{
pluginId: 'numUses',
instanceId: 'num-uses',
version: '0',
publicParams: { maxUses: 100, hideCurrentState: false, displayAsUnlimited: false },
privateParams: {}
},
{
pluginId: 'whitelist',
instanceId: 'vip-whitelist',
version: '0',
publicParams: { maxUsesPerAddress: 1, hasPrivateList: true },
privateParams: { listId: 'vip-list' }
},
{
pluginId: 'whitelist',
instanceId: 'early-access',
version: '0',
publicParams: { maxUsesPerAddress: 1, hasPrivateList: true },
privateParams: { listId: 'early-access-list' }
}
],
satisfyMethod: {
type: 'AND',
conditions: ['num-uses', { type: 'OR', conditions: ['vip-whitelist', 'early-access'] }]
},
rewards: [],
metadata: {
name: 'Early access mint',
description: 'VIP and early-access holders can mint one Demo NFT.',
image: 'ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/claim.png'
},
showInSearchResults: true,
categories: ['nft'],
estimatedCost: 'Free',
estimatedTime: '1 minute',
approach: 'in-site'
}
]
});
// Complete it for bob: plugin inputs are keyed by instanceId (these plugins take none)
const res = await BitBadgesApi.completeClaim('claim_demo_01', 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue', {
_expectedVersion: 0
});
console.log(res.claimAttemptId); // 3b9d2f7a1c4e6b8d0f2a4c6e8b1d3f5a
```
{% hint style="info" %}
**Ask your agent.** With the MCP builder tools connected, a prompt like this works:
```text
Create a claim for collection 1 that requires a Discord role and a password, 100 uses total, one per address.
```
The agent calls `search_plugins` to look up the `discord` and `password` parameter schemas, then `build_claim` to produce the claim document. Review it, then create it through the API or the site.
{% endhint %}
## What Is in This Section
| Page | Read it when |
| --- | --- |
| [Endpoints](endpoints.md) | You complete, simulate, verify, fetch, create, or delete claims through the API, or build the merkle proof for an on-chain claim. |
| [Plugins](plugins.md) | You need a plugin's parameters or the request contract for a custom plugin endpoint. |
| [Dynamic Stores](dynamic-stores.md) | You want a BitBadges-hosted address list you update from your own systems. |
Guides: [Distribute with Claims](../../guides/distribute-with-claims.md) walks through designing and shipping a claim. [Build a Claim Plugin](../../guides/build-a-claim-plugin.md) walks through writing an endpoint.
## How a Claim Works
1. A claim is created with a set of plugin instances (core, BitBadges-hosted, or custom).
2. A user attempts the claim on the site, through the API, or programmatically.
3. All plugins run in parallel. Each receives the user's address and its parameters and answers pass or fail.
4. If the success logic is satisfied (by default every plugin passes), the claim succeeds.
5. Rewards follow: a token mint, gated content, points, an entry in a list, or anything your app decides.
A claim checks criteria. You decide what success means and how you verify it later (a direct lookup by address, an NFT the user now holds, and so on).
### Plugin ID Versus Instance ID
A plugin id names the plugin (`codes`, `whitelist`, `must-own-badges`). An instance id names one use of that plugin inside a claim. One claim can hold several instances of the same plugin with different configuration when the plugin's version config sets `duplicatesAllowed`. Instance ids appear in `satisfyMethod.conditions`, as the keys of the `completeClaim` body, in per-instance plugin state, and in `_specificInstanceIds`.
### Parallel Execution
Plugins never see each other's state changes inside one attempt. Each plugin reads state as it was before the attempt started. Passing plugins return state updates that commit together, and only if the whole claim succeeds. A plugin that needs to coordinate with another must use your own external state. Custom plugin endpoints have 10 seconds to respond.
### Asynchronous Processing
`completeClaim` simulates first. If the simulation fails, the call returns the error and nothing is queued. If it passes, the attempt joins a queue and you get a `claimAttemptId` right away. Attempts resolve in about 1 to 5 seconds. Poll `getClaimAttemptStatus`. Attempts for the same collection process one at a time; different collections process in parallel.
## Indexed Versus On-Demand Claims
| | Indexed (standard) | On-demand (non-indexed) |
| --- | --- | --- |
| `numUses` plugin | present | absent |
| Attempt ledger | every success recorded with a claim number (#0, #1, #2, and so on) | none |
| Usage limits, per-address tracking, claim numbers | yes | no |
| Typical use | distribution with a fixed supply | a live yes/no eligibility check such as token ownership |
| Result caching | n/a | per the claim's cache policy |
Most claims are indexed. A plugin must set `reuseForNonIndexed` to work in an on-demand claim.
### Cache Policy (On-Demand Claims)
```ts
interface iClaimCachePolicy {
ttl?: T; // seconds, default 300
alwaysPermanent?: boolean; // cache forever after the first evaluation
permanentAfter?: UNIXMilliTimestamp; // TTL until this time, then permanent
}
```
| Strategy | Behavior |
| --- | --- |
| No policy | Cache for 5 minutes |
| `ttl: 60` | Cache for 60 seconds |
| `alwaysPermanent: true` | Cache forever after the first evaluation |
| `permanentAfter: 1788825600000` | Cache with TTL until 2026-09-07T00:00:00Z, then permanently |
Use short TTLs for criteria that change (token ownership can transfer). Use permanent caching for one-time checks.
## Claim Numbers
Indexed claims number successes from 0 upward. By default `numUses` assigns the next number. A plugin with the `ClaimNumbers` response preset can assign numbers instead (`codes` assigns the code index, `whitelist` the list index). Only one plugin per claim can assign claim numbers.
## Success Logic
By default every plugin must pass. Override with `satisfyMethod`:
```ts
interface iSatisfyMethod {
type: 'AND' | 'OR' | 'NOT';
conditions: Array; // instance ids or nested logic
options?: { minNumSatisfied?: number }; // M of N for OR
}
```
| Rule | `satisfyMethod` |
| --- | --- |
| All must pass (default) | falsy |
| 2 of 3 social checks must pass | `{ type: 'OR', conditions: ['discord-gate', 'github-gate', 'twitter-gate'], options: { minNumSatisfied: 2 } }` |
| Must not pass | `{ type: 'NOT', conditions: ['banned-list'] }` |
| Nested | `{ type: 'AND', conditions: [{ type: 'OR', conditions: ['vip-whitelist', 'early-access'] }, 'num-uses'] }` |
- `numUses` is always required and cannot be made optional.
- Evaluation short-circuits. If 2 of 8 pass and that is enough, the other 6 are not checked.
- Users can pass `_specificInstanceIds` to choose which instances to attempt.
- State updates only apply to plugins on the success path.
## Signed-In Versus Select Address
The `initiatedBy` plugin decides whether the claiming address is verified.
| Mode | Behavior |
| --- | --- |
| Sign in required | The user authenticates with BitBadges. The address is verified. |
| No sign in | Any address can be entered. No verification. |
Disable sign in for a lighter UX (no wallet signature), for mobile or limited wallets, or for backend auto-completion where your server chooses the address. Most claims should require sign in. For auto-completion, disable sign in and gate with a password only your backend knows, then call `completeClaim` for any address.
## Rewards
A claim can carry rewards that become visible on success: gated text, a URL, or custom data.
```ts
interface iClaimReward {
rewardId: string;
instanceId: string; // unique per reward within the claim
metadata?: iMetadata; // name, description, image
automatic?: boolean; // grant automatically on success
gatedContent: { content?: string; url?: string; params?: object };
calculationMethod?: { alwaysShow?: boolean; minClaimSuccesses?: number };
}
```
Configure rewards in the claim builder or through the `rewards` field of the claim document. Gated content is only visible to users who have succeeded.
## Metadata and Discoverability
| Field | Purpose |
| --- | --- |
| `metadata` | Name, description, image. Shown in the UI and search. |
| `showInSearchResults` | Appear in public search on the site. |
| `categories` | String array for filtering, for example `["nft", "gaming"]`. |
| `estimatedCost`, `estimatedTime` | Display-only strings such as `"$10"` and `"5 minutes"`. Not enforced. |
| `testOnly` | Excluded from public queries and production distribution. |
## Gating an On-Chain Approval
Claims can gate on-chain token operations such as minting. The claim controls the right to initiate a transfer, not the transfer itself.
1. The user completes the claim and receives a merkle leaf (a one-time code). BitBadges signs the leaf for that address.
2. The user submits `MsgTransferTokens` with the merkle proof.
3. The approval's merkle challenge verifies the proof on-chain.
On-chain, the chain verifies the leaf signature matches the sender, verifies the path from leaf to root, checks the root against `approvalCriteria.merkleChallenges`, marks the leaf used in the challenge tracker (no replay), and then applies every other approval criterion before executing the transfer. On the site all of this is hidden; the user completes the claim, then signs. Through the API you fetch the reserved code and the proof yourself, see [Endpoints](endpoints.md).
### On-Chain Types
```ts
interface iMerkleChallenge {
root: string; // merkle root
maxUsesPerLeaf: T; // 1 = one-time use
uri: string; // where the leaf metadata lives
customData: string;
challengeTrackerId: string; // usage tracker id
leafSigner: string; // ETH address that signs leaves
}
interface iMerkleProof {
aunts: { aunt: string; onRight: boolean }[]; // path from leaf to root
leaf: string; // the one-time code
leafSignature: string; // ETH signature of `${leaf}-${intendedBitBadgesAddress}`
}
interface iMerkleChallengeTrackerDoc {
collectionId: CollectionId;
challengeTrackerId: string;
approvalId: string;
approvalLevel: 'collection' | 'incoming' | 'outgoing' | '';
approverAddress: BitBadgesAddress;
usedLeafIndices: iUsedLeafStatus[];
}
```
`merkleChallenges` lives on collection-level, outgoing, and incoming approval criteria. See [Merkle Challenges](../../token-standard/approval-criteria/merkle-challenges.md).
### How the Claim Links to the Approval
```ts
interface iChallengeTrackerIdDetails {
collectionId: CollectionId;
approvalId: string;
challengeTrackerId: string;
approvalLevel: 'collection' | 'incoming' | 'outgoing' | '';
approverAddress: BitBadgesAddress;
}
```
| Claim field | Purpose |
| --- | --- |
| `trackerDetails` | The link above. Must match the on-chain merkle challenge. |
| `collectionId` | Positive integer for an on-chain claim. Off-chain claims use a non-positive value. |
| `docClaimed` | Must be `true` for the claim to be active. Set when the claim is finalized and linked on-chain. |
| `cid` | Equals the `challengeTrackerId` for on-chain claims. |
| `action.seedCode` | Encrypted seed for the one-time codes. Decrypted only for authorized managers. |
The claim builder sets these for you. When creating claims through the API, make `trackerDetails` match the approval.
### Keep Both Sides Aligned
| Misalignment | Result |
| --- | --- |
| Claim allows 100 codes, tree has 50 leaves | The last 50 users succeed off-chain and fail on-chain |
| Approval updated, claim not updated | Users receive proofs for a stale root |
| Claim time window differs from on-chain `transferTimes` | Valid proof, transfer rejected |
| On-chain `initiatedBy` excludes the claim's allowed addresses | Valid proof, sender rejected |
Use the claim builder, which generates the tree and the claim together. Update both sides together. Make `numUses.maxUses` equal the leaf count and the approval's maximum transfers, and keep the claim's `transferTimes` inside the approval's window.
### Who Can Manage a Linked Claim
| Approval level | Who manages linked claims |
| --- | --- |
| Collection | The current collection manager. BitBadges checks it. |
| Outgoing (sender) | The sender address |
| Incoming (recipient) | The recipient address |
Transferring the manager role transfers control of every claim linked to collection approvals. The new manager can update, reconfigure, or disable them; the old manager loses access. With a manager splitter or multisig, all signers share that authority.
## Trust Model
Claims are an off-chain system run by BitBadges. Know what you trust.
- **The claim creator.** They control plugins, params, and success logic and can change them at any time. Each update increments the claim `version`. Pass `_expectedVersion` when completing claims so a changed claim fails instead of silently applying new criteria.
- **BitBadges as oracle.** BitBadges evaluates criteria, keeps state, and issues proofs. On-chain verification proves a proof is structurally valid, not that the off-chain criteria were applied correctly.
- **Third-party plugins.** A custom plugin must be honest, available within 10 seconds, and secure. A compromised plugin can approve the wrong users. Fewer plugins means fewer dependencies.
- **Codes and passwords.** Whoever holds a code or password can attempt the claim. For on-chain gating, the leaf signature binds the proof to the address that claimed, so a leaked code only helps that address on-chain. Use passwords mainly for backend auto-completion.
- **Flash criteria.** A user can qualify, transfer the qualifying token, and let the next holder qualify too. Design criteria with that in mind.
For high-stakes flows: prefer on-chain criteria (token ownership, on-chain dynamic store challenges), treat claims as a convenience layer over approval criteria that enforce the limits on their own, plan for rollback, audit each plugin and its owner, and monitor attempts through the API.
## Do You Need a Claim
Claims are a convenience, not a requirement. The on-chain merkle challenge is fully decentralized: generate your own tree, hand out leaves, and let users submit proofs without BitBadges claims.
Simpler alternatives that often fit:
- Fully on-chain criteria (token ownership, dynamic store challenges, address checks).
- A snapshot of eligible addresses baked into an approval.
- Checking ownership in your own backend with the API or SDK.
- Sending tokens directly.
Claims earn their place when you want the hosted in-site experience, composable criteria with AND/OR/NOT logic, managed state (who claimed, limits, code redemption), off-chain to on-chain gating with proof generation handled for you, or custom endpoints with managed state and webhooks.
## Related
- [Endpoints](endpoints.md)
- [Plugins](plugins.md)
- [Distribute with Claims](../../guides/distribute-with-claims.md)
- [Merkle Challenges](../../token-standard/approval-criteria/merkle-challenges.md)
## File: api/claims/dynamic-stores.md
---
description: "Dynamic stores are BitBadges-hosted address lists you update by API, UI, or Zapier and attach to claims through the whitelist plugin."
---
# Dynamic Stores
A dynamic store is an address list that BitBadges hosts for you. You add and remove addresses from anywhere that can send an HTTP request (a backend, a cron job, an agent, a Zapier zap), then attach the store to one or more claims. Eligibility management is decoupled from claim configuration.
See the [API reference](/api-reference) for every route's request and response schema.
The TypeScript snippets use the configured `BitBadgesApi` client from the [API setup example](../README.md#example).
## Example
Add bob to the store `store_demo_01`. `STORE_SECRET` holds the store's `dataSecret` from the developer portal.
```bash
curl -X POST https://api.bitbadges.io/api/v0/storeActions/single \
-H "Content-Type: application/json" -H "x-api-key: $BITBADGES_API_KEY" \
-d '{ "dynamicDataId": "store_demo_01", "dataSecret": "'"$STORE_SECRET"'",
"actionName": "add", "payload": { "address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue" } }'
```
```ts
// Single action: POST /api/v0/storeActions/single
await BitBadgesApi.performStoreAction({
dynamicDataId: 'store_demo_01',
dataSecret: process.env.STORE_SECRET, // omit when signed in as the creator or manager
actionName: 'add', // 'add' | 'remove'
payload: { address: 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue' }
});
// Batch: POST /api/v0/storeActions/batch
await BitBadgesApi.performBatchStoreAction({
dynamicDataId: 'store_demo_01',
dataSecret: process.env.STORE_SECRET,
actions: [
{ actionName: 'add', payload: { address: 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue' } },
{ actionName: 'add', payload: { address: 'bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf' } },
{ actionName: 'remove', payload: { address: 'bb18cad7xxsk3drvwdxeasc3wqn2plftpzq2tsrsr' } }
]
});
```
Both routes answer `200` with an empty object:
```json
{}
```
Attach it to a claim with the `whitelist` plugin:
```ts
const whitelistPlugin = {
pluginId: 'whitelist',
instanceId: 'store-gate',
version: '0',
publicParams: { maxUsesPerAddress: 1, hasPrivateList: true },
privateParams: {
useDynamicStore: true,
dynamicDataId: 'store_demo_01',
dataSecret: process.env.STORE_SECRET
}
}
```
## How It Works
1. Create a store in the [developer portal](https://bitbadges.io/developer). You receive a store ID (`dynamicDataId`) and a store secret (`dataSecret`).
2. Add or remove addresses whenever your eligibility changes.
3. Attach the store to claims with the `whitelist` plugin.
4. Users claim. BitBadges checks whether the address is in the store.
One store can back many claims. Update it once and every attached claim reflects the change.
Store actions run through a queue. Expect a 1 to 2 second delay between the API call and the store update. If a user claims right after you add them, the store may not have caught up; add a small buffer or retry on the user side. Actions in one batch are processed together, so they stay consistent relative to each other.
## Payloads
```ts
interface iPerformStoreActionSingleWithBodyAuthPayload {
_isSimulation?: boolean; // dry run
dynamicDataId: string;
dataSecret?: string; // omit when signed in as creator or manager
actionName: string; // 'add' | 'remove'
payload: { address: string };
}
interface iPerformStoreActionBatchWithBodyAuthPayload {
_isSimulation?: boolean;
dynamicDataId: string;
dataSecret?: string;
actions: { actionName: string; payload: { address: string } }[];
}
```
The developer portal shows the exact route and body for your store if you want to send the request without the SDK.
## Data Model
```ts
interface iDynamicDataDoc {
handlerId: 'addresses'; // the only supported handler
dynamicDataId: string;
label: string;
dataSecret: string;
data: string[]; // the stored addresses
createdBy: BitBadgesAddress;
managedBy: BitBadgesAddress;
publicUseInClaims?: boolean; // anyone can attach the store to a claim
createdAt?: number; // UNIX ms
lastUpdated?: number; // UNIX ms
}
```
The store document after the batch above, as the developer portal shows it to the owner (synthesized from the SDK types; `dataSecret` is only returned to the owner):
```json
{
"_docId": "store_demo_01",
"handlerId": "addresses",
"dynamicDataId": "store_demo_01",
"label": "Eligible buyers",
"dataSecret": "dss_4f9a1c7e2b8d6f3a0e5c9b2d7a4f1e8c",
"data": ["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue", "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf"],
"createdBy": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"managedBy": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"publicUseInClaims": false,
"createdAt": "1788652800000",
"lastUpdated": "1788739200000"
}
```
Only addresses are supported as identifiers. Emails, usernames, and platform IDs are not.
## Authentication
| Caller | Requirement |
| --- | --- |
| Signed in as creator or manager | No `dataSecret` |
| API with a key | `dataSecret` in the body |
| Attaching a store you do not own | The store must have `publicUseInClaims: true`. Reads then need no secret; add and remove still require authentication. |
Keep the secret on your backend.
## Ways to Update a Store
**UI.** Manage entries directly in the developer portal: add, remove, and view addresses.
**API.** The single and batch routes above.
**Zapier.** The BitBadges Zapier integration connects 7000+ apps. Your trigger is the app you integrate; the action is **BitBadges: Add User to Dynamic Store**. Map fields from the trigger (for example an Eventbrite attendee's address) into the store action step. Zapier docs: [Field mapping](https://help.zapier.com/hc/en-us/articles/31709122224653-Enter-data-in-Zap-fields#01JC4MFMXXJXSS7GBAYZP32XKZ), [Send data between steps by mapping fields](https://help.zapier.com/hc/en-us/articles/8496343026701-Send-data-between-steps-by-mapping-fields). The action shows several identifier fields; fill only the one that matches your store type and leave the rest blank.
Zapier can also do the criteria check. Example: a Google Form collects an email, a zap triggers on each response, a Mailchimp step checks the subscriber, and the zap adds the user to the store on success. Point in-site claimers at the form with the `url-clicker` or `custom-instructions` plugin, or from the claim description.
## Integration Patterns
```ts
// Backend webhook: a purchase makes the buyer eligible
app.post('/webhooks/shopify-purchase', async (req, res) => {
const buyerAddress = await lookupAddress(req.body.customer.email); // your own email to address mapping
await BitBadgesApi.performStoreAction({
dynamicDataId: 'store_demo_01',
dataSecret: process.env.STORE_SECRET,
actionName: 'add',
payload: { address: buyerAddress }
});
res.sendStatus(200);
});
```
```ts
// Agent or model decides eligibility
const eligible = await yourModel.evaluate(userAddress);
if (eligible) {
await BitBadgesApi.performStoreAction({
dynamicDataId: 'store_demo_01',
dataSecret: process.env.STORE_SECRET,
actionName: 'add',
payload: { address: userAddress }
});
}
```
```ts
// Scheduled sync from your database
const eligibleUsers = await db.query('SELECT address FROM users WHERE eligible = true');
await BitBadgesApi.performBatchStoreAction({
dynamicDataId: 'store_demo_01',
dataSecret: process.env.STORE_SECRET,
actions: eligibleUsers.map((u) => ({ actionName: 'add', payload: { address: u.address } }))
});
```
A custom plugin with a status webhook can add addresses to a store when a claim succeeds, which chains claims together.
Why this shape works: the store is reusable across claims, updates apply in real time (check a user in at the door and they can claim), BitBadges holds the storage and validation so you only send update hooks, and the claim builder can attach a store in one click (select it in the templates section, or open the store's page and click **Create Claim**).
## Limits
- Off-chain stores work only in off-chain contexts (claims through the `whitelist` plugin). They are not readable on-chain.
- Only addresses are supported.
- Queue processing adds a 1 to 2 second delay.
## On-Chain Dynamic Stores
The chain has its own dynamic stores: boolean address-value stores managed by transactions and checked by approval criteria through `DynamicStoreChallenge`. They are separate from the off-chain stores on this page. See [Dynamic Store Challenges](../../token-standard/approval-criteria/dynamic-store-challenges.md) and [MsgCreateDynamicStore](../../token-standard/messages/msg-create-dynamic-store.md).
## Related
- [Plugins](plugins.md)
- [Claims](README.md)
- [Distribute with Claims](../../guides/distribute-with-claims.md)
## File: api/claims/endpoints.md
---
description: "Claims API reference. Complete, simulate, and verify claims, fetch and search them, create and delete them, and build the merkle proof for on-chain claims."
---
# Claims
The claims routes let a backend or an agent complete claims for users, check who has claimed, and manage claims without the site. All claim routes require an API key in the `x-api-key` header. Create one at [bitbadges.io/developer](https://bitbadges.io/developer). Concepts (indexed versus on-demand claims, success logic, claim numbers, the trust model) live in [Claims](README.md).
See the [API reference](/api-reference) for every route's request and response schema.
The TypeScript snippets use the configured `BitBadgesApi` client from the [API setup example](../README.md#example).
Addresses in paths accept any supported format. `0x` and `bb1` addresses both work and resolve to the same account. The examples complete `claim_demo_01` (a code-gated claim on collection 1 with the instance id `codes-gate`) for bob.
## Complete a Claim
```bash
curl -X POST https://api.bitbadges.io/api/v0/claims/complete/claim_demo_01/bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue \
-H "Content-Type: application/json" -H "x-api-key: $BITBADGES_API_KEY" \
-d '{ "_expectedVersion": 0, "codes-gate": { "code": "54cf408a23d18090ae296d44ee588da291bdc4bec75ef0af8b2513e5903caaba-0" } }'
```
```ts
const res = await BitBadgesApi.completeClaim('claim_demo_01', 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue', {
_expectedVersion: 0,
'codes-gate': { code: '54cf408a23d18090ae296d44ee588da291bdc4bec75ef0af8b2513e5903caaba-0' }
});
console.log(res.claimAttemptId); // 3b9d2f7a1c4e6b8d0f2a4c6e8b1d3f5a
// Claims process asynchronously. Poll the attempt.
const status = await BitBadgesApi.getClaimAttemptStatus(res.claimAttemptId);
console.log(status.success); // true once the queue has processed it
```
```json
{ "claimAttemptId": "3b9d2f7a1c4e6b8d0f2a4c6e8b1d3f5a" }
```
### Body
```ts
type CompleteClaimBody = {
_expectedVersion: number; // claim.version from getClaim(). -1 skips the check (not recommended)
_specificInstanceIds?: string[]; // only run these plugin instances
[instanceId: string]: Record | number | string[] | undefined; // per-plugin user inputs, keyed by instance ID
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `_expectedVersion` | number | yes | The claim version you fetched. The attempt fails on a mismatch, so a creator cannot change criteria or actions without you knowing. `-1` disables the check. |
| `_specificInstanceIds` | string[] | no | Complete only these plugin instances. Must be compatible with the claim's success logic. |
| `[instanceId]` | object | per plugin | User inputs for that plugin instance, for example `{ password }` or `{ code }`. Custom plugins define their own schema. Look it up with [Plugins](plugins.md). |
### Behavior
- The route simulates first. If simulation fails, it returns the error immediately and nothing is queued. If simulation passes, the attempt enters the queue and the response carries `claimAttemptId`.
- If the claim requires sign in (`initiatedBy` plugin), the request needs a session for that address with the `Complete Claims` scope. See [Sign In with BitBadges](../sign-in/README.md).
- If sign in is not required, gate the claim another way, for example a password that only your backend knows.
- On the site, the **API Code** tab of a claim shows snippets customized to that claim.
## Simulate a Claim
```ts
const res = await BitBadgesApi.simulateClaim('claim_demo_01', 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue', {
_expectedVersion: 0,
_specificInstanceIds: ['num-uses', 'codes-gate'],
'codes-gate': { code: '54cf408a23d18090ae296d44ee588da291bdc4bec75ef0af8b2513e5903caaba-0' }
});
```
```bash
curl -X POST https://api.bitbadges.io/api/v0/claims/simulate/claim_demo_01/bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue \
-H "Content-Type: application/json" -H "x-api-key: $BITBADGES_API_KEY" \
-d '{ "_expectedVersion": 0, "codes-gate": { "code": "54cf408a23d18090ae296d44ee588da291bdc4bec75ef0af8b2513e5903caaba-0" } }'
```
```json
{ "claimAttemptId": "00000000000000000000000000000000" }
```
Simulation is instant, is not queued, and consumes no use. The body is the same as `completeClaim`. Use `_specificInstanceIds` to test only some plugins. The returned `claimAttemptId` is a zeroed placeholder for compatibility.
## Check an Attempt
```bash
curl https://api.bitbadges.io/api/v0/claims/status/3b9d2f7a1c4e6b8d0f2a4c6e8b1d3f5a -H "x-api-key: $BITBADGES_API_KEY"
```
```ts
const status = await BitBadgesApi.getClaimAttemptStatus('3b9d2f7a1c4e6b8d0f2a4c6e8b1d3f5a');
if (status.success) {
// this attempt succeeded
}
```
```json
{
"success": true,
"error": "",
"code": "54cf408a23d18090ae296d44ee588da291bdc4bec75ef0af8b2513e5903caaba-0",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"
}
```
| Field | Description |
| --- | --- |
| `success` | Final result of the attempt. |
| `error` | Error message when it failed. |
| `code` | The reserved merkle code for an on-chain token claim. Only present when you have permission and the claim is on-chain. |
| `bitbadgesAddress` | The claiming address. |
The route accepts `GET` and `POST`. Obtain `claimAttemptId` from `completeClaim` or from a custom plugin (it is in every plugin request).
### Polling
```ts
const res = await BitBadgesApi.completeClaim('claim_demo_01', 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue', {
_expectedVersion: 0,
'codes-gate': { code: '54cf408a23d18090ae296d44ee588da291bdc4bec75ef0af8b2513e5903caaba-0' }
});
const pollStatus = async (attemptId: string, maxRetries = 10): Promise => {
for (let i = 0; i < maxRetries; i++) {
await new Promise((r) => setTimeout(r, 2000));
const status = await BitBadgesApi.getClaimAttemptStatus(attemptId);
if (status.success !== undefined) return status.success;
}
throw new Error('Claim processing timed out');
};
const success = await pollStatus(res.claimAttemptId);
```
Typical processing time is 1 to 5 seconds. Claims for the same collection process sequentially. Different collections process in parallel.
## Check Success by Address
```bash
curl https://api.bitbadges.io/api/v0/claims/success/claim_demo_01/bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue -H "x-api-key: $BITBADGES_API_KEY"
```
```ts
const res = await BitBadgesApi.checkClaimSuccess('claim_demo_01', 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue');
if (res.successCount >= 1) {
// the address has completed the claim
}
```
```json
{ "successCount": 1, "claimNumbers": [0] }
```
`successCount` is `1` for on-demand claims and the number of completions for standard (indexed) claims. `claimNumbers` lists the zero-based claim numbers for indexed claims.
Verifying a claim is two-fold. First authenticate the user (with [Sign In with BitBadges](../sign-in/README.md) or your own method). Then look up the attempt or the address as above. Never trust a client that says it claimed.
## Fetch Claim Attempts
```ts
const res = await BitBadgesApi.getClaimAttempts('claim_demo_01', {
address: 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue', // omit for the most recent attempts
includeErrors: true,
bookmark: ''
});
```
```bash
curl "https://api.bitbadges.io/api/v0/claims/claim_demo_01/attempts?address=bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue&includeErrors=true" \
-H "x-api-key: $BITBADGES_API_KEY"
```
```json
{
"docs": [
{
"success": true,
"attemptedAt": "1788739200000",
"claimId": "claim_demo_01",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"claimAttemptId": "3b9d2f7a1c4e6b8d0f2a4c6e8b1d3f5a",
"claimNumber": 0
},
{
"success": false,
"attemptedAt": "1788738900000",
"claimId": "claim_demo_01",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"claimAttemptId": "8e1c4a6f2d9b3e7a5c0f1d4b6a8e2c9f",
"claimNumber": -1,
"error": "Invalid code. Not found in list of codes."
}
],
"bookmark": "eyJza2lwIjoyNX0",
"total": 2
}
```
Route: `GET /api/v0/claims/{claimId}/attempts` with `address`, `includeErrors`, and `bookmark` as query parameters. Paginated with a bookmark. See [Pagination and Views](../pagination-and-views.md).
## Fetch a Claim
```bash
curl "https://api.bitbadges.io/api/v0/claim/claim_demo_01?fetchAllClaimedUsers=true" -H "x-api-key: $BITBADGES_API_KEY"
```
```ts
const { claim } = await BitBadgesApi.getClaim('claim_demo_01', {
fetchPrivateParams: false, // true = include private params (creator or manager only, needs auth)
fetchAllClaimedUsers: true, // populates numUses publicState.claimedUsers { [address]: [claimNumbers] }
privateStatesToFetch: ['codes-gate'] // private state for specific plugin instances
});
// Batch form: POST /api/v1/claims/fetch
const res = await BitBadgesApi.getClaims({
claimsToFetch: [{ claimId: 'claim_demo_01', fetchAllClaimedUsers: true }]
});
```
The claim document after one success (synthesized from the SDK types; private params are stripped because `fetchPrivateParams` is false):
```json fold=9-15,38-44,46-51
{
"claim": {
"_includesPrivateParams": false,
"claimId": "claim_demo_01",
"createdBy": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"managedBy": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "1",
"standaloneClaim": false,
"trackerDetails": {
"collectionId": "1",
"approvalId": "mint-approval",
"challengeTrackerId": "claim_demo_01",
"approvalLevel": "collection",
"approverAddress": ""
},
"plugins": [
{
"pluginId": "numUses",
"instanceId": "num-uses",
"version": "0",
"publicParams": { "maxUses": 100, "hideCurrentState": false, "displayAsUnlimited": false },
"privateParams": {},
"publicState": {
"numUses": 1,
"usedClaimNumbers": [{ "start": "0", "end": "0" }],
"claimedUsers": { "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue": [0] }
}
},
{
"pluginId": "codes",
"instanceId": "codes-gate",
"version": "0",
"publicParams": { "numCodes": 100, "hideCurrentState": false },
"privateParams": {},
"publicState": { "usedCodeRanges": [{ "start": "0", "end": "0" }] }
},
{
"pluginId": "initiatedBy",
"instanceId": "sign-in",
"version": "0",
"publicParams": {},
"privateParams": {},
"publicState": {}
}
],
"rewards": [],
"showInSearchResults": true,
"categories": ["nft"],
"estimatedCost": "Free",
"estimatedTime": "1 minute",
"approach": "in-site",
"metadata": {
"name": "Demo NFT mint",
"description": "Redeem a one-time code for one Demo NFT.",
"image": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/claim.png"
},
"assignMethod": "",
"lastUpdated": "1788652800000",
"version": "0"
}
}
```
The **JSON** tab of a claim on the site shows the same document. `fetchPrivateParams: true` requires the `Read Private Claim Data` scope for that claim's creator or manager.
## Search Claims
```ts
const res = await BitBadgesApi.searchClaims({
searchValue: 'demo nft', // regex match on name
bookmark: ''
});
console.log(res.claims.map((c) => c.claimId)); // ['claim_demo_01']
```
```bash
curl "https://api.bitbadges.io/api/v0/claims/search?searchValue=demo%20nft" -H "x-api-key: $BITBADGES_API_KEY"
```
Route: `GET /api/v0/claims/search` with `searchValue` and `bookmark` as query parameters. The response is `{ claims, bookmark }` with the same claim documents as above. Only claims with `showInSearchResults: true` appear.
## Create Claims
```ts
import crypto from 'crypto';
import { BitBadgesAPI, BigIntify } from 'bitbadges';
const api = new BitBadgesAPI({ apiKey: process.env.BITBADGES_API_KEY, convertFunction: BigIntify });
// Generate codes the same way the API does: sha256(`${seedCode}-${i}`) + `-${i}`
const seedCode = crypto.randomBytes(32).toString('hex');
const numCodes = 50;
const codes: string[] = [];
for (let i = 0; i < numCodes; i++) {
codes.push(crypto.createHash('sha256').update(`${seedCode}-${i}`).digest('hex') + `-${i}`);
}
await api.createClaims({
claims: [
{
claimId: 'claim_demo_02',
plugins: [
{
pluginId: 'numUses',
instanceId: 'num-uses',
version: '0',
publicParams: { maxUses: numCodes, hideCurrentState: false, displayAsUnlimited: false },
privateParams: {}
},
{
pluginId: 'codes',
instanceId: 'codes-gate',
version: '0',
publicParams: { numCodes, hideCurrentState: false },
privateParams: { codes, seedCode }
}
],
rewards: [],
metadata: { name: 'Bot code drop', description: 'Codes distributed by my bot', image: '' },
showInSearchResults: false,
categories: [],
approach: 'api'
}
]
});
```
Route: `POST /api/v0/claims`. Requires a session with the `Manage Claims` scope. The response is `{}`. Pass `testClaims: true` to create test claims that disappear when the browser session ends and never show in search. Linking a claim to an on-chain collection approval requires the proper permissions and extra setup; the site or the MCP builder tools apply `collectionId` and the tracker details for you. If you use the MCP builder tools, call the `build_claim` tool instead of writing this payload. For the `codes` plugin, set `publicParams.numCodes` as a number and leave `privateParams` empty. The server generates `seedCode` and the codes. Use the `search_plugins` tool to list plugins and their parameters.
Update with `PUT /api/v0/claims` (`{ claims: UpdateClaimRequest[] }`).
## Delete Claims
```ts
await BitBadgesApi.deleteClaims({ claimIds: ['claim_demo_02'] });
```
```json
{}
```
Route: `DELETE /api/v0/claims`. Deletion is a soft delete. The claim gets `deletedAt` and drops out of queries. Attempt records stay for history.
## Reserved Codes and the Merkle Proof
An on-chain gated claim reserves a merkle code for the user when the claim succeeds. The user then proves that code in `MsgTransferTokens`. The flow is: create claim, complete claim, get the reserved code, get the proof path, submit the transfer.
### 1. Get Reserved Codes and Leaf Signatures
```ts
const reserved = await BitBadgesApi.getReservedClaimCodes('claim_demo_01', 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue', {
_expectedVersion: claim.version
});
```
```json
{
"reservedCodes": ["54cf408a23d18090ae296d44ee588da291bdc4bec75ef0af8b2513e5903caaba-0"],
"leafSignatures": [
"0x5c1d8f0b3a7e2c9d4f6a1b8e0c3d5f7a9b2e4c6d8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d0e2f4a6b8c1d3e5f7a9b0c2d4e6f8a1b3c5d7e9f0a2b4c6d8e1b"
]
}
```
Route: `POST /api/v0/claims/reserved/{claimId}/{address}`. `leafSignatures[i]` proves the mapping between `reservedCodes[i]` and the address.
### 2. Get the Merkle Path
The proof route is HTTP only (no SDK wrapper). Leaves are `sha256(code)`, or `sha256(bitbadgesAddress)` when the challenge uses `useCreatorAddressAsLeaf`. The leaf below is `sha256` of the reserved code above.
```bash
curl -X POST https://api.bitbadges.io/api/v0/merkleProofInfo \
-H "Content-Type: application/json" -H "x-api-key: $BITBADGES_API_KEY" \
-d '{
"collectionId": "1",
"approvalId": "mint-approval",
"approvalLevel": "collection",
"approverAddress": "",
"challengeTrackerId": "claim_demo_01",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"claimCodes": ["54cf408a23d18090ae296d44ee588da291bdc4bec75ef0af8b2513e5903caaba-0"],
"leaves": ["ab24f755d460e42069f4e20715f9807c896a8c5b974c4fda776ebdbd357a8526"]
}'
```
```json
{
"allProofDetails": [
{
"proofObj": [
{ "aunt": "0647c17406a5682d95a118679d324a19eee7556a06158a1f475a7ea664d7d73f", "onRight": true },
{ "aunt": "64c8e890a8cfacb411332d91e3b6c260c59d1d63c3122ed5d4e461c4afaed04e", "onRight": true },
{ "aunt": "a70e0dae15ca1d64303f6f50b22a961f46e9b4d0e29dce2718149d113bdf2b32", "onRight": true }
],
"isValidProof": true,
"leafIndex": 0,
"leaf": "ab24f755d460e42069f4e20715f9807c896a8c5b974c4fda776ebdbd357a8526"
}
]
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `collectionId` | string | yes | Collection with the approval. |
| `approvalId` | string | yes | Approval that carries the merkle challenge. |
| `approvalLevel` | string | yes | `collection`, `outgoing`, or `incoming`. |
| `approverAddress` | string | yes | Empty string for collection level. The user's address for outgoing or incoming. |
| `challengeTrackerId` | string | yes | The `challengeTrackerId` of the merkle challenge in `approvalCriteria.merkleChallenges`. |
| `leaves` | string[] | yes | `sha256` of each code, or of the address for `useCreatorAddressAsLeaf`. |
| `claimCodes` | string[] | code claims | The reserved codes, in the same order as `leaves`. |
| `bitbadgesAddress` | string | address claims | The claiming address. Required when `useCreatorAddressAsLeaf` is set. |
### 3. Submit the Transfer
```ts
const proof = proofInfo.allProofDetails[0];
const result = await client.signAndBroadcast([
MsgTransferTokens.create({
creator: client.address,
collectionId: '1',
transfers: [
{
from: 'Mint',
toAddresses: [client.address],
balances: [
{
tokenIds: [{ start: '1', end: '1' }],
amount: '1',
ownershipTimes: [{ start: '1', end: '18446744073709551615' }]
}
],
merkleProofs: [
{
leaf: reserved.reservedCodes[0],
leafSignature: reserved.leafSignatures[0],
aunts: proof.proofObj
}
],
prioritizedApprovals: [
{ approvalId: 'mint-approval', approvalLevel: 'collection', approverAddress: '', version: '0' }
],
onlyCheckPrioritizedCollectionApprovals: true,
onlyCheckPrioritizedIncomingApprovals: false,
onlyCheckPrioritizedOutgoingApprovals: false,
memo: ''
}
]
})
]);
```
`client` is the signing client for the claiming address (bob), so `client.address` is `bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue`. `prioritizedApprovals` tells the chain which approval to check. The `approvalId` must match the approval that references the claim's merkle challenge. Always pass `prioritizedApprovals`, even when empty. See [Prioritized Approvals](../../token-standard/concepts/prioritized-approvals.md) and [MsgTransferTokens](../../token-standard/messages/msg-transfer-tokens.md).
## Patterns for Agents and Backends
- **Bot distributes codes.** Create a code-gated claim, hand out codes through your app or bot, and let users complete on the site or complete on their behalf.
- **Backend auto-completion.** Create a password-gated claim where only your backend knows the password. Complete claims for users when they meet your own criteria. This is incompatible with in-site plugins that need user interaction; a custom plugin is usually the better option. See [Build a Claim Plugin](../../guides/build-a-claim-plugin.md).
- **Ownership-gated minting.** Use the `must-own-badges` plugin so holders of token X can mint token Y.
- **Time-windowed drops.** Add `transferTimes` to bound when claims can complete.
- **Standalone claims.** Criteria check, then an off-chain reward (points, a list spot, gated content). No transaction needed.
Tips:
- Call `simulateClaim` before `completeClaim` in production to catch errors without consuming a use.
- Match `numUses.maxUses` to the merkle tree leaf count for on-chain claims. A mismatch leaves some codes without a valid proof.
- Pass `_expectedVersion` so concurrent agents or users cannot race a changed claim.
- Poll `getClaimAttemptStatus` after submitting. Attempts resolve in about 2 to 5 seconds.
- Plugin schemas: see [Plugins](plugins.md) for `getPlugin`, `getPlugins`, `searchPlugins`, and `getPluginErrors`.
## Related
- [Claims](README.md)
- [Plugins](plugins.md)
- [Distribute with Claims](../../guides/distribute-with-claims.md)
- [API reference](/api-reference)
## File: api/claims/plugins.md
---
description: "Every claim plugin id with its parameters, state, and where it runs, plus the request and response contract for custom plugin endpoints."
---
# Plugins
A claim is a list of plugin instances. Each plugin checks one criterion and reports pass or fail. This page is the reference for the built-in plugin ids and for the HTTP contract that custom plugins implement. The tutorial for writing one is [Build a Claim Plugin](../../guides/build-a-claim-plugin.md).
See the [API reference](/api-reference) for every route's request and response schema.
## Look Up Any Plugin
```bash
curl https://api.bitbadges.io/api/v0/plugin/must-own-badges -H "x-api-key: $BITBADGES_API_KEY"
```
```ts
import { BitBadgesAdminAPI, BigIntify } from 'bitbadges';
// searchPlugins is exposed on the admin client, which includes the base API methods.
const BitBadgesApi = new BitBadgesAdminAPI({ convertFunction: BigIntify, apiKey: process.env.BITBADGES_API_KEY });
const { plugin } = await BitBadgesApi.getPlugin('must-own-badges');
const latest = plugin.versions[plugin.versions.length - 1];
console.log(latest.userInputsSchema); // what the claiming user provides
console.log(latest.publicParamsSchema); // creator-configured, public
console.log(latest.privateParamsSchema); // creator-configured, private
console.log(latest.verificationCall); // HTTP endpoint config
console.log(latest.stateFunctionPreset); // 'Stateless' | 'ClaimToken' | 'ClaimNumbers' | 'CustomResponseHandler'
// Several at once: POST /api/v0/plugins/fetch
const many = await BitBadgesApi.getPlugins({
pluginIds: ['must-own-badges', 'min-badge', 'url-clicker'],
returnSensitiveData: false // true = include pluginSecret (owner only)
});
// Directory search: GET /api/v0/plugins/search
const found = await BitBadgesApi.searchPlugins({ searchValue: 'badge', bookmark: '', locale: 'en' });
// Execution errors for your own plugin: GET /api/v0/plugins/errors
const errors = await BitBadgesApi.getPluginErrors({ pluginId: 'ticket-check', bookmark: '' });
```
The `must-own-badges` plugin document (synthesized from the indexer's seed data; timestamps vary):
```json fold=15-19,24-25
{
"plugin": {
"_docId": "must-own-badges",
"pluginId": "must-own-badges",
"createdBy": "",
"managedBy": "",
"metadata": {
"name": "Token Requirements",
"description": "Which tokens must the user own?",
"image": "https://bitbadges.io/images/bitbadgeslogo.png",
"createdBy": "BitBadges",
"documentation": "https://docs.bitbadges.io",
"sourceCode": ""
},
"locale": "en",
"toPublish": false,
"reviewCompleted": true,
"lastUpdated": "1788652800000",
"createdAt": "1788652800000",
"versions": [
{
"version": "0",
"finalized": true,
"createdAt": "1788652800000",
"lastUpdated": "1788652800000",
"requiresSessions": false,
"requiresUserInputs": false,
"duplicatesAllowed": true,
"reuseForNonIndexed": true,
"receiveStatusWebhook": false,
"stateFunctionPreset": "Stateless",
"userInputsSchema": [],
"privateParamsSchema": [],
"publicParamsSchema": [
{ "key": "ownershipRequirements", "label": "Requirements", "type": "ownershipRequirements", "required": true }
],
"verificationCall": {
"uri": "https://api.bitbadges.io/api/v0/integrations/query/must-own-badges",
"passAddress": true,
"hardcodedInputs": []
}
}
]
}
}
```
`getPluginErrors` returns the stored failures for a plugin you own:
```json
{
"docs": [
{
"_docId": "ticket-check-1788739200000",
"pluginId": "ticket-check",
"timestamp": 1788739200000,
"error": "Ticket 4821 was already redeemed",
"context": { "claimId": "claim_demo_01", "claimAttemptId": "8e1c4a6f2d9b3e7a5c0f1d4b6a8e2c9f", "instanceId": "ticket-gate" }
}
],
"bookmark": "eyJza2lwIjoyNX0",
"total": 1
}
```
The eight core plugins below are not plugin documents, so `getPlugin` does not return them. Their schemas are fixed and listed here. Every other id is a plugin document that `getPlugin` describes in full.
Plugin ids are localized. The English document is the bare id (`min-badge`); other locales append a suffix (`min-badge-es`).
## Core Plugins
Core plugins run in memory inside the API. No HTTP call is made.
| Plugin id | Purpose | Duplicates allowed | Stateful |
| --- | --- | --- | --- |
| `numUses` | Cap total claims and assign claim numbers | no | yes |
| `codes` | One-time codes | yes | yes |
| `password` | Shared secret | yes | no |
| `transferTimes` | Time windows | yes | no |
| `initiatedBy` | Proof of address (sign in) | yes | no |
| `whitelist` | Address list or dynamic store gate | yes | yes |
| `halt` | Pause the claim | yes | no |
| `anonymous` | Allow claims with no address | no | no |
### `numUses`
Limits total successful claims. This plugin is always required and cannot be made optional through success logic. It is the default claim number assigner: the claim number is the prior `numUses` count (zero-based).
```ts
// Public params
type NumUsesPublicParams = { maxUses: number; hideCurrentState?: boolean; displayAsUnlimited?: boolean }
// Public state
type NumUsesPublicState = { numUses?: number; usedClaimNumbers?: UintRange[]; claimedUsers?: { [bitbadgesAddress: string]: number[] } }
```
No user input. Fails with `Overall max uses exceeded` once `numUses >= maxUses`. `claimedUsers` is populated when you fetch the claim with `fetchAllClaimedUsers: true`.
### `codes`
Codes are either generated from `seedCode` or listed explicitly. Generated code `i` is `sha256("-") + "-"`. When `seedCode` is set, the index after the dash is checked against the regenerated hash. When only `codes` is set, `codes.length` must equal `numCodes`.
```ts
// User input
type CodesUserInput = { code: string }
// Public params
type CodesPublicParams = { numCodes: number; hideCurrentState?: boolean }
// Private params
type CodesPrivateParams = { codes: string[]; seedCode: string }
// Public state
type CodesPublicState = { usedCodeRanges?: UintRange[] } // ranges of used code indices
```
When this plugin is the claim number assigner, the claim number is the code index. Errors: `Invalid code in body provided.`, `Invalid code. Not found in list of codes.`, `Code already used`.
### `password`
```ts
// User input
type PasswordUserInput = { password: string }
// Private params
type PasswordPrivateParams = { password: string }
```
No public params or state. The comparison is constant-time. Error: `Incorrect password`.
### `transferTimes`
```ts
// Public params
type TransferTimesPublicParams = { transferTimes: UintRange[] } // allowed windows, UNIX ms
```
No user input, state, or private params. Passes when `Date.now()` falls inside a range. Error: `We are currently outside the approved time window.`
### `initiatedBy`
Requires the claiming address to be signed in to BitBadges, or approved with the `Complete Claims` scope. No configurable params. Configuration happens in the claim builder UI. Errors: `Must be authenticated to claim`, `Invalid address. Provided address does not match the address of the signed in user.`
### `whitelist`
Gates on an address list. The list can be public (in `publicParams`), private (in `privateParams`), or a dynamic store. Both `0x` and `bb1` forms of an address match.
```ts
// Public params
type WhitelistPublicParams = { listId?: string; list?: AddressList; maxUsesPerAddress?: number; hasPrivateList?: boolean }
// Private params
type WhitelistPrivateParams = { useDynamicStore?: boolean; dynamicDataId?: string; dataSecret?: string; listId?: string; list?: AddressList }
// Private state
type WhitelistPrivateState = { addresses: { [address: string]: number } } // claims per address
```
A `whitelist: false` list acts as a denylist. When this plugin is the claim number assigner, the claim number is the address's index in the list. A public dynamic store (`publicUseInClaims`) needs no `dataSecret` for reads; other stores do. See [Dynamic Stores](dynamic-stores.md). Errors: `User not in whitelist`, `User in denylist`, `User already exceeded max uses`, `Dynamic data doc not found`, `Invalid data secret`.
### `halt`
No params. Every attempt fails with `Claim halted` while the plugin is present. Add it to pause, remove it to resume.
### `anonymous`
No params. Requires the claiming address to be the null address `bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv`. Error: `BitBadges address must be anonymous.`
## BitBadges-Hosted Plugins
These are plugin documents created by BitBadges. Their `verificationCall.uri` points at `https://api.bitbadges.io/api/v0/integrations/query/`, and the API handles them in-process rather than over the network. All are `Stateless`, finalized, and allow duplicates.
| Plugin id | Checks | Public params | User inputs | `passAddress` | On-demand compatible |
| --- | --- | --- | --- | --- | --- |
| `must-own-badges` | Ownership requirements (`$and` / `$or` groups of assets, amounts, times) | `ownershipRequirements` (type `ownershipRequirements`, required) | none | yes | yes |
| `min-badge` | `BADGE` balance of at least `minBalance` (whole `BADGE`, `ubadge / 1e9`) | `minBalance` (number, required) | none | yes | yes |
| `satisfies-claim` | The address has at least one success on another claim. Indexed claims check history; on-demand claims re-run the criteria. | `claimIdToCheck` (string, required) | none | yes | yes |
| `username-set` | The address has set a BitBadges username | none | none | yes | yes |
| `url-clicker` | Shows a link the user must open before claiming. The check always passes. | `url` (url, required, rendered as a hyperlink) | none (requires user interaction) | no | no |
| `custom-instructions` | Shows instructions to the user. The check always passes. | `instructions` (string, required) | none (requires user interaction) | no | no |
`must-own-badges` example public params:
```json
{
"ownershipRequirements": {
"$and": [
{
"assets": [
{
"chain": "BitBadges",
"collectionId": "1",
"assetIds": [{ "start": "1", "end": "20" }],
"ownershipTimes": [],
"mustOwnAmounts": { "start": "1", "end": "1" }
}
],
"options": {}
}
]
}
}
```
Nested `satisfies-claim` checks on on-demand claims have a depth limit of 5. Circular references are rejected. Results for `min-badge` are cached briefly (60 seconds on success, 10 seconds on failure) so a simulation and the real attempt share one check.
## Social Account Plugins
| Plugin id | Checks |
| --- | --- |
| `discord` | The user's connected Discord account |
| `github` | The user's connected GitHub account |
| `github-contributions` | The user's GitHub contributions to a repository |
| `google` | The user's connected Google account |
| `twitch` | The user's connected Twitch account |
| `twitter` | The user's connected X (Twitter) account |
`discord`, `github`, `google`, `twitch`, and `twitter` share one shape. The creator lists allowed accounts; the user must be signed in to that provider on BitBadges (the site's **Connections** flow).
```ts
// Public params
type SocialPublicParams = { users: string[]; maxUsesPerUser: number; hasPrivateList: boolean }
// Private params
type SocialPrivateParams = { usernames: string[] } // the allowed list when hasPrivateList is true
// State (per instance)
type SocialState = { ids: { [id: string]: number }; usernames: { [username: string]: number } }
```
`github-contributions` is `Stateless` with `verificationCall.uri` `https://api.bitbadges.io/api/v0/integrations/query/github-contributions` and no creator params in source. Fetch its current schema with `getPlugin('github-contributions')`.
These six run inside the BitBadges service with the user's OAuth session. Their handlers are not part of the open-source indexer repository, so this page documents only the parameter shapes found in source. Use `getPlugin` for the authoritative schema.
## Custom Plugin Contract
A custom plugin is an HTTPS endpoint you register in the developer portal (**Plugins** tab). BitBadges calls it during simulation and execution, and optionally after the claim resolves. The endpoint answers `200` to pass or any other status to fail.
### Request
BitBadges always sends `POST` with a JSON body. Fields from every source are merged into one flat object. The example is a plugin `ticket-check` with one user input (`ticketNumber`), one public param (`eventId`), one private param (`venueSecret`), and `passAddress: true`, running as instance `ticket-gate` in `claim_demo_01`:
```json
{
"ticketNumber": "4821",
"eventId": "demo-launch-2026",
"venueSecret": "vs_9b2d7a4f1e8c5f3a0e6c9b2d7a4f1e8c",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"ethAddress": "0x092bb4851ae26850588243e7bef22a56287f4739",
"isAddressSignedIn": true,
"pluginSecret": "ps_3f9c1e7a2b8d6f4a0c5e9b2d7a4f1e8c",
"version": 0,
"claimId": "claim_demo_01",
"claimAttemptId": "3b9d2f7a1c4e6b8d0f2a4c6e8b1d3f5a",
"instanceId": "ticket-gate",
"_isSimulation": false,
"_attemptStatus": "executing",
"lastUpdated": 1788652800000,
"createdAt": 1788652800000,
"locale": "en"
}
```
| Field | Source | Description |
| --- | --- | --- |
| `ticketNumber`, `eventId`, `venueSecret` | your schemas | `userInputsSchema`, `publicParamsSchema`, `privateParamsSchema`, and `hardcodedInputs`, flattened into the body (or sent as headers when `headerField` is set). |
| `bitbadgesAddress`, `ethAddress`, `isAddressSignedIn` | identity | Only when `verificationCall.passAddress` is true; otherwise `null`. `ethAddress` is derived from `bitbadgesAddress` when the user has no ETH wallet. `isAddressSignedIn` is true when the claiming address is signed in. |
| `pluginSecret` | context | Verify BitBadges is the caller. |
| `version` | context | Plugin version in use. |
| `claimId`, `claimAttemptId`, `instanceId` | context | The claim, the attempt (empty for simulations), and this plugin instance in the claim. |
| `_isSimulation` | context | `true` for a dry run. |
| `_attemptStatus` | context | `executing` during the claim, `success` or `failure` on the status webhook. |
| `lastUpdated`, `createdAt` | context | Claim last updated and created, UNIX ms. |
| `locale` | context | The claiming user's locale. |
Headers:
| Header | Value |
| --- | --- |
| `X-BitBadges-Signature` | HMAC-SHA256 hex of `` `${timestamp}.${JSON.stringify(body)}` `` keyed with your `pluginSecret` |
| `X-BitBadges-Timestamp` | The `timestamp` used in the signature (UNIX ms as a string) |
| `X-BitBadges-Version` | `1` |
| any schema field with `headerField: true` | Sent as a header instead of a body field |
Reserved keys that your schemas cannot use: `pluginSecret`, `version`, `claimId`, `bitbadgesAddress`, `claimAttemptId`, `_isSimulation`, `_isQueueHandler`, `_attemptStatus`, `lastUpdated`, `createdAt`, `email`, `isAddressSignedIn`, `locale`, `instanceId`, `X-BitBadges-Signature`, `X-BitBadges-Timestamp`, `X-BitBadges-Version`.
Constraints on the endpoint:
- Public HTTPS URL that passes SSRF validation (no private or internal addresses). DNS is pinned for the request.
- Calls to `api.bitbadges.io` are blocked to prevent loops.
- Respond within 10 seconds.
- Handle CORS and method yourself. The caller is a server, not a browser.
### Identifying the User
The address is only trustworthy when `isAddressSignedIn` is `true`, which requires the claim (or your plugin's `requireSignIn`) to demand sign in. If you identify users your own way, or do not want to trust BitBadges, issue a one-time code from your service while the user is authenticated with you, collect it as a user input, and verify it in the handler.
### Response
Return `200` and a JSON body that matches the version's `stateFunctionPreset`:
| Preset | Body | Meaning |
| --- | --- | --- |
| `Stateless` | `{}` | Pass. Nothing else is read. |
| `ClaimToken` | `{ "claimToken": "ticket-4821" }` | A one-time token you issued. BitBadges marks it used only if the whole claim succeeds; a failed claim leaves it available for retry. |
| `ClaimNumbers` | `{ "claimNumber": 0 }` | The zero-based claim number to assign. Only one plugin per claim can assign claim numbers. |
| `CustomResponseHandler` | your shape | You manage state and interpretation yourself. |
A `200` is a hypothetical state transition. The claim can still fail on another plugin. Do not commit external state on `200` alone (see state below).
Returned JSON keys must not contain `.` (write `bob@abc[dot]com`, not `bob@abc.com`). This is a storage constraint.
### Errors
Return a non-200 status with a `message` field:
```json
{ "message": "Ticket 4821 was already redeemed" }
```
BitBadges stores the error for debugging (`getPluginErrors`) and may show it to the claiming user or the creator. Be informative without revealing secrets or internal details.
### Simulations
BitBadges simulates before execution. Detect a dry run with `_isSimulation === true` (and an empty `claimAttemptId`). Validate inputs and preconditions, return the status you would return for real, and do not mutate state, send notifications, or consume tokens. Set `ignoreSimulations: true` in the version config to skip the simulation call entirely; the plugin then auto-passes simulation and is only called at execution.
For a custom plugin, the execution step trusts the simulation result and replays it through the state handler. Your endpoint is called once per attempt, not twice.
### Status Webhook
With `receiveStatusWebhook: true`, BitBadges POSTs the same payload to your endpoint after the claim resolves with `_attemptStatus: 'success'` or `'failure'`. Retries use exponential backoff: base delay 1 hour, `2^retries * base`, maximum 7 days (1h, 2h, 4h, 8h, 16h, and so on). Make the handler idempotent and deduplicate on `claimAttemptId`.
### State Rules
- All plugins in a claim run in parallel against the state as it was before the attempt. A plugin cannot depend on another plugin's state change in the same attempt.
- Attempts process through a queue. Live claim totals may be stale by the time your handler runs. Your own params and the context fields are safe to depend on.
- Prefer BitBadges-managed state (`Stateless`, `ClaimToken`, `ClaimNumbers`). Updates commit only when your plugin returned `200` and the whole claim succeeded.
- For self-managed state, verify the outcome first with `getClaimAttemptStatus(claimAttemptId)` or the status webhook, and use idempotency keys because attempts run concurrently.
### On-Demand Compatibility
To work with on-demand (non-indexed) claims, set `reuseForNonIndexed: true`. The plugin must be stateless, take no user inputs, and work from the address and hardcoded params alone.
## Plugin Version Config
```ts
interface PluginVersionConfig {
version: number; // auto-incrementing
finalized: boolean; // immutable and usable by others once true
stateFunctionPreset: 'Stateless' | 'ClaimToken' | 'ClaimNumbers' | 'CustomResponseHandler';
duplicatesAllowed: boolean;
requiresSessions: boolean;
requiresUserInputs: boolean;
reuseForNonIndexed: boolean;
receiveStatusWebhook: boolean;
skipProcessingWebhook?: boolean;
ignoreSimulations?: boolean;
requireSignIn?: boolean;
userInputsSchema: JsonBodyInputSchema[];
publicParamsSchema: JsonBodyInputSchema[];
privateParamsSchema: JsonBodyInputSchema[];
verificationCall?: {
uri: string;
hardcodedInputs: JsonBodyInputWithValue[];
passAddress?: boolean;
};
customDetailsDisplay?: string; // "Requires {{minBalance}} tokens"
}
```
| Field | Description |
| --- | --- |
| `finalized` | Locks schemas, URL, and settings. Unfinalized versions are usable only by the plugin creator (Claim Tester). |
| `duplicatesAllowed` | A claim may include several instances of this plugin with different params. |
| `requiresSessions` | The claim needs an active BitBadges session. |
| `requiresUserInputs` | The user must fill a form at claim time. BitBadges renders it from `userInputsSchema`. |
| `reuseForNonIndexed` | Compatible with on-demand claims. Requires stateless, no user inputs. |
| `receiveStatusWebhook` | Receive the final outcome POST described above. |
| `skipProcessingWebhook` | Auto-pass without calling the endpoint. Used by display-only plugins such as `custom-instructions`. |
| `ignoreSimulations` | Do not call the endpoint during dry runs. |
| `requireSignIn` | The user must be signed in before this plugin runs. |
| `verificationCall.uri` | Your handler URL. |
| `verificationCall.hardcodedInputs` | Static key-value pairs included in every request (body or header per `headerField`). Useful for API keys. |
| `verificationCall.passAddress` | Include `bitbadgesAddress`, `ethAddress`, and `isAddressSignedIn` in the payload. |
| `customDetailsDisplay` | Template shown to users in the claim UI. `{{key}}` references public param keys. |
### Schema Fields
Each entry in a schema array describes one input:
```ts
interface JsonBodyInputSchema {
key: string; // field name in the payload
label: string; // shown to the person filling it
type: string; // 'string' | 'number' | 'boolean' | 'date' | 'url' (date values are UNIX ms)
required?: boolean;
defaultValue?: string | number | boolean;
helper?: string;
options?: { label: string; value: string | number | boolean }[]; // renders a dropdown
arrayField?: boolean; // multiple values
headerField?: boolean; // send as an HTTP header instead of a body field
hideFromDetailsDisplay?: boolean; // public params only: hide from the public view
hyperlink?: { url: string; showAsGenericView?: boolean };
}
```
The `ticket-check` plugin from the request example declares its three inputs like this:
```json
{
"userInputsSchema": [
{ "key": "ticketNumber", "label": "Ticket number", "type": "string", "required": true, "helper": "Printed on your ticket" }
],
"publicParamsSchema": [
{ "key": "eventId", "label": "Event", "type": "string", "required": true }
],
"privateParamsSchema": [
{ "key": "venueSecret", "label": "Venue secret", "type": "string", "required": true }
]
}
```
BitBadges-created plugins also use the type `ownershipRequirements`. Custom plugins use the five listed types.
Versioning: a new version starts unfinalized. Finalize it to make it immutable and usable by others. A claim keeps the version it was created with, even after you publish newer ones. Your handler receives `version` and `createdAt` in every request to branch on if needed. Private plugins (default) can be added only by you; published plugins appear in the directory for everyone.
## Internal Plugin Interface
Core plugins implement the same shape internally. Custom plugins get the same atomicity: `toSet` updates apply only if the whole claim succeeds.
```ts
interface BackendIntegrationPlugin
{
pluginId: P;
metadata: {
name: string;
description: string;
image: string;
createdBy: string;
stateless: boolean;
scoped: boolean; // state scoped to the claim instance
duplicatesAllowed: boolean;
mandatoryToSucceed?: boolean; // cannot be made optional via success logic
toSkipIfSuccessfulSimulation: boolean;
};
validate: (args: {
context: ContextInfo & { instanceId: string; pluginId: string; version: string };
publicParams: PublicParamsType;
privateParams: PrivateParamsType;
customBody?: UserInputType;
priorState?: any;
adminInfo?: any;
simulationResult?: CachedPluginResult;
}) => Promise<{
success: boolean;
error?: string;
toSet?: object[]; // state updates, applied only if the claim succeeds
data?: any;
claimNumber?: number; // when this plugin assigns claim numbers
apiCall?: { uri: string; method: string; body: object; headers: object };
}>;
defaultState: any;
getPublicState: (currState: any) => PublicStateType;
getBlankPublicState: () => PublicStateType;
}
interface ContextInfo {
locale: string;
bitbadgesAddress: string;
ethAddress: string;
isAddressSignedIn: boolean;
claimId: string;
_isQueueHandler: boolean; // internal: processing from the queue
_isSimulation: boolean;
_attemptStatus: string;
lastUpdated: number;
createdAt: number;
claimAttemptId: string;
assignMethod: string; // claim number assignment method
isClaimNumberAssigner: boolean;
}
```
## Related
- [Build a Claim Plugin](../../guides/build-a-claim-plugin.md)
- [Claims](README.md)
- [Endpoints](endpoints.md)
- [Dynamic Stores](dynamic-stores.md)
## File: api/pagination-and-views.md
---
description: "Bookmark pagination and the views object in BitBadges API responses, with the SDK helpers that page through them."
---
# Pagination and Views
Paginated routes return a `bookmark` and `hasMore`. Pass the bookmark back to get the next page. Some routes (accounts, collections) group several paginated lists into one `views` object.
See the [API reference](/api-reference) for every route's request and response schema.
The TypeScript snippets use the configured `BitBadgesApi` client from the [API setup example](README.md#example).
## Example
```bash
# First page: empty bookmark. Create an API key at https://bitbadges.io/developer
curl -X POST https://api.bitbadges.io/api/v0/collections \
-H "Content-Type: application/json" -H "x-api-key: $BITBADGES_API_KEY" \
-d '{ "collectionsToFetch": [ { "collectionId": "1",
"viewsToFetch": [ { "viewType": "owners", "viewId": "owners", "bookmark": "" } ] } ] }'
```
```ts
const res = await BitBadgesApi.getCollections({
collectionsToFetch: [
{
collectionId: '1',
viewsToFetch: [{ viewType: 'owners', viewId: 'owners', bookmark: '' }]
}
]
});
const collection = res.collections[0];
if (!collection) throw new Error('Collection not found');
const page1 = collection.getOwnersView('owners');
// Next page
await collection.fetchNextForView(BitBadgesApi, 'owners', 'owners');
const page2 = collection.getOwnersView('owners');
```
## How Bookmark Pagination Works
1. First request: send an empty bookmark (`""`).
2. Each response includes the data, a `bookmark` for the next page, and a `hasMore` boolean.
3. Next request: send the `bookmark` from the previous response.
4. Stop when `hasMore` is `false`.
The `views` and `owners` parts of the collection response above, for a page with two owners (synthesized from the SDK types; the rest of the collection document is omitted here):
```json fold=21-33,46-58
{
"views": {
"owners": {
"ids": ["1:bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d", "1:bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"],
"type": "owners",
"pagination": { "bookmark": "eyJza2lwIjoyNX0", "hasMore": true }
}
},
"owners": [
{
"_docId": "1:bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "1",
"bitbadgesAddress": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"balances": [
{
"amount": "1",
"tokenIds": [{ "start": "1", "end": "50" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
],
"incomingApprovals": [],
"outgoingApprovals": [],
"userPermissions": {
"canUpdateOutgoingApprovals": [],
"canUpdateIncomingApprovals": [],
"canUpdateAutoApproveSelfInitiatedOutgoingTransfers": [],
"canUpdateAutoApproveSelfInitiatedIncomingTransfers": [],
"canUpdateAutoApproveAllIncomingTransfers": []
},
"autoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedIncomingTransfers": true,
"autoApproveAllIncomingTransfers": false,
"updateHistory": []
},
{
"_docId": "1:bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"collectionId": "1",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"balances": [
{
"amount": "1",
"tokenIds": [{ "start": "51", "end": "51" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}
],
"incomingApprovals": [],
"outgoingApprovals": [],
"userPermissions": {
"canUpdateOutgoingApprovals": [],
"canUpdateIncomingApprovals": [],
"canUpdateAutoApproveSelfInitiatedOutgoingTransfers": [],
"canUpdateAutoApproveSelfInitiatedIncomingTransfers": [],
"canUpdateAutoApproveAllIncomingTransfers": []
},
"autoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedIncomingTransfers": true,
"autoApproveAllIncomingTransfers": false,
"updateHistory": []
}
]
}
```
Routes that take a `bookmark` directly (search, claim attempts, plugin errors) use the same rule without the `views` wrapper. Check the [API reference](/api-reference) for each route.
## The Views Object
{% hint style="info" %}
The `views` object is planned for deprecation in favor of dedicated per-view routes. Prefer those routes when one exists.
{% endhint %}
Collections and accounts carry a `views` map keyed by `viewId`:
```ts
type Views = {
[viewId: string]: {
ids: string[]; // document IDs in this page
type: string; // the view type
pagination: {
bookmark: string;
hasMore: boolean;
};
} | undefined;
}
```
| Field | Description |
| --- | --- |
| `viewId` | Your identifier for the view. Reuse the same `viewId` when paging the same dataset. |
| `ids` | `_docId` values. Map them to the full documents in the matching response array. |
| `type` | The view type, for example `owners`. |
| `pagination` | `bookmark` and `hasMore` for the next request. |
Documents live in the response array for their type (`activity`, `owners`, and so on). Map `ids` to documents by `_docId`:
```ts
const activity = collection.views['activity']?.ids.map(
(id) => collection.activity.find((entry) => entry._docId === id)
) ?? [];
```
### View Types
| Interface | `viewType` values (`CollectionViewKey` / `AccountViewKey`) |
| --- | --- |
| Collection | `transferActivity`, `owners`, `amountTrackers`, `challengeTrackers`, `listings`, `tokenFloorPrices` |
| Account | `siwbbRequests`, `transferActivity`, `tokensCollected`, `createdTokens`, `managingTokens`, `publicClaimActivity`, `allClaimActivity`, `pointsActivity` |
## SDK Helpers
`BitBadgesCollection` and `BitBadgesUserInfo` wrap the bookkeeping:
```ts
collection.viewHasMore('owners'); // boolean, true when unknown
collection.getViewPagination('owners'); // { bookmark, hasMore }
collection.getViewBookmark('owners'); // string
await collection.fetchNextForView(BitBadgesApi, 'owners', 'owners');
await collection.fetchAllForView(BitBadgesApi, 'owners', 'owners'); // all pages, 1 s between pages
collection.getView('owners', 'owners'); // typed by viewType
collection.getOwnersView('owners');
collection.getActivityView('activity');
collection.getChallengeTrackersView('challengeTrackers');
```
`fetchNextForView` accepts optional `oldestFirst` and `address` arguments for views that support them.
## Rules of Thumb
- Keep `viewId` stable while paging one dataset.
- Check for an undefined view before reading it.
- Track both `bookmark` and `hasMore`.
- Each response only contains that request's page. Merge pages yourself or use the helpers, which merge into the object.
## Related
- [BitBadges API](README.md)
- [SDK](../sdk/README.md)
## File: api/sign-in/README.md
---
description: "Sign In with BitBadges (SIWBB) is an OAuth 2.0 flow that proves address ownership and grants your app BitBadges API scopes."
---
# Sign In with BitBadges
Sign In with BitBadges (SIWBB) is an OAuth 2.0 provider. A user proves they own an address on BitBadges, your app receives an authorization code, and you exchange it for the address plus access and refresh tokens. Use it when you need authenticated BitBadges API access on a user's behalf, or when you want one flow that combines address ownership with claim criteria.
See the [API reference](/api-reference) for every route's request and response schema.
## Example
```ts
import crypto from 'crypto';
import { BigIntify, BitBadgesAPI, generateBitBadgesAuthUrl } from 'bitbadges';
const BitBadgesApi = new BitBadgesAPI({ apiKey: process.env.BITBADGES_API_KEY, convertFunction: BigIntify }); // key from https://bitbadges.io/developer
// 1. Send the user here. Store `state` in the session so the callback can check it.
const state = crypto.randomBytes(16).toString('hex');
const authUrl = generateBitBadgesAuthUrl({
client_id: 'app_demo_01',
redirect_uri: 'https://example.com/api/callback',
state,
scope: 'completeClaims,readPrivateClaimData' // optional
});
// 2. At the redirect URI, exchange the code
const res = await BitBadgesApi.exchangeSIWBBAuthorizationCode({
code: req.query.code as string,
grant_type: 'authorization_code',
client_id: 'app_demo_01',
client_secret: process.env.SIWBB_CLIENT_SECRET,
redirect_uri: 'https://example.com/api/callback'
});
const { address, chain, bitbadgesAddress, verificationResponse, access_token } = res;
if (!verificationResponse?.success) throw new Error('Not authenticated');
```
```bash
curl -X POST https://api.bitbadges.io/api/v0/siwbb/token \
-H "Content-Type: application/json" -H "x-api-key: $BITBADGES_API_KEY" \
-d '{ "code": "9c1f4e2b7a6d5c4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e",
"grant_type": "authorization_code",
"client_id": "app_demo_01", "client_secret": "'"$SIWBB_CLIENT_SECRET"'",
"redirect_uri": "https://example.com/api/callback" }'
```
```json
{
"address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"chain": "Cosmos",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"verificationResponse": { "success": true },
"access_token": "siwbb_at_7d2e9f4a1b6c3d8e5f0a2b9c4d7e1f6a",
"token_type": "Bearer",
"access_token_expires_at": "1788825600000",
"refresh_token": "siwbb_rt_2b9c4d7e1f6a7d2e9f4a1b6c3d8e5f0a",
"refresh_token_expires_at": "1793923200000"
}
```
{% hint style="info" %}
**Ask your agent.** With the MCP builder tools connected, a prompt like this works:
```text
Check whether bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue owns at least one token from collection 1, then tell me which Sign In with BitBadges scopes my backend needs to complete claims on that user's behalf.
```
The agent calls `verify_ownership` and `fetch_docs` and answers with the scope names to put in the authorization URL.
{% endhint %}
## OAuth Endpoints
| Endpoint | URL |
| --- | --- |
| Authorization | `https://bitbadges.io/siwbb/authorize?` |
| Token | `https://api.bitbadges.io/api/v0/siwbb/token` |
| Revoke | `https://api.bitbadges.io/api/v0/siwbb/token/revoke` |
[Demo](https://bitbadges.io/siwbb/authorize?client_id=example-client-id&redirect_uri=https://example.com&)
## What Is in This Section
| Page | Read it when |
| --- | --- |
| [Setup](setup.md) | You register an OAuth app and get a client ID and secret. |
| [Authorization URL](authorization-url.md) | You build the URL the user visits, with scopes and an optional claim. |
| [Callback](callback.md) | You handle the redirect with `code` and `state`. |
| [Verification](verification.md) | You exchange the code, manage access and refresh tokens, and know what is and is not verified. |
| [Frameworks](frameworks.md) | You integrate through Auth0, WordPress, Supabase, Discourse, or Passport.js. |
The short guide version is [Sign In Users](../../guides/sign-in-users.md). The CLI has the same flow for terminals and agents under `bb auth` (`login`, `challenge`, `verify`, `status`, `logout`), see [CLI auth](../../cli/auth.md).
## Flow
1. **BitBadges side.** The user opens your authorization URL (direct link or popup), proves address ownership, meets any extra criteria you attached (for example a claim), and receives an authorization code. With a redirect URI the code goes to your callback; without one it is shown as a QR code and stored in the user's account for delayed or in-person flows.
2. **Your side.** Receive the code, exchange it for the address and tokens, check any other criteria server-side, and apply your own security (state validation, replay protection). Then get on with your app.
## When to Use Something Else
SIWBB exists mainly for OAuth authorization of the BitBadges API. If you only need Web3 login and never call the API on the user's behalf, a general Web3 auth provider (WalletConnect, Magic, and others) works too. You can still combine any authentication with claim checks:
```ts
// 1. Authenticate the user with your existing setup, which yields `address`
// 2. Verify claim success through the API
const res = await BitBadgesApi.checkClaimSuccess('claim_demo_01', address);
if (res.successCount < 1) throw new Error('Claim not completed');
```
## Related
- [Setup](setup.md)
- [Sign In Users](../../guides/sign-in-users.md)
- [Claims](../claims/README.md)
- [API reference](/api-reference)
## File: api/sign-in/authorization-url.md
---
description: "Build the Sign In with BitBadges authorization URL. Parameters, scopes, attaching a claim, and the SDK helper that generates it."
---
# Authorization URL
The user authenticates at `https://bitbadges.io/siwbb/authorize` with your parameters in the query string. On success they receive an authorization code, delivered to your redirect URI (digital flows) or shown as a QR code (in-person and delayed flows).
See the [API reference](/api-reference) for every route's request and response schema.
## Example
```ts
import crypto from 'crypto';
import { generateBitBadgesAuthUrl, CodeGenQueryParams } from 'bitbadges';
const state = crypto.randomBytes(16).toString('hex'); // store it in the session for the callback check
const params: CodeGenQueryParams = {
client_id: 'app_demo_01',
redirect_uri: 'https://example.com/api/callback',
state,
scope: 'completeClaims,readPrivateClaimData',
claimId: 'claim_demo_01',
hideIfAlreadyClaimed: true,
expectVerifySuccess: true
};
const authUrl = generateBitBadgesAuthUrl(params);
```
The generated URL, with `state` set to `f3a9c2e1b7d4a6c8`:
```bash
https://bitbadges.io/siwbb/authorize?client_id=app_demo_01&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fcallback&state=f3a9c2e1b7d4a6c8&scope=completeClaims%2CreadPrivateClaimData&claimId=claim_demo_01&hideIfAlreadyClaimed=true&expectVerifySuccess=true
```
Three ways to produce the URL:
- **Developer portal (recommended).** Open your app and click **Create SIWBB URL**.
- **Link generator.** [https://bitbadges.io/auth/linkgen](https://bitbadges.io/auth/linkgen) also lists every available scope.
- **SDK.** `generateBitBadgesAuthUrl` from `bitbadges`, shown above. Object values are JSON-encoded and URI-escaped; falsy values are skipped.
## Parameters
```ts
interface CodeGenQueryParams {
client_id: string;
redirect_uri?: string;
state?: string;
scope?: string;
claimId?: string;
hideIfAlreadyClaimed?: boolean;
expectVerifySuccess?: boolean;
}
```
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `client_id` | string | yes | Your app's client ID from the developer portal. |
| `redirect_uri` | string | for instant auth | Must match a registered redirect URI. Omit it for QR code or delayed flows; the code is then generated and stored in the user's account. |
| `state` | string | no | Opaque value passed back to your redirect URI. Use it for CSRF protection. |
| `scope` | string | no | Comma-separated BitBadges API scopes, for example `completeClaims,readClaimAlerts`. Omit it to verify address ownership only. |
| `claimId` | string | no | Show this claim on the authorize screen. Display only. Verify the claim yourself after authentication. |
| `hideIfAlreadyClaimed` | boolean | no | Hide the claim when the user already has a success (`successCount >= 1`). |
| `expectVerifySuccess` | boolean | no | Block sign in unless the claim verification succeeds. Users can edit URL parameters, so this is not a replacement for a server-side check. |
## Scopes
Scopes are only needed for authenticated API access on the user's behalf. Without scopes you still get address ownership and access to the health check route. Scope names are the camelCase form of the labels below (`completeClaims` for `Complete Claims`).
| Scope | Grants |
| --- | --- |
| `Full Access` | Full access to all features. |
| `Report` | Report users or collections. |
| `Read Profile` | Read private profile information: email, approved sign-in methods, connections. |
| `Manage Applications` | Create, update, and delete applications. |
| `Manage Utility Pages` | Create, update, and delete utility pages. |
| `Approve Sign In With BitBadges Requests` | Sign In with BitBadges on behalf of the user. |
| `Read Authentication Codes` | Read authentication codes. |
| `Delete Authentication Codes` | Delete authentication codes. |
| `Manage Claims` | Create, update, and delete claims. |
| `Manage Developer Apps` | Create, update, and delete developer apps. |
| `Manage Dynamic Stores` | Create, update, and delete dynamic stores. |
| `Read Private Claim Data` | Read private claim data (codes, passwords, private lists). |
| `Complete Claims` | Complete claims on behalf of the user. |
The [API reference](/api-reference) states the scope each route requires.
## Claims
A `claimId` attaches a claim to the authorize screen. Create the claim in the developer portal. Claims can require anything: token ownership, a payment, a code. They are not part of the core authentication step. After the code exchange, verify the claim server-side with `checkClaimSuccess`. See [Claims](../claims/README.md).
## Related
- [Callback](callback.md)
- [Verification](verification.md)
- [Setup](setup.md)
## File: api/sign-in/callback.md
---
description: "Handle the Sign In with BitBadges redirect callback. Read code and state from the query string, then exchange the code."
---
# Callback
With a `redirect_uri` in the authorization URL, BitBadges sends the user back to your app with `code` and `state` as query parameters as soon as authentication finishes. The user never sees the code.
See the [API reference](/api-reference) for every route's request and response schema.
## Example
A Next.js API route at `https://example.com/api/callback`. BitBadges redirects to `/api/callback?code=9c1f4e2b7a6d5c4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e&state=f3a9c2e1b7d4a6c8`.
```ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { BigIntify, BitBadgesAPI } from 'bitbadges';
import { getSession } from '../../lib/session'; // your own session helper
const BitBadgesApi = new BitBadgesAPI({ apiKey: process.env.BITBADGES_API_KEY, convertFunction: BigIntify }); // key from https://bitbadges.io/developer
export default async function callbackHandler(req: NextApiRequest, res: NextApiResponse) {
const { code, state } = req.query;
if (typeof code !== 'string' || typeof state !== 'string') {
return res.status(400).json({ error: 'Expected one code and one state' });
}
const session = await getSession(req, res);
// 1. Validate state against the value you issued when building the authorization URL
if (!state || state !== session.siwbbState) {
return res.status(400).json({ error: 'Invalid state' });
}
delete session.siwbbState;
await session.save();
// 2. Exchange the code (see Verification)
const auth = await BitBadgesApi.exchangeSIWBBAuthorizationCode({
code,
grant_type: 'authorization_code',
client_id: 'app_demo_01',
client_secret: process.env.SIWBB_CLIENT_SECRET,
redirect_uri: 'https://example.com/api/callback'
});
if (!auth.verificationResponse?.success) {
return res.status(401).json({ error: auth.verificationResponse?.errorMessage ?? 'Not authenticated' });
}
// 3. Apply your own checks and start a session
session.bitbadgesAddress = auth.bitbadgesAddress;
session.accessToken = auth.access_token;
session.refreshToken = auth.refresh_token;
await session.save();
return res.redirect('/dashboard');
}
```
## How It Works
1. The user opens your authorization URL.
2. BitBadges walks them through authentication. On completion it redirects to your `redirect_uri` with `code` and `state`.
3. Your handler exchanges the code with your client secret and reads the authentication details. See [Verification](verification.md).
This is the standard OAuth 2.0 authorization code flow. Any OAuth tutorial applies.
## Requirements
- The `redirect_uri` in the URL must match a registered URI exactly. See [Setup](setup.md).
- Validate `state` to reject requests you did not start.
- Serve the callback over HTTPS.
Without a redirect URI, the code is shown as a QR code and stored in the user's account for in-person or delayed use. That path skips this page and goes straight to [Verification](verification.md).
## Related
- [Verification](verification.md)
- [Authorization URL](authorization-url.md)
## File: api/sign-in/frameworks.md
---
description: "Use Sign In with BitBadges through Auth0, WordPress, Supabase, Discourse, Passport.js, and other frameworks that already speak OAuth 2.0."
---
# Frameworks
Sign In with BitBadges is a standard OAuth 2.0 provider, so most frameworks connect to it with configuration rather than code. This page collects the ready-made integrations.
See the [API reference](/api-reference) for every route's request and response schema.
## Auth0
Auth0 lists BitBadges as a preconfigured social connection: [https://marketplace.auth0.com/integrations/bitbadges](https://marketplace.auth0.com/integrations/bitbadges). Enter your client ID and client secret, then register your Auth0 callback as a redirect URI in the developer portal. It ends with `/callback`, for example `https://dev-pgv803tz4ztg35oi.us.auth0.com/login/callback`.
### Custom Connection
A custom connection lets you add parameters beyond the standard flow. The callback is the same.
| Setting | Value |
| --- | --- |
| Authorization URL | `https://bitbadges.io/siwbb/authorize` |
| Token URL | `https://api.bitbadges.io/api/v0/siwbb/token` |
| Scopes | None needed for the address. Add scopes as required. |
| Fields | API key, client ID, and client secret from [https://bitbadges.io/developer](https://bitbadges.io/developer) |
| Custom headers | `{ "x-api-key": "api_demo_0123456789abcdef" }` (your own key from the developer portal) |
Fetch profile script. Put your own API key in both the custom headers and the script; `api_demo_0123456789abcdef` below stands in for it:
```js
function fetchUserProfile(accessToken, ctx, cb) {
request.post(
{
url: 'https://api.bitbadges.io/api/v0/auth/status',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'api_demo_0123456789abcdef',
Authorization: 'Bearer ' + accessToken
}
},
(err, resp, body) => {
if (err) {
return cb(err);
}
if (resp.statusCode !== 200) {
return cb(new Error(body));
}
let bodyParsed;
try {
bodyParsed = JSON.parse(body);
} catch (jsonError) {
return cb(new Error('Failed JSON parsing for user profile response.'));
}
const account = bodyParsed;
const profile = {
address: account.address,
chain: account.chain,
id: account.bitbadgesAddress,
name: account.address
};
return cb(null, profile);
}
);
}
```
## WordPress
The **Sign in with BitBadges** plugin adds a "Sign in with BitBadges" button to the WordPress login form and can gate the site on a claim.
- Directory: [https://wordpress.org/plugins/sign-in-with-bitbadges/](https://wordpress.org/plugins/sign-in-with-bitbadges/)
- Source: [https://github.com/BitBadges/bitbadges-wp-plugin](https://github.com/BitBadges/bitbadges-wp-plugin)
The plugin handles authentication only. It does not restrict access by itself. Pair it with [Force Login](https://wordpress.org/plugins/wp-force-login/), [Restrict Content](https://wordpress.org/plugins/restrict-content/), [Members](https://wordpress.org/plugins/members/), or another access-control plugin.
### Install and Configure
1. Upload the plugin files to `/wp-content/plugins/` and activate the plugin from the **Plugins** menu.
2. Create an OAuth app in the [developer portal](https://bitbadges.io/developer).
3. Set the redirect URI to `https://example.com/wp-login.php?action=bitbadges-callback`, with your own WordPress domain in place of `example.com`.
4. In WordPress open **Settings** then **BitBadges SIWBB**. Enter the client ID and client secret.
5. Optionally set a claim ID to gate access, then save.
### Features
- Creates WordPress users automatically on first sign in.
- OAuth 2.0 with `state` verification, WordPress nonce checks, input sanitization, and secure credential storage.
- Optional exclusive mode that disables the normal WordPress login, with an emergency admin access URL.
- Claim-gated access and configurable claim visibility on the authorization page.
- Shortened wallet addresses as display names.
Requirements: WordPress 5.0 or higher, PHP 7.0 or higher, HTTPS. License: GPL v2 or later. Version 1.0.0 is the initial release with the features above.
## Supabase
[https://github.com/BitBadges/bitbadges-supabase-demo](https://github.com/BitBadges/bitbadges-supabase-demo) is a starting point, not production code. The repository is archived. It uses the Next.js and Supabase template with normal Supabase authentication, then adds Sign In with BitBadges on top, which gives you a username to address mapping.
Setup:
1. Set up Supabase per its documentation.
2. Add your BitBadges API key, client ID, and client secret to `.env`.
3. Run the SQL in `supabase/migrations` from the Supabase SQL editor to create the tables.
Left to you: gating the whole site instead of using traditional auth, gating specific pages, and linking claims. Supabase is flexible; Express, Auth0, or another route works as well. Supabase also supports Auth0 as a provider: [https://supabase.com/partners/integrations/auth0](https://supabase.com/partners/integrations/auth0).
## Discourse
There is no dedicated Discourse plugin. Because SIWBB is OAuth2 compatible, reuse an existing plugin:
- **Through Auth0.** Set up the Auth0 connection above, then follow [Configure sign up and log in with Auth0 using the OAuth2 Basic plugin](https://meta.discourse.org/t/configure-sign-up-and-log-in-with-auth0-using-the-oauth2-basic-plugin/64633).
- **Direct.** Use the [Discourse OAuth2 Basic plugin](https://meta.discourse.org/t/discourse-oauth2-basic/33879) with the endpoints from [Sign In with BitBadges](README.md).
## Express and Passport.js
[https://github.com/BitBadges/bitbadges-passportjs-example](https://github.com/BitBadges/bitbadges-passportjs-example) is a Passport.js integration example for Express. The whole example lives in its `index.ts`.
## Others
Frameworks that support Auth0 can use Sign In with BitBadges through it:
- Auth.js (NextAuth): [https://authjs.dev/getting-started/providers/auth0](https://authjs.dev/getting-started/providers/auth0)
- Amazon Cognito: [https://auth0.com/docs/customize/integrations/aws/amazon-cognito#configure-amazon-web-services](https://auth0.com/docs/customize/integrations/aws/amazon-cognito#configure-amazon-web-services)
- Supabase: [https://supabase.com/partners/integrations/auth0](https://supabase.com/partners/integrations/auth0)
You are not locked into Sign In with BitBadges. Any Web3 authentication provider plus a claim success check gates anything. Suggestions for other integrations are welcome.
## Related
- [Sign In with BitBadges](README.md)
- [Verification](verification.md)
- [Claims](../claims/README.md)
## File: api/sign-in/setup.md
---
description: "Register an OAuth app in the BitBadges developer portal and get the client ID, client secret, and redirect URIs that Sign In with BitBadges needs."
---
# Setup
Register an OAuth app before building an authorization URL. Registration gives you a client ID and client secret and records the redirect URIs your app may use.
See the [API reference](/api-reference) for every route's request and response schema.
## Register the App
1. Open [https://bitbadges.io/developer](https://bitbadges.io/developer) and choose **OAuth Apps**.
2. Create an app and add your redirect URIs.
3. Save the client ID and client secret.
## Components
| Component | Purpose | Rules |
| --- | --- | --- |
| Client ID | Public identifier for your app. Goes in the authorization URL. | Assigned at registration. |
| Client secret | Proves your backend is the app when exchanging codes. | Keep it server-side. Treat it like a password. Never ship it in client code. |
| Redirect URIs | Where the user lands after authenticating, carrying `code` and `state`. | Pre-registered, HTTPS, and an exact match with the `redirect_uri` you send. Not needed for delayed or QR code flows. |
## Security
- The client secret is required to fetch a user's authentication details. Anyone holding it can act as your app.
- Registered and used redirect URIs must match exactly.
- Use HTTPS for every redirect.
## Related
- [Authorization URL](authorization-url.md)
- [Sign In with BitBadges](README.md)
## File: api/sign-in/verification.md
---
description: "Exchange a Sign In with BitBadges code for the address and tokens, manage access and refresh tokens, and know what is verified natively and what is not."
---
# Verification
Exchange the authorization code (a 32-byte hex string) at the token endpoint. The response carries the address, a `verificationResponse`, and access and refresh tokens for session management and authorized API calls.
See the [API reference](/api-reference) for every route's request and response schema.
## Example
```bash
# Create an API key at https://bitbadges.io/developer
curl -X POST https://api.bitbadges.io/api/v0/siwbb/token \
-H "Content-Type: application/json" -H "x-api-key: $BITBADGES_API_KEY" \
-d '{
"code": "9c1f4e2b7a6d5c4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e",
"grant_type": "authorization_code",
"client_id": "app_demo_01",
"client_secret": "'"$SIWBB_CLIENT_SECRET"'",
"redirect_uri": "https://example.com/api/callback",
"options": { "issuedAtTimeWindowMs": 600000 }
}'
```
```ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { BigIntify, BitBadgesAPI } from 'bitbadges';
const BitBadgesApi = new BitBadgesAPI({ apiKey: process.env.BITBADGES_API_KEY, convertFunction: BigIntify });
async function myHandler(req: NextApiRequest, res: NextApiResponse) {
// Call only after validating and consuming OAuth state, as shown on the Callback page.
const code = req.query.code as string;
const auth = await BitBadgesApi.exchangeSIWBBAuthorizationCode({
code,
options: { issuedAtTimeWindowMs: 1000 * 60 * 10 }, // 10 minutes (default). 0 disables the check.
grant_type: 'authorization_code',
client_secret: process.env.SIWBB_CLIENT_SECRET,
client_id: 'app_demo_01',
redirect_uri: 'https://example.com/api/callback' // only when the code was created with a redirect URI
});
const { address, chain, bitbadgesAddress, verificationResponse } = auth;
if (!verificationResponse?.success) {
console.log(verificationResponse?.errorMessage);
throw new Error('Not authenticated');
}
const { access_token, access_token_expires_at, refresh_token, refresh_token_expires_at } = auth;
// Session management and authorized API access
// The address is now proven. Check anything else you require. For example, a claim:
const claim = await BitBadgesApi.checkClaimSuccess('claim_demo_01', bitbadgesAddress);
if (claim.successCount < 1) throw new Error('Claim not completed');
// Or token ownership:
const ownership = await BitBadgesApi.verifyOwnershipRequirements({
address: bitbadgesAddress,
assetOwnershipRequirements: {
$and: [
{
assets: [
{
chain: 'BitBadges',
collectionId: '1',
assetIds: [{ start: '1', end: '100' }],
ownershipTimes: [],
mustOwnAmounts: { start: '1', end: '1' }
}
],
options: {}
}
]
}
});
if (!ownership.success) throw new Error(ownership.errorMessage ?? 'Ownership check failed');
// Other checks to consider:
// - Replay protection: timestamps or nonces
// - Flash ownership: is the qualifying asset transferable? one use per asset?
// - Allowlist or denylist of addresses that may sign in
// - Cache anything you need later
res.status(200).json({ address, chain, bitbadgesAddress });
}
```
## Request
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | for `authorization_code` | The authorization code. |
| `grant_type` | `'authorization_code'` or `'refresh_token'` | yes | Which exchange to perform. |
| `client_id` | string | yes | Your app's client ID. |
| `client_secret` | string | yes | Your app's client secret. Server-side only. |
| `redirect_uri` | string | when used | Required only if the code was created with a redirect URI. |
| `refresh_token` | string | for `refresh_token` | The refresh token to rotate. |
| `options.issuedAtTimeWindowMs` | number | no | How recent the code must be. Default 10 minutes. `0` disables the check, which in-person flows often need. |
| `code_verifier` | string | no | PKCE code verifier, when you used PKCE. |
## Response
```ts
type AuthorizationResponse = {
address: string;
chain: SupportedChain; // 'Cosmos' | 'ETH' | 'Unknown'
bitbadgesAddress: string;
verificationResponse?: { success: boolean; errorMessage?: string };
access_token: string;
token_type: 'Bearer';
access_token_expires_at?: T; // UNIX ms; string over HTTP
refresh_token?: string;
refresh_token_expires_at?: T; // converted by the SDK
}
```
```json
{
"address": "0x092bb4851ae26850588243e7bef22a56287f4739",
"chain": "ETH",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"verificationResponse": { "success": true },
"access_token": "siwbb_at_7d2e9f4a1b6c3d8e5f0a2b9c4d7e1f6a",
"token_type": "Bearer",
"access_token_expires_at": "1788825600000",
"refresh_token": "siwbb_rt_2b9c4d7e1f6a7d2e9f4a1b6c3d8e5f0a",
"refresh_token_expires_at": "1793923200000"
}
```
`address` is the address the user signed in with (an `0x` address for an Ethereum wallet). `bitbadgesAddress` is its `bb1` form; use it as the unique identifier so the same user cannot sign in twice through the two forms. Each code can be exchanged once. BitBadges enforces this.
## Access Tokens
Send the access token as `Authorization: Bearer ` on authenticated routes. The SDK sets and clears the header for you:
```ts
BitBadgesApi.setAccessToken(auth.access_token);
BitBadgesApi.unsetAccessToken();
```
Access tokens expire after 1 day. Refresh tokens expire after 60 days. Both become invalid when the user revokes access.
### Health Check
```ts
// POST /api/v0/auth/status
const res = await BitBadgesApi.checkIfSignedIn({});
console.log(res.signedIn); // false when expired, revoked, or not authenticated
```
```json
{
"signedIn": true,
"address": "0x092bb4851ae26850588243e7bef22a56287f4739",
"bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"chain": "ETH",
"scopes": [
{ "scopeName": "Complete Claims", "scopeId": "completeClaims" },
{ "scopeName": "Read Private Claim Data", "scopeId": "readPrivateClaimData" }
],
"message": "https://bitbadges.io wants you to sign in with your Ethereum address 0x092bb4851ae26850588243e7bef22a56287f4739",
"email": ""
}
```
This route works even when you requested no scopes. Use it to confirm the user has not revoked your app. `scopes` lists the approved scopes with their camelCase `scopeId`; `message` is the signed sign-in message; `email` is filled only when the session was created through an email sign in.
### Refreshing
```ts
const res = await BitBadgesApi.exchangeSIWBBAuthorizationCode({
grant_type: 'refresh_token',
refresh_token: session.refreshToken,
client_secret: process.env.SIWBB_CLIENT_SECRET,
client_id: 'app_demo_01',
redirect_uri: 'https://example.com/api/callback' // only if redirected
});
const { access_token, access_token_expires_at, refresh_token: newRefreshToken, refresh_token_expires_at } = res;
```
A refresh returns a new access token and a new refresh token with reset expirations. Repeat on a rolling basis for as long as the user keeps the authorization.
### Revoking
```ts
// POST https://api.bitbadges.io/api/v0/siwbb/token/revoke
await BitBadgesApi.revokeOauthAuthorization({ token: session.accessToken });
```
Revoke when you are done with a token. Users can also revoke from **Connections** then **Authorizations** on the site.
Sessions are your call. Tokens are one option; checking IDs, stamping hands, and claim numbers are others.
## What Is Verified Natively
Checked by BitBadges:
- Proof of address ownership through the user's authenticated BitBadges account.
- Anything in the verify challenge options.
- The code was issued within `options.issuedAtTimeWindowMs` (default 10 minutes).
- One exchange per authorization code.
Not checked by BitBadges:
- App-specific criteria (claims, ownership requirements, attestations). Adding `claimId` to the URL does not verify the claim. Check it server-side.
- Flash ownership, replay, or man-in-the-middle beyond what OAuth 2.0 itself protects against.
## Security Considerations
The flow is OAuth 2.0 compatible. Follow the OAuth 2.0 specification and its security guidance for the protocol itself.
Flash criteria checks are your problem to solve. Bob signs in with a token, transfers it to Alice, and Alice signs in with the same token. Both hold a valid session from one asset. The same applies to any transferable criterion. Design your claim criteria and any attestations so this cannot happen when it matters.
## Related
- [Authorization URL](authorization-url.md)
- [Claims endpoints](../claims/endpoints.md)
- [Frameworks](frameworks.md)
- [API reference](/api-reference)
## File: api/swaps.md
---
description: "Estimate a token swap through the BitBadges API. Payload, response, Skip Go compatibility, and how to execute the returned messages."
---
# Swaps
Set `SIGNER` to the wallet address authorized to sign the transaction, matching its explicit creator or sender. Browser deployment requires this binding even when reading a saved transaction.
`POST /api/v0/swap/estimate` returns the expected output amount and the messages needed to execute a swap. Routing covers native pools (the `x/gamm` module) and Skip Go routes across IBC chains such as Osmosis. The route requires an API key in the `x-api-key` header. Create one at [bitbadges.io/developer](https://bitbadges.io/developer).
See the [API reference](/api-reference) for every route's request and response schema.
## Example
Swap 0.001 BADGE (`1000000ubadge`; 1 BADGE = 1,000,000,000 ubadge) for USDC (denom `ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8`) from bob's address:
```bash
curl -X POST https://api.bitbadges.io/api/v0/swap/estimate \
-H "Content-Type: application/json" -H "x-api-key: $BITBADGES_API_KEY" \
-d '{
"tokenIn": "amount:1000000,denom:ubadge",
"tokenOutDenom": "ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8",
"chainIdsToAddresses": { "bitbadges-1": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue" },
"slippageTolerancePercent": 1
}'
```
```ts
import { BitBadgesAPI, BigIntify } from 'bitbadges';
const BitBadgesApi = new BitBadgesAPI({ convertFunction: BigIntify, apiKey: process.env.BITBADGES_API_KEY });
const res = await BitBadgesApi.estimateSwap({
tokenIn: 'amount:1000000,denom:ubadge', // or '1000000ubadge'
tokenOutDenom: 'ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8',
chainIdsToAddresses: { 'bitbadges-1': 'bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue' },
slippageTolerancePercent: 1
});
console.log(res.estimate.tokenOutAmount); // "1834"
console.log(res.estimate.skipGoMsgs);
// Sign and broadcast the msgs to execute the swap
```
```bash
bb swap estimate ubadge ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8 1000000 --addresses "{\"bitbadges-1\":\"$SIGNER\"}" --execute --browser --expected-address "$SIGNER"
```
A BitBadges-only route answers with one `multi_chain_msg` that wraps a `gamm.v1beta1.MsgSwapExactAmountIn` (synthesized from the SDK types):
```json
{
"success": true,
"estimate": {
"tokenOutAmount": "1834",
"tokenInAmount": "1000000",
"skipGoMsgs": [
{
"multi_chain_msg": {
"chain_id": "bitbadges-1",
"path": ["bitbadges-1"],
"msg": "{\"sender\":\"bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue\",\"routes\":[{\"pool_id\":\"1\",\"token_out_denom\":\"ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8\"}],\"token_in\":{\"denom\":\"ubadge\",\"amount\":\"1000000\"},\"token_out_min_amount\":\"1815\"}",
"msg_type_url": "/gamm.v1beta1.MsgSwapExactAmountIn"
}
}
],
"assetPath": [
{ "denom": "ubadge", "chainId": "bitbadges-1", "how": "genesis" },
{ "denom": "ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8", "chainId": "bitbadges-1", "how": "swap" }
],
"doesSwap": true,
"lowLiquidityWarning": false,
"complianceNotPassedWarning": false,
"estimatedTime": 6
}
}
```
The older path `/api/v0/swaps/estimate` still works as a deprecated alias that forwards to the same handler.
## Payload
```ts
interface iEstimateSwapPayload {
tokenIn: string;
tokenInChainId?: string;
tokenOutDenom: string;
tokenOutChainId?: string;
chainIdsToAddresses: Record;
chainIdsToAffiliates?: Record }>;
slippageTolerancePercent: string | number;
forcefulRecheckCompliance?: boolean;
isLocalOnly?: boolean;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `tokenIn` | string | yes | Token to swap in. Formats: `"amount:1,denom:ubadge"` or `"1ubadge"`. |
| `tokenInChainId` | string | no | Chain ID of the input token. Defaults to `bitbadges-1`. |
| `tokenOutDenom` | string | yes | Denom to receive. |
| `tokenOutChainId` | string | no | Chain ID of the output token. Defaults to `bitbadges-1`. |
| `chainIdsToAddresses` | object | yes | Chain ID to address. Supports `bitbadges-1` (bech32 `bb` address) and `1` (EVM `0x` address). Other chain addresses are derived from these. |
| `chainIdsToAffiliates` | object | no | Chain ID to affiliate fee recipients: `{ [chainId]: { affiliates: [{ address, basis_points_fee }] } }`. |
| `slippageTolerancePercent` | string or number | yes | Slippage tolerance, 0 to 100. |
| `forcefulRecheckCompliance` | boolean | no | Recheck compliance and skip the 5 minute cache. |
| `isLocalOnly` | boolean | no | Only use local pools for the estimate. |
## Response
```ts
interface iEstimateSwapSuccessResponse {
success: boolean;
estimate: {
tokenOutAmount: string;
tokenInAmount: string;
skipGoMsgs: SkipGoMessage[];
assetPath: { denom: string; chainId: string; how: 'genesis' | 'swap' | 'transfer' }[];
doesSwap: boolean;
lowLiquidityWarning?: boolean;
complianceNotPassedWarning?: boolean;
complianceErrorMessage?: string;
estimatedTime?: number;
fallbackAsset?: { denom: string; chainId: string };
autoRedirectedToWETH?: boolean;
rerouted?: boolean;
};
}
interface SkipGoMessage {
multi_chain_msg?: { chain_id: string; path: string[]; msg: string; msg_type_url: string };
evm_tx?: {
chain_id: string;
to: string;
value: string;
data: string;
required_erc20_approvals?: { token: string; spender: string }[];
signer_address: string;
};
}
```
| Field | Description |
| --- | --- |
| `tokenOutAmount` | Estimated amount received. |
| `tokenInAmount` | Amount swapped in. |
| `skipGoMsgs` | Messages for execution. Each entry is either a `multi_chain_msg` (Cosmos chains) or an `evm_tx` (EVM chains). `msg` is a JSON string of the Cosmos message. |
| `assetPath` | The path the asset takes: denom, chain ID, and how it moves (`genesis`, `swap`, `transfer`). |
| `doesSwap` | `true` when a swap occurs, `false` for a pure transfer. |
| `lowLiquidityWarning` | The pool has low liquidity. Execution may fail or slip. |
| `complianceNotPassedWarning` | Compliance checks failed. The BitBadges pool swap is likely to fail. `complianceErrorMessage` has the detail. |
| `estimatedTime` | Estimated seconds to complete, when available. |
| `fallbackAsset` | Asset to fall back to when the swap is not possible. |
| `autoRedirectedToWETH` | The route was redirected to WETH. BitBadges only supports single-transaction operations, bridges return WETH, and the extra unwrap transaction is not handled. |
| `rerouted` | Internal flag: the result differs from the standard estimate. |
## Skip Go Compatibility
The API mirrors Skip Go where it can. Full integration is planned, but there are differences:
- Skip does not support BitBadges routing yet, so the Skip API, engines, explorers, and client may not support the full feature set.
- `skipGoMsgs` follow the format of the [Skip API `POST /v2/fungible/msgs`](https://docs.skip.build/go/api-reference/prod/fungible/post-v2fungiblemsgs).
- Only Cosmos swaps are recommended. Chains outside Cosmos such as ETH and SOL are not supported yet.
## Executing from the CLI
For a BitBadges-only route (one native swap on the BitBadges chain with no Skip Go rerouting, EVM transaction, IBC transfer leg, or WETH redirect), the CLI signs and broadcasts without you handling `skipGoMsgs`:
```bash
bb swap estimate ubadge ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8 1000000 --addresses "{\"bitbadges-1\":\"$SIGNER\"}" --execute --browser --expected-address "$SIGNER"
```
Cross-chain, EVM, and multi-hop routes are returned but not auto-executed. Sign the estimate in your wallet, broadcast the first transaction, then run `bb swap track`. See [CLI swap](../cli/swap.md).
## Related
- [Trade on the DEX](../guides/trade-on-the-dex.md)
- [CLI swap](../cli/swap.md)
- [gamm module](../chain/modules/gamm/README.md)
- [API reference](/api-reference)
## File: chain/README.md
---
description: "The BitBadges chain: what runs on it, chain IDs, the BADGE denom and EVM decimals, public endpoints (RPC, LCD, EVM RPC, explorer, chain registry), IBC channels."
---
# Chain
BitBadges is an L1 delegated proof-of-stake chain built with the Cosmos SDK and CometBFT. Blocks reach instant finality. Accounts can sign with Cosmos keys (`bb1` bech32 addresses) or with Ethereum keys (`0x` addresses) through the EVM precompiles. This tab covers the chain around the token standard; the standard itself is the [Token Standard](../token-standard/README.md) tab.
| Area | What it covers | Read when |
| --- | --- | --- |
| [Chain API reference](/chain-api-reference) | Every LCD (REST) route the chain serves, with a live playground against `https://lcd.bitbadges.io` | You want to call a node query directly, or see a message's exact payload |
| [Modules](modules/README.md) | `x/tokenization` (the token standard), `x/gamm`, `x/poolmanager`, `x/sendmanager`, `x/managersplitter`, IBC hooks, rate limits | You use the DEX, shared management, or IBC middleware |
| [EVM](evm/README.md) | Precompiles at `0x0000000000000000000000000000000000001001` through `0x...1003`, Solidity quickstart, JSON-RPC | You write contracts against tokens or connect an Ethereum wallet |
| [Cross-Chain Queries](cross-chain-queries.md) | Interchain queries that verify ownership from another chain | Another chain gates on BitBadges balances |
| [Supported Denoms](supported-denoms.md) | The IBC coins the chain accepts for payments and pools | You attach a payment or seed a pool |
| [Run a Node](run-a-node.md) | Full node and validator setup, cosmovisor, upgrades | You operate infrastructure |
| [Testnet](testnet.md) | Status and the faucet API shape | You need a sandbox |
| [WebSocket Events](websocket-events.md) | CometBFT event subscriptions | A bot reacts to transfers in real time |
The rest of this page holds the constants every integration needs.
```bash
curl https://lcd.bitbadges.io/cosmos/base/tendermint/v1beta1/node_info | jq .default_node_info.network
# "bitbadges-1"
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' https://evm-rpc.bitbadges.io
# {"jsonrpc":"2.0","id":1,"result":"0xc368"} (50024)
```
## Chain IDs
| Network | Cosmos chain ID | EVM chain ID | Status |
| --- | --- | --- | --- |
| mainnet | `bitbadges-1` | `50024` | live |
| testnet | `bitbadges-2` | `50025` | offline since 2026-04-25, see [Testnet](testnet.md) |
| local | `bitbadges-1` (default from `init`) | `90123` | your machine |
Both EVM chain IDs are claimed in the ethereum-lists/chains registry.
## Native Coin
| Property | Value |
| --- | --- |
| Base denom | `ubadge` |
| Display denom | `BADGE` |
| Cosmos decimals | 9 (1 BADGE = 10^9 `ubadge`) |
| EVM decimals | 18, through the precisebank module |
| EVM base unit | `abadge` (1 `ubadge` = 10^9 `abadge`, 1 BADGE = 10^18 `abadge`) |
In Cosmos messages, use 9-decimal precision (`1000000000ubadge` is one BADGE). In Solidity and EVM JSON-RPC, use 18-decimal precision (one BADGE is `1e18` wei-equivalent). The precisebank module converts between the two; `abadge` is only visible on the EVM side. See [About BADGE](../about/badge-token.md) for supply and rewards.
## Endpoints
| Surface | mainnet | Notes |
| --- | --- | --- |
| CometBFT RPC | `https://rpc.bitbadges.io` | Tx broadcast, block and tx queries |
| WebSocket | `wss://rpc.bitbadges.io/websocket` | Event subscriptions, see [WebSocket Events](websocket-events.md) |
| LCD (REST) | `https://lcd.bitbadges.io` | gRPC-gateway for every module; `/bitbadges/bitbadgeschain/tokenization/...`, `/osmosis/gamm/...`, `/cosmos/...` |
| EVM JSON-RPC | `https://evm-rpc.bitbadges.io` | `eth_`, `net_`, `web3_` namespaces, see [EVM RPC Endpoints](evm/rpc-endpoints.md) |
| Explorer | `https://explorer.bitbadges.io` | Blocks, validators, staking |
| BitBadges API | `https://api.bitbadges.io` | Indexed data, needs an API key, see [API](../api/README.md) |
| Source | `https://github.com/bitbadges/bitbadgeschain` | Chain source and proto definitions |
| Chain registry | `https://github.com/cosmos/chain-registry/tree/master/bitbadges` | Official metadata, assets, IBC connections, peer lists |
Testnet endpoints are listed on [Testnet](testnet.md) and are down.
## IBC Connections
BitBadges (`bitbadges-1`) keeps these ICS-20 transfer channels, all registered in the chain registry `_IBC` directory and all active:
| Peer chain | BitBadges channel | Peer channel | Connection (BitBadges side) | Notable assets |
| --- | --- | --- | --- | --- |
| Osmosis | `channel-0` | `channel-104311` | `connection-1` | OSMO; BADGE liquidity on Osmosis |
| Noble | `channel-2` | `channel-158` | `connection-6` | Legacy `USDC.n` (backwards compatibility only) |
| Cosmos Hub | `channel-3` | `channel-1420` | `connection-8` | ATOM |
| Injective | `channel-40` | `channel-464` | `connection-89` | Canonical `USDC`: Circle's native `USDC.inj` (`erc20:0xa00C59fF5a080D2b954d0c75e46E22a0c371235a`), arriving as `ibc/E1116484...` |
The Injective connection (`_IBC/bitbadges-injective.json`) carries the canonical USDC denom, one IBC hop from Injective's native USDC. See [Supported Denoms](supported-denoms.md) for the full table and the canonical-versus-legacy USDC policy.
## Pages in This Section
| Page | Read when |
| --- | --- |
| [Supported Denoms](supported-denoms.md) | You price, pay, or back a collection in a coin other than BADGE |
| [Run a Node](run-a-node.md) | You operate a full node or validator |
| [Testnet](testnet.md) | You are looking for the testnet or faucet |
| [WebSocket Events](websocket-events.md) | Your bot reacts to on-chain events in real time |
## Related
- [Accounts](../token-standard/concepts/accounts.md)
- [EVM](evm/README.md)
- [IBC and x/bank Compatibility](../token-standard/ibc/README.md)
## File: chain/cross-chain-queries.md
---
description: "Interchain queries let another Cosmos chain verify x/tokenization ownership over IBC without moving tokens. Packet types, channel setup, Go examples."
---
# Cross-Chain Queries
Other Cosmos chains can verify token ownership on BitBadges over IBC with interchain queries. Tokens stay in the BitBadges silo; the remote chain receives a balance answer. Use this for cross-chain gating, collateral checks, shared credentials, and ownership-weighted governance. To move value across chains, wrap first (see [Cosmos Coin Wrapper Paths](../token-standard/ibc/cosmos-coin-wrapper-paths.md)).
```go
// Create ownership query packet for a single token ID and time
query := &types.OwnershipQueryPacket{
QueryId: "my-query-123",
Address: "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d", // or the 0x form, 0x0bc63cfe31d5218eb414b142c799e20964a54a1a
CollectionId: "5",
TokenId: "1", // Single token ID
OwnershipTime: "1609459200000", // Single timestamp (ms)
}
// Wrap in packet data
packetData := &types.TokenizationPacketData{
Packet: &types.TokenizationPacketData_OwnershipQuery{
OwnershipQuery: query,
},
}
// Send via IBC channel to BitBadges
// Response contains exact balance amount for that token/time
```
## Channel Setup
| Setting | Value |
| --- | --- |
| Port | `tokenization` |
| Version | `tokenization-1` |
| Ordering | `UNORDERED` |
## Packet Types
Two query shapes exist. `OwnershipQuery` answers one (token ID, ownership time) pair with an exact amount. `FullBalanceQuery` returns the whole `UserBalanceStore`.
### OwnershipQueryPacket
| Field | Type | Description |
| --- | --- | --- |
| `query_id` | string | Correlation ID |
| `address` | string | Address to check (bech32 or 0x hex) |
| `collection_id` | string | Collection to query |
| `token_id` | string | One token ID (uint as string) |
| `ownership_time` | string | One ownership time (uint as string, typically a ms timestamp) |
### OwnershipQueryResponsePacket
| Field | Type | Description |
| --- | --- | --- |
| `query_id` | string | Correlation ID from the request |
| `owns_tokens` | bool | `true` when `total_amount > 0` |
| `total_amount` | Uint | Exact balance for the (token ID, ownership time) pair |
| `proof_height` | uint64 | Block height of the response |
| `error` | string | Error message, empty on success |
### FullBalanceQueryPacket
| Field | Type | Description |
| --- | --- | --- |
| `query_id` | string | Correlation ID |
| `address` | string | Address to check (bech32 or 0x hex) |
| `collection_id` | string | Collection to query |
### FullBalanceQueryResponsePacket
| Field | Type | Description |
| --- | --- | --- |
| `query_id` | string | Correlation ID from the request |
| `balance_store` | bytes | Serialized `UserBalanceStore` (protobuf bytes) |
| `proof_height` | uint64 | Block height of the response |
| `error` | string | Error message, empty on success |
`balance_store` decodes to:
- `balances`: `Balance[]` (amount, token ID ranges, ownership time ranges)
- `outgoingApprovals` and `incomingApprovals`
- `autoApproveSelfInitiatedOutgoingTransfers`, `autoApproveSelfInitiatedIncomingTransfers`, `autoApproveAllIncomingTransfers`
- `userPermissions`
### Bulk Queries
`BulkOwnershipQueryPacket` carries `queries: OwnershipQueryPacket[]` and returns `BulkOwnershipQueryResponsePacket` with `responses: OwnershipQueryResponsePacket[]`. One packet may hold at most 100 queries.
## Full Balance Store Example
```go
// Create full balance query packet
query := &types.FullBalanceQueryPacket{
QueryId: "my-query-456",
Address: "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
CollectionId: "5",
}
// Wrap in packet data
packetData := &types.TokenizationPacketData{
Packet: &types.TokenizationPacketData_FullBalanceQuery{
FullBalanceQuery: query,
},
}
// Send via IBC channel to BitBadges
// Response contains serialized UserBalanceStore with all data
```
## Use Cases
- Cross-chain token gating: check ownership before granting access on another chain.
- DeFi collateral verification without transfer.
- Multi-chain identity: BitBadges tokens as credentials across the Cosmos ecosystem.
- Governance weighted by verified holdings.
- Approval checks: query the full store to inspect approval state.
## Related
- [Balances](../token-standard/concepts/balances.md)
- [Cosmos Coin Wrapper Paths](../token-standard/ibc/cosmos-coin-wrapper-paths.md)
- [BB-402](../token-standard/bb-402/README.md)
## File: chain/evm/README.md
---
description: "How the BitBadges EVM works: three custom precompiles that expose the tokenization, gamm, and send manager modules to Solidity contracts."
---
# EVM
BitBadges runs the cosmos/evm module, so Solidity contracts deploy and run on the chain. This section is for developers who want to reach the token standard, liquidity pools, or native coins from a contract.
```text
Solidity contract -> precompile (Go) -> Cosmos SDK module keeper -> chain state
```
A precompile is a fixed contract address that runs native Go instead of EVM bytecode. Calling it gives a contract direct access to a Cosmos SDK module, with type conversion, validation, and error handling done on the Go side.
## The Three BitBadges Precompiles
| Precompile | Address | Module | Use it for |
| --- | --- | --- | --- |
| [Tokenization](tokenization-precompile/README.md) | `0x0000000000000000000000000000000000001001` | `x/tokenization` | Collections, transfers, approvals, dynamic stores, address lists, votes |
| [GAMM](gamm-precompile/README.md) | `0x0000000000000000000000000000000000001002` | `x/gamm` | Join and exit liquidity pools, single and multi-hop swaps, swap with IBC transfer, pool queries |
| [Send manager](send-manager-precompile.md) | `0x0000000000000000000000000000000000001003` | `x/sendmanager` | Send native coins (including alias denoms such as `badgeslp:...`) from a contract without ERC20 wrapping |
All three take one `string calldata msgJson` argument per method. The JSON matches the protobuf JSON of the underlying Cosmos message. The caller (`msg.sender`) is set as the `creator` or `sender` on the Go side and cannot be spoofed. Addresses are the same on both sides: a `0x` address and its `bb1` bech32 form are the same 20 bytes.
The chain also ships the upstream [Cosmos SDK Precompiles](cosmos-precompiles.md) (staking, distribution, bank, governance, IBC, bech32, P256) at `0x0000000000000000000000000000000000000100` through `0x0000000000000000000000000000000000000807`.
## Pages
| Page | Read it when |
| --- | --- |
| [Solidity Quickstart](solidity-quickstart.md) | You want a working contract in five minutes. Constants, addresses, patterns, errors. |
| [Setup](setup.md) | You need chain IDs, RPC ports, MetaMask, a deploy script, and a frontend hook. |
| [Developer Guide](developer-guide.md) | You need the rules: who can sign what, address conversion, `msg.sender` in precompiles, 9 vs 18 decimals. |
| [Architecture](architecture.md) | You want the call path and the Go package layout. |
| [Tokenization Precompile](tokenization-precompile/README.md) | Overview, then [API](tokenization-precompile/api.md), [Gas](tokenization-precompile/gas.md), [Errors](tokenization-precompile/errors.md), [Security](tokenization-precompile/security.md). |
| [GAMM Precompile](gamm-precompile/README.md) | Overview, then [API](gamm-precompile/api.md) and [gotchas](gamm-precompile/gotchas.md). |
| [Send Manager Precompile](send-manager-precompile.md) | The `send` method. |
| [Cosmos SDK Precompiles](cosmos-precompiles.md) | Staking, distribution, governance, IBC, bank, bech32, P256, slashing, ICS02. |
| [EVM RPC Endpoints](rpc-endpoints.md) | Public JSON-RPC URLs, Hardhat and Foundry config, running your own JSON-RPC node. |
## Minimal Example
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract Example {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function balanceOf(uint256 collectionId, address user) external view returns (uint256) {
string memory json = TokenizationJSONHelpers.getBalanceAmountJSON(
collectionId, user, 1, block.timestamp * 1000
);
return TOKENIZATION.getBalanceAmount(json);
}
}
```
The Solidity interfaces, helper libraries, and example contracts live in the chain repo under [`contracts/`](https://github.com/BitBadges/bitbadgeschain/tree/master/contracts). The [counter-dapp](https://github.com/BitBadges/bitbadgeschain/tree/master/counter-dapp) is an end-to-end example with a deploy script and a Next.js frontend.
## Related
- [Token Standard](../../token-standard/README.md)
- [Network endpoints](../README.md)
- [EVM Query Challenges](../../token-standard/approval-criteria/evm-query-challenges.md)
- [Cosmos EVM documentation](https://docs.cosmos.network/evm/v0.5.0/documentation/overview)
## File: chain/evm/architecture.md
---
description: "The call path from a Solidity contract through a Go precompile to a Cosmos SDK keeper, the package layout, and how to add methods or precompiles."
---
# Architecture
This page explains how a precompile call travels from Solidity to chain state. Read it if you maintain contracts that depend on precompile behavior or want to extend the chain.
## Call Path
```text
┌─────────────────────────────────────────────────────────────┐
│ Solidity smart contract │
│ calls 0x0000000000000000000000000000000000001001 (tokenization), │
│ 0x0000000000000000000000000000000000001002 (gamm), │
│ 0x0000000000000000000000000000000000001003 (sendmanager) │
└────────────────────────┬────────────────────────────────────┘
│ ABI-encoded call: method ID + string msgJson
┌────────────────────────▼────────────────────────────────────┐
│ Precompile contract (Go) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ABI decoder: method ID resolution, unpack args │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ JSON unmarshal: msgJson -> Cosmos SDK Msg / Query │ │
│ │ creator/sender := contract.Caller() │ │
│ │ 0x addresses -> bech32 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Security layer: caller check, ValidateBasic, │ │
│ │ array size limits, overflow checks │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
│ keeper / msg server call
┌────────────────────────▼────────────────────────────────────┐
│ Cosmos SDK module keeper │
│ x/tokenization, x/gamm, x/sendmanager │
│ state, business rules, validation, events │
└─────────────────────────────────────────────────────────────┘
```
## Registration
`app/evm.go` builds the EVM keeper with the upstream cosmos/evm static precompiles, then registers the three BitBadges precompiles:
```go
func (app *App) registerCustomPrecompiles() {
tokenizationPrecompile := tokenizationprecompile.NewPrecompile(app.TokenizationKeeper)
app.EVMKeeper.RegisterStaticPrecompile(common.HexToAddress(tokenizationprecompile.TokenizationPrecompileAddress), tokenizationPrecompile)
gammPrecompile := gammprecompile.NewPrecompile(app.GammKeeper)
app.EVMKeeper.RegisterStaticPrecompile(common.HexToAddress(gammprecompile.GammPrecompileAddress), gammPrecompile)
sendManagerPrecompile := sendmanagerprecompile.NewPrecompile(app.SendmanagerKeeper)
app.EVMKeeper.RegisterStaticPrecompile(common.HexToAddress(sendmanagerprecompile.SendManagerPrecompileAddress), sendManagerPrecompile)
// Next available address: 0x0000000000000000000000000000000000001004
}
```
A precompile must be both registered and enabled. Registration happens at app start. Enabling happens in genesis (the `active_static_precompiles` list of the EVM params) or in an upgrade handler. `app/precompile_helpers.go` provides `GetAllCustomPrecompileAddresses`, `ValidateNoAddressCollisions` (panics at startup on a duplicate), and a test helper that registers and enables everything.
The EVM keeper is built on the `precisebank` keeper, not raw `x/bank`, because the chain's native coin has 9 decimals and the EVM expects 18. See [Developer Guide](developer-guide.md#decimals-9-on-the-cosmos-side-18-on-the-evm-side).
## Package Layout
Each precompile is a Go package under its module: `x/tokenization/precompile/`, `x/gamm/precompile/`, `x/sendmanager/precompile/`.
| File | Role |
| --- | --- |
| `precompile.go` | Implements `vm.PrecompiledContract`: `RequiredGas()`, `Run()`, `Execute()`, method constants, gas constants, handlers |
| `abi.json` | The Solidity ABI: method signatures, parameter and return types, events. Embedded into the binary |
| `json_unmarshal.go` | Maps a method name to a Msg or Query type, unmarshals `msgJson`, sets the creator or sender from the caller, converts addresses |
| `validation.go` | Input validation helpers |
| `security.go` | Caller check, overflow check, array size limits |
| `errors.go` | Structured `PrecompileError` with codes, Cosmos error mapping, detail sanitization |
| `events.go` | Emits `precompile_*` Cosmos events |
| `gas.go` | Per-element gas constants and calculators |
| `return_types.go`, `return_types_conversions.go` (tokenization) | Go-to-Solidity struct conversion helpers |
| `metrics.go` (tokenization, gamm) | Usage logging |
The Solidity side lives in the chain repo under `contracts/`: `interfaces/` (`ITokenizationPrecompile.sol`, `IGammPrecompile.sol`, `ISendManagerPrecompile.sol`), `libraries/` (JSON helpers, struct helpers, errors, decoders, wrappers), `types/` (`TokenizationTypes.sol` mirrors the proto types), `examples/`, `templates/`, and `test/`.
## Transaction Flow
1. The contract calls a precompile method with an ABI-encoded `string msgJson`.
2. `RequiredGas` reads the method ID and charges the base cost plus a fixed buffer ([Gas](tokenization-precompile/gas.md)).
3. `Run` opens an SDK context from the EVM state DB and dispatches by method name.
4. `unmarshalMsgFromJSON` picks the Msg type, unmarshals the JSON, overrides `creator` (or `sender`) with the caller's bech32 address, converts any `0x` addresses in the message to bech32, and runs `ValidateBasic`.
5. The handler calls the module's msg server. The keeper applies the business rules and writes state.
6. The keeper and the precompile emit events.
7. The result is ABI-packed and returned: `bool success`, or a `uint256` ID for creation methods.
An error at any step returns a `PrecompileError` and reverts the EVM call. State written by the keeper is rolled back with the transaction.
## Query Flow
1. The contract calls a `view` method with `msgJson`.
2. `unmarshalQueryFromJSON` picks the query request type, unmarshals, and converts addresses (the `userAddress` alias is accepted for `getBalance` and `getDynamicStoreValue`).
3. `validateQueryRequest` rejects zero IDs and oversized range arrays (`MaxQueryArraySize` = 1000).
4. The keeper's gRPC query handler runs.
5. The response is packed. Most getters return the protobuf-encoded response as `bytes`. `getChallengeTracker`, `getETHSignatureTracker`, `getWrappableBalances`, `getBalanceAmount`, and `getTotalSupply` return `uint256`; `isAddressReservedProtocol` returns `bool`; `getAllReservedProtocolAddresses` returns `address[]`.
Decode `bytes` off-chain with the TypeScript SDK, or extract single fields on-chain with `TokenizationDecoders` (for example `parseHolderCountFromStats`).
## Address Conversion
```go
// EVM address -> Cosmos address
caller := contract.Caller() // common.Address (20 bytes)
cosmosAddr := sdk.AccAddress(caller.Bytes()).String() // Bech32 format
```
Address strings inside JSON go through `convertEVMAddressToBech32`: a valid bech32 string passes through; a hex address is converted; anything else is left for `ValidateBasic` to reject.
## Gas
```go
// x/tokenization/precompile/gas.go: base gas + per-element costs
func CalculateTransferGas(toAddresses []common.Address, tokenIdsRanges []uintRange, ownershipTimesRanges []uintRange) uint64 {
var gas uint64 = GasTransferTokensBase
gas += uint64(len(toAddresses)) * GasPerRecipient
gas += uint64(len(tokenIdsRanges)) * GasPerTokenIdRange
gas += uint64(len(ownershipTimesRanges)) * GasPerOwnershipTimeRange
return gas
}
```
The per-method base costs, the fixed buffers (`+200,000` for transactions, `+50,000` for queries), and the per-element constants are listed on [Gas](tokenization-precompile/gas.md).
## Security
Every transaction method verifies the caller:
```go
caller := contract.Caller()
if err := VerifyCaller(caller); err != nil {
return nil, err
}
```
Inputs are type-checked, range-checked (`start <= end`), size-limited (DoS protection), and validated against module rules. Errors are structured and sanitized:
```go
return nil, ErrInvalidInput("invalid collectionId")
```
Details on [Security](tokenization-precompile/security.md).
## Performance Notes
- Gas is computed from the method and input size, so simple calls stay cheap.
- The ABI is parsed once at init. Method lookup is a map lookup.
- If `abi.json` fails to load, the precompile is disabled but the chain still starts (`GetABILoadError`).
## Extending
Add a method to an existing precompile:
1. Define the method in `abi.json`.
2. Add the method constant in `precompile.go`.
3. Add a gas constant and a case in `RequiredGas`.
4. Add a case to `Execute` and implement the handler.
5. Add JSON routing in `json_unmarshal.go` and type conversions if needed.
6. Add validation and tests.
Add a new precompile:
1. Create a package that implements `vm.PrecompiledContract`.
2. Register it in `registerCustomPrecompiles` (next address is `0x...1004`) and add it to `GetAllCustomPrecompileAddresses`.
3. Enable it in genesis or an upgrade handler.
4. Define the ABI and the Solidity interface under `contracts/`.
## Related
- [Tokenization Precompile](tokenization-precompile/README.md)
- [Developer Guide](developer-guide.md)
- [Token Standard](../../token-standard/README.md)
- [Cosmos EVM documentation](https://docs.cosmos.network/evm/v0.5.0/documentation/overview)
## File: chain/evm/cosmos-precompiles.md
---
description: "The upstream cosmos/evm precompiles on BitBadges (P256, bech32, staking, distribution, IBC, bank, governance, slashing, ICS02) with addresses and methods."
---
# Cosmos SDK Precompiles
These precompiles come from the upstream [cosmos/evm](https://github.com/cosmos/evm) module and give Solidity contracts direct access to standard Cosmos SDK modules. Unlike the BitBadges precompiles, which take JSON, these use ordinary ABI-encoded parameters.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IBech32 {
function hexToBech32(address addr, string calldata prefix) external pure returns (string memory);
function bech32ToHex(string calldata bech32Addr) external pure returns (address);
}
contract WhoAmI {
IBech32 constant BECH32 = IBech32(0x0000000000000000000000000000000000000400);
function myCosmosAddress() external view returns (string memory) {
return BECH32.hexToBech32(msg.sender, "bb");
}
}
```
## Addresses
| Precompile | Address | Type |
| --- | --- | --- |
| P256 | `0x0000000000000000000000000000000000000100` | Cryptography |
| Bech32 | `0x0000000000000000000000000000000000000400` | Utility |
| Staking | `0x0000000000000000000000000000000000000800` | Transactions + queries |
| Distribution | `0x0000000000000000000000000000000000000801` | Transactions + queries |
| ICS20 (IBC transfer) | `0x0000000000000000000000000000000000000802` | Transactions + queries |
| Bank | `0x0000000000000000000000000000000000000804` | Queries only |
| Governance | `0x0000000000000000000000000000000000000805` | Transactions + queries |
| Slashing | `0x0000000000000000000000000000000000000806` | Transactions + queries |
| ICS02 (IBC light clients) | `0x0000000000000000000000000000000000000807` | Transactions + queries |
`app/evm.go` registers this set through `DefaultStaticPrecompiles` from cosmos/evm v0.7.x. A precompile also has to be in the EVM params' `active_static_precompiles` list to be callable. Check the live list with:
```bash
bb query evm params --node https://rpc.bitbadges.io
```
## P256
Address `0x0000000000000000000000000000000000000100`. Implements secp256r1 (P-256) signature verification per [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md).
Low-level call with 160 bytes of input:
- Bytes 0-31: hash of the signed data
- Bytes 32-63: `r` component of the signature
- Bytes 64-95: `s` component of the signature
- Bytes 96-127: `x` coordinate of the public key
- Bytes 128-159: `y` coordinate of the public key
Returns 32 bytes with value `1` if valid, empty if invalid. Gas: 3,450.
## Bech32
Address `0x0000000000000000000000000000000000000400`. Converts between hex and bech32 addresses.
```solidity
interface IBech32 {
/// @notice Convert hex address to bech32 string
function hexToBech32(address addr, string calldata prefix) external pure returns (string memory);
/// @notice Convert bech32 string to hex address
function bech32ToHex(string calldata bech32Addr) external pure returns (address);
}
```
```solidity
contract AddressRoundTrip {
IBech32 constant BECH32 = IBech32(0x0000000000000000000000000000000000000400);
function roundTrip() external pure returns (string memory cosmosAddr, address evmAddr) {
// Convert EVM address to Cosmos address
cosmosAddr = BECH32.hexToBech32(0x0bc63cfe31d5218eb414b142c799e20964a54a1a, "bb");
// Result: "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
// Convert Cosmos address to EVM address
evmAddr = BECH32.bech32ToHex("bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d");
// Result: 0x0bc63cfe31d5218eb414b142c799e20964a54a1a
}
}
```
The tokenization precompile exposes the same conversion as `convertEvmAddressToBech32` and `convertBech32ToEvmAddress` with the `bb` prefix fixed.
## Staking
Address `0x0000000000000000000000000000000000000800`.
Transaction methods:
| Method | Description |
| --- | --- |
| `createValidator(Description description, CommissionRates commissionRates, uint256 minSelfDelegation, address validatorAddress, string pubkey, uint256 value)` | Create a new validator |
| `editValidator(Description description, address validatorAddress, int256 commissionRate, int256 minSelfDelegation)` | Edit validator parameters |
| `delegate(address validator, uint256 amount)` | Delegate tokens to a validator |
| `undelegate(address validator, uint256 amount)` | Undelegate tokens from a validator |
| `redelegate(address srcValidator, address dstValidator, uint256 amount)` | Redelegate tokens between validators |
| `cancelUnbondingDelegation(address validator, uint256 amount, int64 creationHeight)` | Cancel an unbonding delegation |
Query methods:
| Method | Description |
| --- | --- |
| `delegation(address delegator, address validator)` | Get delegation info |
| `unbondingDelegation(address delegator, address validator)` | Get unbonding delegation info |
| `validator(address validator)` | Get validator info |
| `validators(string status, PageRequest pagination)` | Get all validators |
| `redelegation(address delegator, address srcValidator, address dstValidator)` | Get redelegation info |
| `redelegations(address delegator, address srcValidator, address dstValidator, PageRequest pagination)` | Get all redelegations |
## Distribution
Address `0x0000000000000000000000000000000000000801`. Staking rewards.
Transaction methods:
| Method | Description |
| --- | --- |
| `setWithdrawAddress(address withdrawAddr)` | Set the withdrawal address for rewards |
| `withdrawDelegatorRewards(address validator)` | Withdraw rewards from a validator |
| `withdrawValidatorCommission(address validator)` | Withdraw validator commission |
| `fundCommunityPool(Coin[] amount)` | Fund the community pool |
| `claimRewards(address delegator, uint32 maxRetrieve)` | Claim all rewards |
| `depositValidatorRewardsPool(address validator, Coin[] amount)` | Deposit to validator rewards pool |
Query methods:
| Method | Description |
| --- | --- |
| `validatorDistributionInfo(address validator)` | Get validator distribution info |
| `validatorOutstandingRewards(address validator)` | Get validator outstanding rewards |
| `validatorCommission(address validator)` | Get validator commission |
| `validatorSlashes(address validator, uint64 startingHeight, uint64 endingHeight, PageRequest pagination)` | Get validator slashes |
| `delegationRewards(address delegator, address validator)` | Get delegation rewards |
| `delegationTotalRewards(address delegator)` | Get total rewards for a delegator |
| `delegatorValidators(address delegator)` | Get validators for a delegator |
| `delegatorWithdrawAddress(address delegator)` | Get withdraw address |
| `communityPool()` | Get community pool balance |
## ICS20 (IBC Transfer)
Address `0x0000000000000000000000000000000000000802`.
```solidity
interface IICS20 {
/// @notice Transfer tokens via IBC
function transfer(
string calldata sourcePort,
string calldata sourceChannel,
string calldata denom,
uint256 amount,
string calldata receiver,
Height calldata timeoutHeight,
uint64 timeoutTimestamp,
string calldata memo
) external returns (uint64 sequence);
}
```
Query methods:
| Method | Description |
| --- | --- |
| `denom(string hash)` | Get denom trace from hash |
| `denoms(PageRequest pagination)` | Get all denom traces |
| `denomHash(string trace)` | Get hash from denom trace |
Outbound IBC transfers are subject to the chain's [rate limits](../modules/ibc-rate-limit.md).
## Bank
Address `0x0000000000000000000000000000000000000804`. Read-only.
```solidity
interface IBank {
/// @notice Get all balances for an account
function balances(address account, PageRequest pagination) external view returns (Coin[] memory, PageResponse memory);
/// @notice Get total supply of all tokens
function totalSupply(PageRequest pagination) external view returns (Coin[] memory, PageResponse memory);
/// @notice Get supply of a specific denom
function supplyOf(string calldata denom) external view returns (Coin memory);
}
```
Gas: `balances` 2,851; `totalSupply` 2,477; `supplyOf` 2,477.
To send native coins from a contract, use the [Send Manager Precompile](send-manager-precompile.md).
## Governance
Address `0x0000000000000000000000000000000000000805`.
Transaction methods:
| Method | Description |
| --- | --- |
| `submitProposal(address proposer, bytes jsonProposal, Coin[] deposit)` | Submit a governance proposal |
| `deposit(uint64 proposalId, Coin[] amount)` | Deposit tokens to a proposal |
| `cancelProposal(uint64 proposalId)` | Cancel a proposal (proposer only) |
| `vote(uint64 proposalId, VoteOption option, string metadata)` | Vote on a proposal |
| `voteWeighted(uint64 proposalId, WeightedVoteOption[] options, string metadata)` | Vote with weights |
Query methods:
| Method | Description |
| --- | --- |
| `getVotes(uint64 proposalId, PageRequest pagination)` | Get votes for a proposal |
| `getVote(uint64 proposalId, address voter)` | Get a specific vote |
| `getDeposit(uint64 proposalId, address depositor)` | Get a deposit |
| `getDeposits(uint64 proposalId, PageRequest pagination)` | Get all deposits |
| `getTallyResult(uint64 proposalId)` | Get tally result |
| `getProposal(uint64 proposalId)` | Get proposal info |
| `getProposals(ProposalStatus status, address voter, address depositor, PageRequest pagination)` | Get all proposals |
| `getParams()` | Get governance params |
| `getConstitution()` | Get chain constitution |
## Slashing
Address `0x0000000000000000000000000000000000000806`.
```solidity
interface ISlashing {
/// @notice Unjail a jailed validator
function unjail(address validatorAddr) external;
}
```
Query methods:
| Method | Description |
| --- | --- |
| `getSigningInfo(address consAddr)` | Get signing info for a validator |
| `getSigningInfos(PageRequest pagination)` | Get all signing infos |
| `getParams()` | Get slashing params |
## ICS02 (IBC Light Clients)
Address `0x0000000000000000000000000000000000000807`. The ICS-02 client router.
```solidity
interface ICS02I {
/// @notice The result of an update operation
enum UpdateResult {
Update,
Misbehaviour
}
/// @notice Update the client with the given client identifier
/// @param updateMsg The encoded update message, e.g. a protobuf Any
function updateClient(string calldata clientId, bytes calldata updateMsg) external returns (UpdateResult);
/// @notice Verify membership of a key-value pair. Not view: may update
/// client state for caching. Returns the unix timestamp (seconds) of the
/// verification height on the counterparty chain.
function verifyMembership(
string calldata clientId,
bytes calldata proof,
Height calldata proofHeight,
bytes[] calldata path,
bytes calldata value
) external returns (uint256);
/// @notice Verify non-membership of a key. Not view: may update client
/// state for caching. Returns the unix timestamp (seconds) of the
/// verification height on the counterparty chain.
function verifyNonMembership(
string calldata clientId,
bytes calldata proof,
Height calldata proofHeight,
bytes[] calldata path
) external returns (uint256);
}
```
Query methods:
| Method | Description |
| --- | --- |
| `getClientState(string clientId)` | Get the client state (returns raw bytes) |
## Common Types
```solidity
struct Coin {
string denom;
uint256 amount;
}
struct PageRequest {
bytes key;
uint64 offset;
uint64 limit;
bool countTotal;
bool reverse;
}
struct PageResponse {
bytes nextKey;
uint64 total;
}
struct Height {
uint64 revisionNumber;
uint64 revisionHeight;
}
enum VoteOption {
Unspecified,
Yes,
Abstain,
No,
NoWithVeto
}
```
## Related
- [EVM overview](README.md)
- [Send Manager Precompile](send-manager-precompile.md)
- [Cosmos EVM documentation](https://docs.cosmos.network/evm/)
- [Cosmos EVM precompiles source](https://github.com/cosmos/evm/tree/v0.7.2/precompiles)
## File: chain/evm/developer-guide.md
---
description: "Rules for the BitBadges EVM: which keys sign which transactions, address conversion, msg.sender inside a precompile, and 9 vs 18 decimals."
---
# Developer Guide
This page covers the rules a developer must know when an application touches both the EVM and Cosmos SDK sides of BitBadges. Read it once before writing contracts or wallet code.
## Transaction Signing
BitBadges accepts two kinds of transactions. Each needs a specific key type.
| Transaction type | Signing key type | Hash algorithm | Use case |
| --- | --- | --- | --- |
| `MsgEthereumTx` | `ethsecp256k1` (Ethereum-style) | Keccak256 | EVM contract calls, precompile calls |
| Standard Cosmos messages | `secp256k1` (Cosmos-style) | SHA256 | Native Cosmos SDK messages (`MsgDelegate`, `MsgTransferTokens`) |
### ETH Wallets (`ethsecp256k1`)
Can sign `MsgEthereumTx`: direct EVM contract calls, precompile calls from Solidity, any Ethereum-compatible transaction.
Cannot sign standard Cosmos SDK messages (`MsgDelegate`, `MsgTransferTokens`, other native messages). The hash algorithm differs.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Works: an ETH wallet signs the MsgEthereumTx that runs this
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract EthWalletTransfer {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function send(uint256 collectionId, address to, uint256 amount) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
// Build JSON using helper
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(1, 1);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER);
string memory transferJson = TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIdsJson, ownershipJson
);
return precompile.transferTokens(transferJson);
}
}
```
### Cosmos Wallets (`secp256k1`)
Can sign standard Cosmos SDK messages: `MsgDelegate`, `MsgTransferTokens`, `MsgCreateCollection`, all native messages.
Cannot sign `MsgEthereumTx`, so cannot call EVM contracts or precompiles directly. The signature format differs.
```go
// Works: a Cosmos wallet signs a standard message
msg := &tokenizationtypes.MsgTransferTokens{
Creator: cosmosAddress,
CollectionId: collectionId,
Transfers: transfers,
}
```
### Cross-Compatibility
Not supported: ETH wallets signing standard Cosmos messages, or Cosmos wallets signing `MsgEthereumTx`. The reasons are different hash algorithms (Keccak256 vs SHA256), different signature formats, and an ante handler that routes by transaction type.
The workaround is to pick the surface that matches the key: ETH wallets reach Cosmos SDK modules through precompiles from Solidity; Cosmos wallets use native messages for direct module access. The BitBadges API and frontend add a separate path for ETH wallets to sign Cosmos messages; see [Sign In with BitBadges](../../api/sign-in/README.md).
### Key Types Side by Side
| Key type | Algorithm | Hash function | Address format | Signs `MsgEthereumTx` | Signs Cosmos messages |
| --- | --- | --- | --- | --- | --- |
| `ethsecp256k1` | secp256k1 | Keccak256 | Both (EVM hex + Cosmos bech32) | Yes | No (different hash) |
| `secp256k1` (Cosmos) | secp256k1 | SHA256 | Cosmos bech32 only | No (different format) | Yes |
### Create Accounts
```bash
# EVM-compatible account (ethsecp256k1 key)
bb keys add my-eth-account --keyring-backend test --algo eth_secp256k1
```
```bash
# Cosmos-only account (standard secp256k1 key, the default)
bb keys add my-cosmos-account --keyring-backend test
```
The chain registers both key types in its codec, so the ante handler verifies signatures from either:
```go
// ethsecp256k1 keys are registered in the codec
registry.RegisterImplementations((*cryptotypes.PubKey)(nil), ðsecp256k1.PubKey{})
registry.RegisterImplementations((*cryptotypes.PrivKey)(nil), ðsecp256k1.PrivKey{})
```
## Address Conversion
An Ethereum address (20 bytes) and a Cosmos bech32 address are two encodings of the same account bytes when the key is `ethsecp256k1`.
| Format | Example | Use case |
| --- | --- | --- |
| EVM (hex) | `0x0bc63cfe31d5218eb414b142c799e20964a54a1a` | Solidity contracts, EVM transactions |
| Cosmos (bech32) | `bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d` | Cosmos SDK messages, queries |
EVM to Cosmos, as done inside precompile code:
```go
// In precompile code
caller := contract.Caller() // common.Address (20 bytes, e.g. 0x0bc63cfe31d5218eb414b142c799e20964a54a1a)
cosmosAddr := sdk.AccAddress(caller.Bytes()).String() // Bech32 (e.g. bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d)
```
Cosmos to EVM:
```go
cosmosAddr, _ := sdk.AccAddressFromBech32("bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d")
evmAddr := common.BytesToAddress(cosmosAddr.Bytes()) // 0x0bc63cfe31d5218eb414b142c799e20964a54a1a
```
Rules:
- Same 20 bytes, different encoding. EVM uses a `0x` hex prefix; Cosmos uses bech32 with the `bb` prefix.
- Precompiles convert for you. Address fields in precompile JSON accept either form; the Go side normalizes to bech32.
- Cosmos SDK validates bech32; the EVM validates hex. Both name the same account.
Concept page: [Accounts](../../token-standard/concepts/accounts.md).
## Precompile Caller
A precompile identifies the caller with `contract.Caller()`. This is the EVM `msg.sender` of the call into the precompile, converted to a Cosmos address:
```go
// In precompile code
func (p Precompile) GetCallerAddress(contract *vm.Contract) (string, error) {
caller := contract.Caller() // EVM msg.sender
return sdk.AccAddress(caller.Bytes()).String(), nil // Converted to Cosmos address
}
```
### The Caller Is the Immediate Caller
When a contract calls a precompile, the precompile sees the contract, not the user who sent the transaction.
```text
User (0xAlice) -> Contract A -> Precompile
^
contract.Caller() = Contract A's address, NOT 0xAlice
```
The `creator` on every message is the contract address. Tokens move from the contract's balance. The original user address is not available to the precompile, so authorization lives in the contract.
### No Cross-Contract Delegation
```text
User -> Contract A -> Contract B -> Precompile
^
contract.Caller() = Contract B
```
Contract A is invisible to the precompile.
### Authorization Patterns
Contract-level authorization:
```solidity
import "./libraries/TokenizationJSONHelpers.sol";
contract MyContract {
ITokenizationPrecompile precompile = ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
mapping(address => bool) public authorized;
function transferTokens(
uint256 collectionId,
address to,
uint256 amount
) external {
require(authorized[msg.sender], "Not authorized");
// Build JSON
address[] memory recipients = new address[](1);
recipients[0] = to;
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(1, 1);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER);
string memory transferJson = TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIdsJson, ownershipJson
);
// Contract is authorized, so precompile call succeeds
precompile.transferTokens(transferJson);
}
}
```
Move tokens from a user instead of the contract:
```solidity
// The precompile always uses contract.Caller() as the creator.
// For transfers, tokens move from the contract's balance by default.
// To move tokens from a specific user, that user grants the contract an
// outgoing approval, and the contract sets "from" to the user in the transfer JSON.
function transferFromUser(uint256 collectionId, address from, address to, uint256 amount) external returns (bool) {
string memory transferJson = string(abi.encodePacked(
'{"collectionId":"', TokenizationJSONHelpers.uintToString(collectionId),
'","transfers":[{"from":"', TokenizationJSONHelpers.addressToString(from),
'","toAddresses":["', TokenizationJSONHelpers.addressToString(to),
'"],"balances":[{"amount":"', TokenizationJSONHelpers.uintToString(amount),
'","tokenIds":', TokenizationJSONHelpers.uintRangeToJson(1, 1),
',"ownershipTimes":', TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER),
'}]}]}'
));
return precompile.transferTokens(transferJson);
}
```
Approval system:
```json
{
"@type": "/tokenization.MsgSetOutgoingApproval",
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "1",
"approval": {
"toListId": "All",
"initiatedByListId": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "1" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "contract-may-spend",
"version": "0",
"approvalCriteria": {}
}
}
```
The user (alice) signs this once. The contract at `0x5FbDB2315678afecb367f032d93F642f64180aa3` is then the initiator of every later transfer, and the precompile sees the contract as caller while the outgoing approval covers alice's tokens.
See [Transferability](../../token-standard/concepts/transferability.md) for how outgoing approvals authorize a third party.
### Security Notes
- The precompile always uses `contract.Caller()`. A malicious contract cannot spoof it.
- Do the authorization check in the contract before the precompile call. Do not expect the precompile to see the original user.
- In multi-contract flows, intermediate contracts are invisible. Design authorization with that in mind.
## Decimals: 9 on the Cosmos Side, 18 on the EVM Side
| Unit | Decimals | Value | Context |
| --- | --- | --- | --- |
| `BADGE` | 9 (Cosmos) / 18 (EVM) | 1 BADGE | Display unit |
| `ubadge` | 9 | 1 * 10^9 | Base unit in Cosmos SDK (`x/bank`) |
| `abadge` | 0 | 1 * 10^0 | Base unit in the EVM (smallest unit) |
- 1 BADGE = 1 * 10^9 `ubadge` (Cosmos)
- 1 BADGE = 1 * 10^18 `abadge` (EVM)
- 1 `ubadge` = 1 * 10^9 `abadge`
The `precisebank` module sits between `x/bank` (9 decimals) and the EVM (18 decimals). It converts amounts in both directions. The EVM keeper is wired to `precisebank`, not raw `x/bank`. You still write amounts in the precision of the side you are on.
In Solidity:
```solidity
// Correct: 18 decimal precision in the EVM
uint256 oneBadge = 1 * 10**18; // 1 BADGE in EVM
uint256 halfBadge = 5 * 10**17; // 0.5 BADGE in EVM
// Wrong: 9 decimals in the EVM
uint256 wrongAmount = 1 * 10**9; // This is 0.000000001 BADGE in EVM!
```
In Cosmos SDK messages and queries:
```typescript
// Correct: 9 decimal precision on the Cosmos side
const coin = {
denom: 'ubadge',
amount: '1000000000', // 1 BADGE in Cosmos (1 * 10^9)
};
// Wrong: 18 decimals on the Cosmos side
const wrongCoin = {
denom: 'ubadge',
amount: '1000000000000000000', // This is 1 * 10^9 BADGE in Cosmos!
};
```
Conversions the module does for you:
- EVM to Cosmos: 18 decimals to 9 decimals when a contract reaches a Cosmos module
- Cosmos to EVM: 9 decimals to 18 decimals when a Cosmos operation touches the EVM
### Common Mistakes
Mixing precisions:
```solidity
// Wrong: Cosmos precision in the EVM
uint256 amount = 1 * 10**9; // This is 0.000000001 BADGE in EVM!
// Correct: EVM precision
uint256 amount = 1 * 10**18; // 1 BADGE in EVM
```
Assuming a 1:1 conversion:
```typescript
// Wrong: these are NOT equal
const cosmosAmount = '1000000000'; // 1 BADGE in Cosmos
const evmAmount = '1000000000'; // 0.000000001 BADGE in EVM!
// Correct: convert
const cosmosAmount = '1000000000'; // 1 BADGE in Cosmos
const evmAmount = '1000000000000000000'; // 1 BADGE in EVM
```
Forgetting the context: Solidity is always 18 decimals; Cosmos messages are always 9. Token balances inside collections (the tokenization module) are plain integers with no implied decimals; this section is about the native `BADGE` coin only.
## EVM Query Challenges and Invariants
An approval or a collection invariant can gate on a read-only call to an EVM contract. Use this to check DeFi positions, compliance registries, or cross-chain state before a transfer goes through.
```solidity
// Gate transfers to users holding 100+ units of a 6-decimal ERC20 (for example a USDC-style token at 0x5FbDB2315678afecb367f032d93F642f64180aa3)
string memory evmQueryChallenge = string(abi.encodePacked(
'{"contractAddress":"0x5FbDB2315678afecb367f032d93F642f64180aa3",',
'"calldata":"70a08231000000000000000000000000$sender",', // balanceOf(address)
'"expectedResult":"0000000000000000000000000000000000000000000000000000000005f5e100",', // 100 * 10^6
'"comparisonOperator":"gte",',
'"gasLimit":"100000"}'
));
```
Full field reference: [EVM Query Challenges](../../token-standard/approval-criteria/evm-query-challenges.md) and [Invariants](../../token-standard/approval-criteria/invariants.md).
## Best Practices
1. Pick the key type for the job. `ethsecp256k1` for EVM contracts, precompile calls, and Ethereum tooling. Standard `secp256k1` for native-only Cosmos use.
2. Use hex addresses in Solidity and bech32 in Cosmos SDK code. Precompiles convert.
3. Authorize in the contract. Do not rely on the precompile seeing the original user. Use approvals when a contract acts for a user.
4. Use `MsgEthereumTx` for contract and precompile calls. Use native Cosmos messages for direct module access, cheaper gas on simple operations, and Cosmos tooling.
5. Test with both key types if your app supports both. Verify address conversions. Test authorization through contract intermediaries.
| Aspect | ETH wallets (`ethsecp256k1`) | Cosmos wallets (`secp256k1`) |
| --- | --- | --- |
| Signs `MsgEthereumTx` | Yes | No |
| Signs Cosmos messages | No | Yes |
| Address format | Both (hex + bech32) | bech32 only |
| Precompile access | Via Solidity contracts | No direct access |
| Native SDK access | Via precompiles only | Direct |
## Related
- [Tokenization Precompile](tokenization-precompile/README.md)
- [Architecture](architecture.md)
- [Security](tokenization-precompile/security.md)
- [Network](../README.md)
## File: chain/evm/gamm-precompile/README.md
---
description: "The GAMM precompile at 0x0000000000000000000000000000000000001002: join and exit pools, swap, swap with an IBC transfer, create and query pools from Solidity."
---
# GAMM Precompile
The GAMM precompile gives Solidity contracts access to the `x/gamm` liquidity pools: join, exit, swap (single and multi-hop), swap with an IBC transfer, create a pool, and read pool state. Address: `0x0000000000000000000000000000000000001002`.
## Example
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/IGammPrecompile.sol";
import "./libraries/GammJSONHelpers.sol";
contract MyPoolContract {
IGammPrecompile constant GAMM =
IGammPrecompile(0x0000000000000000000000000000000000001002);
// Join a pool
function joinPool(
uint64 poolId,
uint256 shareOutAmount,
string memory tokenInMaxsJson // e.g. [{"denom":"ubadge","amount":"1000000000"}]
) external returns (uint256 sharesReceived) {
string memory msgJson = GammJSONHelpers.joinPoolJSON(
poolId,
shareOutAmount,
tokenInMaxsJson
);
(sharesReceived, ) = GAMM.joinPool(msgJson);
return sharesReceived;
}
// Swap tokens
function swap(
string memory routesJson, // e.g. [{"pool_id":"1","token_out_denom":"badgeslp:64:utoken"}]
string memory tokenInJson, // e.g. {"denom":"ubadge","amount":"1000000000"}
uint256 minTokenOutAmount
) external returns (uint256 tokenOutAmount) {
string memory msgJson = GammJSONHelpers.swapExactAmountInJSON(
routesJson,
tokenInJson,
minTokenOutAmount,
"[]" // affiliates
);
return GAMM.swapExactAmountIn(msgJson);
}
}
```
## How It Works
Every method takes one `string calldata msgJson`. The JSON is the protobuf JSON of the matching `x/gamm` message or query. The precompile sets `sender` from `msg.sender`, converts any `0x` affiliate addresses to bech32, runs `ValidateBasic`, and calls the keeper.
```solidity
// Correct: JSON string
string memory json = GammJSONHelpers.joinPoolJSON(poolId, shareOutAmount, tokenInMaxsJson);
(uint256 shares, ) = GAMM.joinPool(json);
// Wrong: struct parameters (old interface)
GAMM.joinPool(poolId, shareOutAmount, tokenInMaxs);
```
Use `GammJSONHelpers` to build the strings:
```solidity
import "./libraries/GammJSONHelpers.sol";
// Simple operations
string memory json = GammJSONHelpers.getPoolJSON(poolId);
bytes memory pool = GAMM.getPool(json);
// Complex operations
string memory swapJson = GammJSONHelpers.swapExactAmountInJSON(
'[{"pool_id":"1","token_out_denom":"badgeslp:64:utoken"}]',
'{"denom":"ubadge","amount":"1000000000"}',
60,
"[]"
);
uint256 amountOut = GAMM.swapExactAmountIn(swapJson);
```
Field names in the JSON are snake_case (`pool_id`, `share_out_amount`, `token_in_maxs`). See [Gotchas](gotchas.md#field-names-are-snake_case).
## Methods
Transactions:
| Method | Returns |
| --- | --- |
| `joinPool(msgJson)` | `(uint256 shareOutAmount, Coin[] tokenIn)` |
| `exitPool(msgJson)` | `Coin[] tokenOut` |
| `swapExactAmountIn(msgJson)` | `uint256 tokenOutAmount` |
| `swapExactAmountInWithIBCTransfer(msgJson)` | `uint256 tokenOutAmount` |
| `createPool(msgJson)` | `uint256 poolId` |
Queries:
| Method | Returns |
| --- | --- |
| `getPool(msgJson)` | `bytes` (protobuf pool) |
| `getPools(msgJson)` | `bytes` (protobuf pool list) |
| `getPoolType(msgJson)` | `string` |
| `calcJoinPoolNoSwapShares(msgJson)` | `(Coin[] tokensOut, uint256 sharesOut)` |
| `calcExitPoolCoinsFromShares(msgJson)` | `Coin[] tokensOut` |
| `calcJoinPoolShares(msgJson)` | `(uint256 shareOutAmount, Coin[] tokensOut)` |
| `getPoolParams(msgJson)` | `bytes` (protobuf params) |
| `getTotalShares(msgJson)` | `Coin totalShares` |
| `getTotalLiquidity(msgJson)` | `Coin[] liquidity` |
Signatures and JSON shapes: [API reference](api.md).
## Patterns
### Join a Pool
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/IGammPrecompile.sol";
import "./libraries/GammJSONHelpers.sol";
import "./types/GammTypes.sol";
contract JoinPoolExample {
IGammPrecompile constant GAMM =
IGammPrecompile(0x0000000000000000000000000000000000001002);
function joinPool(
uint64 poolId,
uint256 desiredShares,
string memory denom1,
uint256 amount1,
string memory denom2,
uint256 amount2
) external returns (uint256 sharesReceived) {
GammTypes.Coin[] memory maxIn = new GammTypes.Coin[](2);
maxIn[0] = GammTypes.Coin(denom1, amount1);
maxIn[1] = GammTypes.Coin(denom2, amount2);
string memory msgJson = GammJSONHelpers.joinPoolJSON(
poolId,
desiredShares,
GammJSONHelpers.coinsToJson(maxIn)
);
(sharesReceived, ) = GAMM.joinPool(msgJson);
return sharesReceived;
}
}
```
### Exit a Pool
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/IGammPrecompile.sol";
import "./libraries/GammJSONHelpers.sol";
import "./types/GammTypes.sol";
contract ExitPoolExample {
IGammPrecompile constant GAMM =
IGammPrecompile(0x0000000000000000000000000000000000001002);
function exitPool(
uint64 poolId,
uint256 shareAmount,
string memory tokenOutMinsJson
) external returns (uint256[] memory amountsOut) {
string memory msgJson = GammJSONHelpers.exitPoolJSON(
poolId,
shareAmount,
tokenOutMinsJson
);
GammTypes.Coin[] memory tokensOut = GAMM.exitPool(msgJson);
// Convert to array
amountsOut = new uint256[](tokensOut.length);
for (uint i = 0; i < tokensOut.length; i++) {
amountsOut[i] = tokensOut[i].amount;
}
return amountsOut;
}
}
```
### Single-Hop Swap
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/IGammPrecompile.sol";
import "./libraries/GammJSONHelpers.sol";
import "./types/GammTypes.sol";
contract SingleHopSwapExample {
IGammPrecompile constant GAMM =
IGammPrecompile(0x0000000000000000000000000000000000001002);
function swapTokens(
uint64 poolId,
string memory tokenInDenom,
uint256 tokenInAmount,
string memory tokenOutDenom,
uint256 minTokenOutAmount
) external returns (uint256 tokenOutAmount) {
GammTypes.SwapAmountInRoute[] memory routes = new GammTypes.SwapAmountInRoute[](1);
routes[0] = GammTypes.SwapAmountInRoute(poolId, tokenOutDenom);
string memory msgJson = GammJSONHelpers.swapExactAmountInJSON(
GammJSONHelpers.swapRoutesToJson(routes),
GammJSONHelpers.coinToJson(GammTypes.Coin(tokenInDenom, tokenInAmount)),
minTokenOutAmount,
"[]"
);
return GAMM.swapExactAmountIn(msgJson);
}
}
```
### Multi-Hop Swap
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/IGammPrecompile.sol";
import "./libraries/GammJSONHelpers.sol";
import "./types/GammTypes.sol";
contract MultiHopSwapExample {
IGammPrecompile constant GAMM =
IGammPrecompile(0x0000000000000000000000000000000000001002);
function multiHopSwap(
uint64[] memory poolIds,
string[] memory tokenOutDenoms, // one per hop
string memory tokenInDenom,
uint256 tokenInAmount,
uint256 minTokenOutAmount
) external returns (uint256 finalAmountOut) {
GammTypes.SwapAmountInRoute[] memory routes = new GammTypes.SwapAmountInRoute[](poolIds.length);
for (uint i = 0; i < poolIds.length; i++) {
routes[i] = GammTypes.SwapAmountInRoute(poolIds[i], tokenOutDenoms[i]);
}
string memory msgJson = GammJSONHelpers.swapExactAmountInJSON(
GammJSONHelpers.swapRoutesToJson(routes),
GammJSONHelpers.coinToJson(GammTypes.Coin(tokenInDenom, tokenInAmount)),
minTokenOutAmount,
"[]"
);
return GAMM.swapExactAmountIn(msgJson);
}
}
```
## Helper Library
`GammJSONHelpers` builds JSON for every method:
| Group | Functions |
| --- | --- |
| Transactions | `joinPoolJSON`, `exitPoolJSON`, `swapExactAmountInJSON`, `swapExactAmountInWithIBCTransferJSON` |
| Queries | `getPoolJSON`, `getPoolsJSON`, `getPoolTypeJSON`, `calcJoinPoolNoSwapSharesJSON`, `calcExitPoolCoinsFromSharesJSON`, `calcJoinPoolSharesJSON`, `getPoolParamsJSON`, `getTotalSharesJSON`, `getTotalLiquidityJSON` |
| Building blocks | `coinToJson`, `coinsToJson`, `swapRouteToJson`, `swapRoutesToJson`, `affiliateToJson`, `affiliatesToJson`, `ibcTransferInfoToJson`, `paginationToJson` |
`GammHelpers`, `GammBuilders`, `GammWrappers`, `GammDecoders`, and `GammErrors` in the same folder add struct builders, typed wrappers, and validation.
## Return Values
`uint256` directly:
```solidity
uint256 tokenOutAmount = GAMM.swapExactAmountIn(swapJson);
```
Tuples:
```solidity
(uint256 shares, GammTypes.Coin[] memory tokens) = GAMM.joinPool(joinJson);
```
Protobuf bytes from `getPool`, `getPools`, and `getPoolParams`. Decode off-chain with the TypeScript SDK or emit them in an event for an indexer.
## Security
- `sender` is set from `msg.sender` on the Go side and cannot be spoofed. A contract can only spend its own balances.
- Invalid JSON reverts with a structured error.
- Set minimum and maximum amounts on every call. See [Gotchas](gotchas.md#slippage-protection).
- Use the helper functions so the JSON matches the schema.
## Best Practices
Use the helpers:
```solidity
// Good: type-safe and readable
string memory json = GammJSONHelpers.joinPoolJSON(poolId, shareOutAmount, tokenInMaxsJson);
// Bad: error-prone manual construction
string memory json = string(abi.encodePacked(
'{"pool_id":"', GammJSONHelpers.uintToString(poolId),
'","share_out_amount":"', GammJSONHelpers.uintToString(shareOutAmount),
'","token_in_maxs":', tokenInMaxsJson, '}'
));
```
Validate before building JSON:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/IGammPrecompile.sol";
import "./libraries/GammJSONHelpers.sol";
import "./types/GammTypes.sol";
contract ValidatedJoinExample {
IGammPrecompile constant GAMM =
IGammPrecompile(0x0000000000000000000000000000000000001002);
function joinPool(uint64 poolId, uint256 shares, string memory tokenInMaxsJson) external {
require(poolId > 0, "Invalid pool ID");
require(shares > 0, "Invalid share amount");
// Now build JSON
string memory json = GammJSONHelpers.joinPoolJSON(poolId, shares, tokenInMaxsJson);
GAMM.joinPool(json);
}
}
```
Handle slippage:
```solidity
// Always specify minimum/maximum amounts
string memory exitJson = GammJSONHelpers.exitPoolJSON(
poolId,
shareAmount,
minTokenOutAmountsJson // Protect against slippage
);
```
## Related
- [GAMM Precompile API](api.md)
- [Gotchas](gotchas.md)
- [GAMM module](../../modules/gamm/README.md)
- [Example contracts](https://github.com/BitBadges/bitbadgeschain/tree/master/contracts/examples)
## File: chain/evm/gamm-precompile/api.md
---
description: "GAMM precompile reference: the 14-function Solidity interface, the snake_case JSON for each message and query, gas constants, and error codes."
---
# GAMM Precompile API
Reference for every function on the GAMM precompile at `0x0000000000000000000000000000000000001002`: 5 transactions and 9 queries, regenerated from `x/gamm/precompile/abi.json`.
## Interface
```solidity
interface IGammPrecompile {
// Transaction methods
function joinPool(string calldata msgJson) external returns (uint256 shareOutAmount, Coin[] memory tokenIn);
function exitPool(string calldata msgJson) external returns (Coin[] memory tokenOut);
function swapExactAmountIn(string calldata msgJson) external returns (uint256 tokenOutAmount);
function swapExactAmountInWithIBCTransfer(string calldata msgJson) external returns (uint256 tokenOutAmount);
function createPool(string calldata msgJson) external returns (uint256 poolId);
// Query methods
function getPool(string calldata msgJson) external view returns (bytes memory pool);
function getPools(string calldata msgJson) external view returns (bytes memory pools);
function getPoolType(string calldata msgJson) external view returns (string memory poolType);
function calcJoinPoolNoSwapShares(string calldata msgJson) external view returns (Coin[] memory tokensOut, uint256 sharesOut);
function calcExitPoolCoinsFromShares(string calldata msgJson) external view returns (Coin[] memory tokensOut);
function calcJoinPoolShares(string calldata msgJson) external view returns (uint256 shareOutAmount, Coin[] memory tokensOut);
function getPoolParams(string calldata msgJson) external view returns (bytes memory params);
function getTotalShares(string calldata msgJson) external view returns (Coin memory totalShares);
function getTotalLiquidity(string calldata msgJson) external view returns (Coin[] memory liquidity);
}
struct Coin {
string denom;
uint256 amount;
}
```
## JSON Formats
The precompile decodes `msgJson` with Go's `encoding/json` into the `x/gamm` protobuf structs, so keys are the snake_case protobuf field names. All integers are strings. `sender` is always overwritten with the caller.
### joinPool
`MsgJoinPool`. Buy `share_out_amount` LP shares, paying at most `token_in_maxs`.
```json
{
"pool_id": "1",
"share_out_amount": "1000000",
"token_in_maxs": [{"denom": "ubadge", "amount": "1000000000"}]
}
```
Returns the shares minted and the coins actually taken.
### exitPool
`MsgExitPool`. Burn `share_in_amount` LP shares, receiving at least `token_out_mins`.
```json
{
"pool_id": "1",
"share_in_amount": "1000000",
"token_out_mins": [{"denom": "ubadge", "amount": "0"}]
}
```
Returns the coins paid out.
### swapExactAmountIn
`MsgSwapExactAmountIn`. Swap a fixed input through one or more pools.
```json
{
"routes": [{"pool_id": "1", "token_out_denom": "badgeslp:64:utoken"}],
"token_in": {"denom": "ubadge", "amount": "1000000000"},
"token_out_min_amount": "60",
"affiliates": [{"address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf", "basis_points_fee": "10"}]
}
```
Each `affiliates` entry carries an `address` (bech32 or `0x`, converted to bech32 on the Go side) and a `basis_points_fee` integer string, as in the example above. Returns the output amount.
### swapExactAmountInWithIBCTransfer
`MsgSwapExactAmountInWithIBCTransfer`. Same as `swapExactAmountIn` plus an `ibc_transfer_info` that forwards the output over IBC.
```json
{
"routes": [
{"pool_id": "1", "token_out_denom": "ubadge"},
{"pool_id": "4", "token_out_denom": "ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701"}
],
"token_in": {"denom": "badgeslp:64:utoken", "amount": "10"},
"token_out_min_amount": "900000",
"ibc_transfer_info": {
"source_channel": "channel-3",
"receiver": "cosmos1py4mfpg6uf59qkyzg0nmau322c5873ee8df8qg",
"memo": "",
"timeout_timestamp": "1788825600000000000"
},
"affiliates": []
}
```
Returns the output amount before the transfer.
### createPool
`MsgCreateBalancerPool`. Creates a weighted (Balancer) pool. Fees are decimal strings.
```json
{
"pool_params": {"swap_fee": "0.003", "exit_fee": "0"},
"pool_assets": [
{"token": {"denom": "ubadge", "amount": "1000000"}, "weight": "1"},
{"token": {"denom": "badgeslp:64:utoken", "amount": "1000000"}, "weight": "1"}
]
}
```
Returns the new pool ID. See [GAMM messages](../../modules/gamm/messages.md) for the pool creation rules.
### Queries
| Method | Request type | JSON |
| --- | --- | --- |
| `getPool` | `QueryPoolRequest` | `{"pool_id": "1"}` |
| `getPools` | `QueryPoolsRequest` | `{"pagination": {"limit": "50"}}` (optional) |
| `getPoolType` | `QueryPoolTypeRequest` | `{"pool_id": "1"}` |
| `calcJoinPoolNoSwapShares` | `QueryCalcJoinPoolNoSwapSharesRequest` | `{"pool_id": "1", "tokens_in": [{"denom": "ubadge", "amount": "1000000"}]}` |
| `calcExitPoolCoinsFromShares` | `QueryCalcExitPoolCoinsFromSharesRequest` | `{"pool_id": "1", "share_in_amount": "1000000"}` |
| `calcJoinPoolShares` | `QueryCalcJoinPoolSharesRequest` | `{"pool_id": "1", "tokens_in": [{"denom": "ubadge", "amount": "1000000"}]}` |
| `getPoolParams` | `QueryPoolParamsRequest` | `{"pool_id": "1"}` |
| `getTotalShares` | `QueryTotalSharesRequest` | `{"pool_id": "1"}` |
| `getTotalLiquidity` | `QueryTotalLiquidityRequest` | `{}` |
`getPool`, `getPools`, and `getPoolParams` return the protobuf-encoded response as `bytes`. Decode off-chain. The other queries return typed values. Query request types run `ValidateBasic` before the keeper call.
## Gas
`RequiredGas` charges a per-method base plus a fixed buffer: `+200,000` for transactions, `+50,000` for queries. The constants come from `x/gamm/precompile/gas.go`.
| Method | Base gas | Charged up front |
| --- | --- | --- |
| `joinPool` | 10,000 | 210,000 |
| `exitPool` | 10,000 | 210,000 |
| `swapExactAmountIn` | 10,000 | 210,000 |
| `swapExactAmountInWithIBCTransfer` | 15,000 | 215,000 |
| `createPool` | 15,000 | 215,000 |
| `getPool` | 3,000 | 53,000 |
| `getPools` | 5,000 | 55,000 |
| `getPoolType` | 2,000 | 52,000 |
| `calcJoinPoolNoSwapShares` | 5,000 | 55,000 |
| `calcExitPoolCoinsFromShares` | 5,000 | 55,000 |
| `calcJoinPoolShares` | 5,000 | 55,000 |
| `getPoolParams` | 3,000 | 53,000 |
| `getTotalShares` | 3,000 | 53,000 |
| `getTotalLiquidity` | 5,000 | 55,000 |
Per-element constants for dynamic estimates (`CalculateDynamicGas`): 5,000 per route, 2,000 per coin, 3,000 per affiliate, 10 per memo byte. Swaps typically use 150k to 300k gas in total depending on pool complexity; the buffer exists so `eth_estimateGas` converges.
## Error Codes
Errors revert with `precompile error [code=N]: message: details`.
| Code | Name | Description | Mapped from |
| --- | --- | --- | --- |
| 1 | `InvalidInput` | Invalid JSON, failed `ValidateBasic`, zero pool ID, empty routes, non-positive amount | `ErrNotPositiveRequireAmount`, `ErrEmptyRoutes` |
| 2 | `PoolNotFound` | Pool not found | `ErrPoolNotFound` |
| 3 | `SwapFailed` | Swap failed: limit breached, denom not in pool, math approximation | `ErrLimitMaxAmount`, `ErrLimitMinAmount`, `ErrDenomNotFoundInPool`, `ErrInvalidMathApprox` |
| 4 | `QueryFailed` | Query failed | |
| 5 | `InternalError` | Internal error | |
| 6 | `Unauthorized` | Unauthorized (zero caller) | |
| 7 | `JoinPoolFailed` | Join pool failed | |
| 8 | `ExitPoolFailed` | Exit pool failed | `ErrTooManyTokensOut` |
| 9 | `IBCTransferFailed` | IBC transfer failed | |
## Events
Successful transactions emit `precompile_join_pool`, `precompile_exit_pool`, and the matching swap and create events with `module=evm_precompile`, `pool_id`, `sender`, and the amounts.
## Related
- [GAMM Precompile](README.md)
- [Gotchas](gotchas.md)
- [GAMM module messages](../../modules/gamm/messages.md)
- [GAMM module queries](../../modules/gamm/queries.md)
## File: chain/evm/gamm-precompile/gotchas.md
---
description: "Mistakes that break GAMM precompile calls: string pool IDs, integer amounts, snake_case keys, tuple and bytes returns, slippage, multi-hop routes."
---
# GAMM Precompile Gotchas
This page lists the errors developers hit most on the GAMM precompile and the fix for each. Read it after the [API reference](api.md).
## Pool IDs Are Strings in JSON
Pool IDs are `uint64` in Go, but the JSON must carry them as strings.
```solidity
// Correct: pool ID as a string in JSON
string memory json = string(abi.encodePacked(
'{"pool_id":"1",', // Note: "1" not 1
'"share_out_amount":"1000000",',
'"token_in_maxs":[{"denom":"ubadge","amount":"1000000000"}]}'
));
// Wrong: pool ID as a number
string memory json = string(abi.encodePacked(
'{"pool_id":1,', // This will fail!
'"share_out_amount":"1000000",',
'"token_in_maxs":[{"denom":"ubadge","amount":"1000000000"}]}'
));
```
The helper converts for you:
```solidity
// Use helper to ensure correct format
string memory json = GammJSONHelpers.joinPoolJSON(
1, // uint64 poolId - helper converts to "1" in JSON
shareOutAmount,
tokenInMaxsJson
);
```
Why: protobuf JSON encodes 64-bit numbers as strings so large values keep their precision, and the same format is used everywhere on the Cosmos side.
## Field Names Are snake_case
The GAMM precompile decodes JSON with Go's `encoding/json` against the protobuf struct tags, which are snake_case (`pool_id`, `share_out_amount`, `token_in_maxs`, `token_out_mins`, `token_in`, `token_out_min_amount`, `ibc_transfer_info`). A camelCase key does not match its snake_case tag, so the field silently stays at its zero value and validation then fails (for example `poolId cannot be zero`). See the full field list on the [API page](api.md#json-formats).
## Amounts Are Integer Strings
Cosmos SDK computes with `sdk.Dec` (18 decimal places of fixed-point precision), but JSON amounts are strings of integers in the smallest unit. No decimal point.
```solidity
// Correct: amount as a string
string memory json = string(abi.encodePacked(
'{"token_in":{',
'"denom":"ubadge",',
'"amount":"1000000000"', // String, no decimal point
'}}'
));
// Wrong: amount as a number or with a decimal
string memory json = string(abi.encodePacked(
'{"token_in":{',
'"denom":"ubadge",',
'"amount":1000000000', // Number - will fail!
// or
'"amount":"1.0"', // Decimal point - will fail!
'}}'
));
```
A conversion helper:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/IGammPrecompile.sol";
import "./libraries/GammJSONHelpers.sol";
import "./types/GammTypes.sol";
contract AmountExample {
IGammPrecompile constant GAMM =
IGammPrecompile(0x0000000000000000000000000000000000001002);
// Convert human-readable amount to smallest unit string
function amountToJson(uint256 amount, uint8 decimals) internal pure returns (string memory) {
// amount is already in smallest unit (e.g., 1000000000 for 1 token with 9 decimals)
return GammJSONHelpers.uintToString(amount);
}
// Example: 1 BADGE with 9 decimals = 1000000000
string memory amountJson = amountToJson(1e9, 9); // Returns "1000000000"
}
```
Three common mistakes:
```solidity
// Wrong
'"amount":"1.5"'
// Correct
'"amount":"1500000000"' // 1.5 tokens with 9 decimals
```
```solidity
// Wrong
'"amount":1000000000'
// Correct
'"amount":"1000000000"'
```
```solidity
// Wrong: assumes 1 token = 1 unit
uint256 amount = 1; // This is 0.000000001 tokens with 9 decimals!
// Correct: account for decimals
uint256 amount = 1 * 10**9; // 1 token with 9 decimals
```
Native `BADGE` is 9 decimals on the Cosmos side (`ubadge`). Alias denoms for collection tokens use the decimals declared in the collection's denom units. See [Developer Guide](../developer-guide.md#decimals-9-on-the-cosmos-side-18-on-the-evm-side).
### Every Amount Field Is a String
```json
{
"pool_id": "1",
"share_out_amount": "1000000",
"token_in_maxs": [
{
"denom": "ubadge",
"amount": "1000000000"
}
]
}
```
```json
{
"pool_id": "1",
"share_out_amount": 1000000,
"token_in_maxs": [
{
"denom": "ubadge",
"amount": 1000000000
}
]
}
```
The first block is valid; the second fails on both numbers. The helpers convert `uint256` values to strings:
```solidity
// Helper converts uint256 to string automatically
string memory json = GammJSONHelpers.joinPoolJSON(
poolId,
shareOutAmount, // uint256 - helper converts to string
tokenInMaxsJson // built with GammJSONHelpers.coinsToJson
);
```
## Return Values Differ by Method
Tuple returns:
```solidity
// Method signature
function joinPool(string calldata msgJson)
external
returns (uint256 shareOutAmount, GammTypes.Coin[] memory tokenIn);
// Usage
(uint256 shares, GammTypes.Coin[] memory tokens) = GAMM.joinPool(json);
```
Coin arrays:
```solidity
struct Coin {
string denom;
uint256 amount;
}
// Access values
for (uint i = 0; i < tokens.length; i++) {
string memory denom = tokens[i].denom;
uint256 amount = tokens[i].amount;
}
```
Protobuf bytes (`getPool`, `getPools`, `getPoolParams`):
```solidity
bytes memory poolBytes = GAMM.getPool(json);
// Decode off-chain using TypeScript SDK or protobuf library
```
## Slippage Protection
Always set a limit. Without one, a price move between simulation and execution changes what you pay or receive.
Join pool: `token_in_maxs` caps what you pay.
```solidity
// Correct: specify maximum amounts you are willing to pay
string memory json = GammJSONHelpers.joinPoolJSON(
poolId,
desiredShares,
GammJSONHelpers.coinsToJson(maxAmounts) // Maximum you are willing to pay per denom
);
// Wrong: no slippage protection
// If pool price changes, you might pay more than expected
```
Exit pool: `token_out_mins` floors what you receive.
```solidity
// Correct: specify minimum amounts you want to receive
string memory json = GammJSONHelpers.exitPoolJSON(
poolId,
shareAmount,
GammJSONHelpers.coinsToJson(minAmounts) // Minimum you want to receive per denom
);
// Wrong: no slippage protection
// If pool price changes, you might receive less than expected
```
Swap: `token_out_min_amount` floors the output.
```solidity
// Correct: specify minimum output amount
string memory json = GammJSONHelpers.swapExactAmountInJSON(
routesJson,
tokenInJson,
minTokenOutAmount, // Minimum you want to receive
affiliatesJson
);
// Wrong: no slippage protection
// If pool price changes, you might receive less than expected
```
A 1% tolerance:
```solidity
// Example: 1% slippage tolerance
uint256 tokenInAmount = 1000000000; // 1 token
uint256 minTokenOutAmount = tokenInAmount * 99 / 100; // 0.99 tokens (1% slippage)
string memory json = GammJSONHelpers.swapExactAmountInJSON(
routesJson,
tokenInJson,
minTokenOutAmount,
affiliatesJson
);
```
Use `calcJoinPoolShares`, `calcJoinPoolNoSwapShares`, and `calcExitPoolCoinsFromShares` to simulate first, then derive the limit from the result.
## Multi-Hop Swaps
Slippage compounds across hops. Two hops at 1% each need about 2% total tolerance.
```solidity
// Multi-hop: A -> B -> C
// Slippage accumulates: 1% per hop = ~2% total for 2 hops
uint256 tokenInAmount = 1000000000;
uint256 minTokenOutAmount = tokenInAmount * 98 / 100; // 2% slippage tolerance
string memory json = GammJSONHelpers.swapExactAmountInJSON(
routesJson, // two routes: pool 1 -> ubadge, pool 4 -> ATOM
tokenInJson, // {"denom":"badgeslp:64:utoken","amount":"1000000000"}
minTokenOutAmount,
affiliatesJson
);
```
Route format:
```json
{
"routes": [
{
"pool_id": "1",
"token_out_denom": "ubadge"
},
{
"pool_id": "4",
"token_out_denom": "ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701"
}
],
"token_in": {
"denom": "badgeslp:64:utoken",
"amount": "10"
},
"token_out_min_amount": "900000",
"affiliates": []
}
```
Each route names the pool and the denom that comes out of it. The input denom of hop N+1 is the output denom of hop N.
## Common Error Messages
### `poolId cannot be zero`
The pool ID is 0 or the JSON key did not match.
```solidity
// Correct
require(poolId > 0, "Invalid pool ID");
string memory json = GammJSONHelpers.joinPoolJSON(poolId, shareOutAmount, tokenInMaxsJson);
// Wrong
uint64 poolId = 0; // Will fail validation
```
### `amount must be greater than zero`
The amount is zero or not formatted as an integer string.
```solidity
// Correct
require(amount > 0, "Amount must be positive");
string memory amountStr = GammJSONHelpers.uintToString(amount);
// Wrong
uint256 amount = 0; // Will fail validation
```
### `invalid JSON syntax`
The JSON is malformed or uses the wrong types.
```solidity
// Correct: use helper functions
string memory json = GammJSONHelpers.joinPoolJSON(poolId, shareOutAmount, tokenInMaxsJson);
// Wrong: manual construction prone to errors
string memory json = string(abi.encodePacked(
'{"pool_id":', GammJSONHelpers.uintToString(poolId), // Missing quotes: the number is not a string
',"share_out_amount":', GammJSONHelpers.uintToString(shareOutAmount), '}'
));
```
### `denom cannot be empty`
```solidity
// Correct
require(bytes(denom).length > 0, "Denom cannot be empty");
string memory json = GammJSONHelpers.joinPoolJSON(poolId, shareAmount, tokenInMaxsJson);
// Wrong
string memory denom = ""; // Will fail validation
```
## Checklist
1. Use the helper functions for JSON. They handle type conversion.
2. Validate pool IDs, amounts, and denoms before building JSON.
3. Set `token_in_maxs`, `token_out_mins`, or `token_out_min_amount` on every call.
4. Convert human-readable amounts to the smallest unit.
5. Match the return type: tuple, `Coin[]`, `Coin`, `uint256`, `string`, or `bytes`.
6. Test with small amounts first.
## Debugging
Log the JSON before sending:
```solidity
import "hardhat/console.sol";
string memory json = GammJSONHelpers.joinPoolJSON(poolId, shareOutAmount, tokenInMaxsJson);
console.log("JSON:", json); // Verify format
```
Check return values:
```solidity
(uint256 shares, GammTypes.Coin[] memory tokens) = GAMM.joinPool(json);
console.log("Shares:", shares);
for (uint i = 0; i < tokens.length; i++) {
console.log("Token:", tokens[i].denom, tokens[i].amount);
}
```
Use a static call to test without sending a transaction:
```typescript
// Test without sending a transaction (ethers.js v6 against the precompile address)
const gamm = new ethers.Contract("0x0000000000000000000000000000000000001002", gammAbi, signer);
const [shares] = await gamm.joinPool.staticCall(json);
console.log("Expected shares:", shares);
```
## Related
- [GAMM Precompile](README.md)
- [GAMM Precompile API](api.md)
- [GAMM module](../../modules/gamm/README.md)
- [Developer Guide](../developer-guide.md)
## File: chain/evm/rpc-endpoints.md
---
description: "Public EVM JSON-RPC endpoints for BitBadges, supported methods, ethers.js, Hardhat and Foundry config, and running your own JSON-RPC node."
---
# EVM RPC Endpoints
BitBadges exposes Ethereum-compatible JSON-RPC endpoints, so MetaMask, ethers.js, web3.js, Hardhat, and Foundry work unchanged. This page lists the URLs and the node settings behind them.
```typescript
import { ethers } from "ethers";
// Connect to BitBadges mainnet EVM RPC
const provider = new ethers.JsonRpcProvider("https://evm-rpc.bitbadges.io");
// Get the current block number
const blockNumber = await provider.getBlockNumber();
console.log("Current block:", blockNumber);
// Get balance of an address
const balance = await provider.getBalance("0x0bc63cfe31d5218eb414b142c799e20964a54a1a");
console.log("Balance:", ethers.formatEther(balance), "BADGE");
```
## Endpoints
| Network | Type | URL | Use for |
| --- | --- | --- | --- |
| Mainnet | EVM JSON-RPC | `https://evm-rpc.bitbadges.io` | MetaMask, Hardhat, ethers.js (chain ID `50024`) |
| Mainnet | Cosmos RPC | `https://rpc.bitbadges.io` | Cosmos SDK queries and broadcasts |
| Mainnet | Cosmos REST/LCD | `https://lcd.bitbadges.io` | REST queries |
| Testnet | EVM JSON-RPC | `https://evm-rpc-testnet.bitbadges.io` | Chain ID `50025`. Offline as of September 2026 |
| Testnet | Cosmos RPC | `https://rpc-testnet.bitbadges.io` | Offline |
| Testnet | Cosmos REST/LCD | `https://lcd-testnet.bitbadges.io` | Offline |
EVM tools use the `evm-rpc*.bitbadges.io` URLs. Cosmos tools (cosmjs, LCD queries, `bb`) use `rpc*.bitbadges.io` or `lcd*.bitbadges.io`. Mixing them up is the most common connection failure. Testnet status: [Testnet](../testnet.md).
web3.js works the same way:
```javascript
// Using web3.js
const Web3 = require('web3');
const web3 = new Web3('https://evm-rpc.bitbadges.io');
// Get the current block number
const blockNumber = await web3.eth.getBlockNumber();
console.log("Current block:", blockNumber);
```
## MetaMask
Settings > Networks > Add Network:
| Field | Mainnet | Testnet |
| --- | --- | --- |
| Network name | BitBadges Mainnet | BitBadges Testnet |
| RPC URL | `https://evm-rpc.bitbadges.io` | `https://evm-rpc-testnet.bitbadges.io` |
| Chain ID | `50024` | `50025` |
| Currency symbol | `BADGE` | `BADGE` |
| Block explorer URL | `https://explorer.bitbadges.io` (optional) | none |
## Supported JSON-RPC Methods
The endpoints serve the standard `eth`, `net`, and `web3` namespaces, including:
| Group | Methods |
| --- | --- |
| Account | `eth_accounts`, `eth_getBalance`, `eth_getTransactionCount` |
| Block | `eth_blockNumber`, `eth_getBlockByNumber`, `eth_getBlockByHash` |
| Transaction | `eth_sendTransaction`, `eth_sendRawTransaction`, `eth_getTransactionByHash`, `eth_getTransactionReceipt` |
| Contract | `eth_call`, `eth_estimateGas` |
| Event | `eth_getLogs`, `eth_newFilter`, `eth_newBlockFilter`, `eth_newPendingTransactionFilter` |
| State | `eth_getCode`, `eth_getStorageAt` |
| Network | `eth_chainId`, `net_version`, `net_listening` |
| Web3 | `web3_clientVersion`, `web3_sha3` |
## Deploy a Contract
```typescript
import { ethers } from "ethers";
import * as fs from "fs";
async function deploy() {
// Connect to BitBadges mainnet EVM RPC
const provider = new ethers.JsonRpcProvider("https://evm-rpc.bitbadges.io");
// Get deployer wallet
const privateKey = process.env.PRIVATE_KEY || "";
if (!privateKey) {
throw new Error("PRIVATE_KEY environment variable required");
}
const wallet = new ethers.Wallet(privateKey, provider);
console.log("Deployer address:", wallet.address);
// Check balance
const balance = await provider.getBalance(wallet.address);
console.log("Balance:", ethers.formatEther(balance), "BADGE");
// Deploy the Counter contract from the Hardhat artifact (no constructor args)
const artifact = JSON.parse(
fs.readFileSync("artifacts/contracts/Counter.sol/Counter.json", "utf8")
);
const contractFactory = new ethers.ContractFactory(
artifact.abi,
artifact.bytecode,
wallet
);
const contract = await contractFactory.deploy();
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log("Contract deployed at:", address);
}
```
## Interact with a Contract
```typescript
import { ethers } from "ethers";
async function interactWithContract() {
// Connect to BitBadges mainnet EVM RPC
const provider = new ethers.JsonRpcProvider("https://evm-rpc.bitbadges.io");
// Load contract
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // Counter deployed above
const counterAbi = [
"function count() view returns (uint256)",
"function increment()"
];
const contract = new ethers.Contract(contractAddress, counterAbi, provider);
// Read from contract
const value = await contract.count();
console.log("Value:", value);
// Write to contract (requires signer)
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const contractWithSigner = contract.connect(signer);
const tx = await contractWithSigner.increment();
await tx.wait();
console.log("Transaction confirmed:", tx.hash);
}
```
## Hardhat
```javascript
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.20",
networks: {
bitbadges: {
url: "https://evm-rpc.bitbadges.io",
chainId: 50024,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
bitbadgesTestnet: {
url: "https://evm-rpc-testnet.bitbadges.io",
chainId: 50025,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
};
```
## Foundry
```toml
[rpc_endpoints]
bitbadges = "https://evm-rpc.bitbadges.io"
bitbadgesTestnet = "https://evm-rpc-testnet.bitbadges.io"
[profile.default]
rpc_endpoints = ["bitbadges", "bitbadgesTestnet"]
```
## Rate Limits
The public endpoints may rate-limit to keep usage fair. For production traffic, run your own node, use a dedicated RPC provider, or cache and batch requests.
## Run Your Own JSON-RPC Node
Follow [Run a Node](../run-a-node.md) for the full node setup. The EVM-specific settings are in `app.toml`.
### Set the EVM Chain ID
{% hint style="warning" %}
The default `evm-chain-id` (`90123`) is the local-dev value. On mainnet or testnet it makes every MetaMask transaction fail. Set it before starting the node.
{% endhint %}
```toml
[evm]
# Set this to match your network's EVM chain ID
# Mainnet: 50024
# Testnet: 50025
# The default (90123) is the local-dev chain ID and causes MetaMask transaction failures on mainnet/testnet
evm-chain-id = 50024
```
`net_version` reports this value and EIP-155 signature verification uses it. If it does not match `eth_chainId`, wallets fail with an error like `incorrect chain-id; expected 90123, got 50024`. The "expected" value is whatever `evm-chain-id` is set to; `90123` is the local-dev default written by `bb init`.
### Enable JSON-RPC
```toml
[json-rpc]
enable = true
address = "0.0.0.0:8545" # Use 127.0.0.1 for local only
ws-address = "0.0.0.0:8546"
api = ["eth", "net", "web3"]
enable-indexer = true
```
### Configuration Reference
| Option | Default | Description |
| --- | --- | --- |
| `enable` | `false` | Enable JSON-RPC server |
| `address` | `127.0.0.1:8545` | HTTP listen address |
| `ws-address` | `127.0.0.1:8546` | WebSocket address |
| `api` | `eth,net,web3` | Enabled namespaces |
| `enable-indexer` | `false` | Custom tx indexer |
| `evm-timeout` | `5s` | `eth_call` timeout |
| `gas-cap` | `25000000` | Gas limit for calls |
| `txfee-cap` | `1.0` | Max tx fee (BADGE) |
| `filter-cap` | `200` | Max active filters |
| `block-range-cap` | `10000` | Max block range for logs |
| `logs-cap` | `10000` | Max log results |
| `batch-request-limit` | `1000` | Max batch size |
| `batch-response-max-size` | `25000000` | Max response bytes |
| `http-timeout` | `30s` | HTTP timeout |
| `http-idle-timeout` | `2m0s` | HTTP idle timeout |
| `max-open-connections` | `0` | Max connections (0 = unlimited) |
| `allow-unprotected-txs` | `false` | Allow non-EIP155 txs |
| `ws-origins` | `127.0.0.1,localhost` | WebSocket allowed origins |
## Related
- [Setup](setup.md)
- [Developer Guide](developer-guide.md)
- [Network](../README.md)
- [Run a Node](../run-a-node.md)
## File: chain/evm/send-manager-precompile.md
---
description: "The send manager precompile at 0x0000000000000000000000000000000000001003: one send method that moves native coins and alias denoms from a contract via x/bank."
---
# Send Manager Precompile
The send manager precompile lets a Solidity contract send native Cosmos coins, including alias denoms such as `badgeslp:...`, without ERC20 wrapping. Address: `0x0000000000000000000000000000000000001003`. Standard coin accounting stays in `x/bank`; alias denom balances stay in `x/tokenization`.
## Example
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ISendManagerPrecompile.sol";
contract TokenSender {
ISendManagerPrecompile constant SENDMANAGER =
ISendManagerPrecompile(0x0000000000000000000000000000000000001003);
function sendCoins(
string memory toAddress,
string memory denom,
uint256 amount
) external returns (bool) {
string memory msgJson = string(abi.encodePacked(
'{"to_address":"', toAddress,
'","amount":[{"denom":"', denom,
'","amount":"', _uintToString(amount), '"}]}'
));
return SENDMANAGER.send(msgJson);
}
function _uintToString(uint256 value) internal pure returns (string memory) {
if (value == 0) return "0";
uint256 temp = value;
uint256 digits;
while (temp != 0) { digits++; temp /= 10; }
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
}
```
From TypeScript with ethers.js:
```typescript
import { ethers } from "ethers";
const SENDMANAGER_ADDRESS = "0x0000000000000000000000000000000000001003";
const sendManagerABI = [
"function send(string memory msgJson) external returns (bool success)"
];
const sendManager = new ethers.Contract(SENDMANAGER_ADDRESS, sendManagerABI, signer);
const msgJson = JSON.stringify({
to_address: "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
amount: [{ denom: "ubadge", amount: "1000000000" }]
});
const tx = await sendManager.send(msgJson);
await tx.wait();
```
## Interface
The ABI has exactly one method.
```solidity
interface ISendManagerPrecompile {
/// @notice Send native Cosmos coins from the caller to a recipient
/// @param msgJson JSON string matching MsgSendWithAliasRouting protobuf format
/// @return success Whether the send succeeded
function send(string memory msgJson) external returns (bool success);
}
```
## JSON Format
The JSON is a `MsgSendWithAliasRouting` from `x/sendmanager`. The Go side decodes it with `encoding/json`, so use the snake_case field names.
```json
{
"to_address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"amount": [
{"denom": "ubadge", "amount": "1000000000"},
{"denom": "badgeslp:64:utoken", "amount": "5"}
]
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `to_address` | string | yes | Recipient. A `bb1` bech32 address, or a `0x` address (converted to bech32 on the Go side) |
| `amount` | Coin[] | yes | Coins to send. `amount` values are integer strings in the base unit (`ubadge` has 9 decimals) |
| `from_address` | string | no | Ignored. Always overwritten with the caller (`msg.sender`) |
## Behavior
- `from_address` is set from `msg.sender` after unmarshal, so a contract can only spend its own balance.
- `ValidateBasic` runs before the send. Empty or invalid coins and addresses fail with code 1.
- Alias denoms route through the tokenization module; standard denoms go through `x/bank`. See [Send manager module](../modules/send-manager.md) and [Alias Denoms](../../token-standard/ibc/alias-denoms.md).
- A successful send emits a `precompile_send` event with `from`, `to_address`, and `amount` attributes.
- Amounts are in Cosmos precision (9 decimals for `BADGE`), not the EVM's 18. See [Developer Guide](developer-guide.md#decimals-9-on-the-cosmos-side-18-on-the-evm-side).
## Gas
| Component | Gas |
| --- | --- |
| `GasSendBase` | 30,000 |
| Fixed buffer added by `RequiredGas` | 150,000 |
| Total charged up front | 180,000 |
| `GasPerCoin` (per entry in `amount`, defined in the precompile for dynamic estimates) | 2,000 |
The base is deducted before the precompile runs; the bank transfer itself uses the remaining gas of the call.
## Error Codes
| Code | Name | Description |
| --- | --- | --- |
| 1 | `InvalidInput` | Invalid JSON, coins, or address |
| 2 | `SendFailed` | The send operation failed |
| 3 | `InsufficientBalance` | The caller does not hold enough of a denom (`ErrInsufficientFunds`) |
| 4 | `InternalError` | Internal error |
| 5 | `Unauthorized` | Unauthorized operation |
Errors revert with the text `precompile error [code=N]: message: details`.
## Related
- [Send manager module](../modules/send-manager.md)
- [Cosmos SDK Precompiles](cosmos-precompiles.md) (bank queries)
- [GAMM Precompile](gamm-precompile/README.md)
## File: chain/evm/setup.md
---
description: "Chain IDs, RPC ports, starting a local node with JSON-RPC, MetaMask config, a minimal contract, deploy script, frontend hook, and troubleshooting."
---
# Setup
This page sets up a development environment for building dApps against the BitBadges EVM. It covers the chain configuration, wallet setup, and a minimal project.
## Chain IDs
| Network | EVM chain ID | Status |
| --- | --- | --- |
| Local development | `90123` | Default when you run a node yourself |
| Testnet | `50025` | Registered in ethereum-lists/chains. Offline, see [Testnet](../testnet.md) |
| Mainnet | `50024` | Registered in ethereum-lists/chains |
The values come from `app/params/constants.go`:
```go
// EVMChainIDMainnet is the EVM chain ID for BitBadges mainnet
// Chain ID: 50024 (claimed in ethereum-lists/chains registry)
// This should match the chain_id in genesis under app_state.evm.params.chain_config.chain_id
EVMChainIDMainnet = "50024"
// EVMChainIDTestnet is the EVM chain ID for BitBadges testnet
// Chain ID: 50025 (claimed in ethereum-lists/chains registry)
// This should match the chain_id in genesis under app_state.evm.params.chain_config.chain_id
EVMChainIDTestnet = "50025"
EVMChainIDLocalDev = "90123" // Default for local development/testing
```
The chain ID in your genesis file (`app_state.evm.params.chain_config.chain_id`) must match this value.
## RPC Interfaces
A node exposes two RPC interfaces:
| Interface | Port | Endpoint | Use |
| --- | --- | --- | --- |
| Tendermint RPC | 26657 | `http://localhost:26657` | Cosmos SDK queries and transactions |
| EVM JSON-RPC | 8545 | `http://localhost:8545` | Standard Ethereum JSON-RPC. Required for MetaMask, ethers.js, web3.js |
Public endpoints are listed on [EVM RPC Endpoints](rpc-endpoints.md).
## Start a Local Node with EVM
The EVM module is always enabled in the chain binary. You only need to enable the JSON-RPC server to reach it with Ethereum tooling.
```bash
# From the bitbadgeschain repository root
bb start --json-rpc.enable --json-rpc.address 0.0.0.0:8545
```
Or use the startup script, which enables JSON-RPC on port 8545:
```bash
# From the bitbadgeschain repository root
./start-chain.sh start
```
## MetaMask
Settings > Networks > Add Network, then enter:
| Field | Local | Mainnet |
| --- | --- | --- |
| Network name | BitBadges Local | BitBadges Mainnet |
| RPC URL | `http://localhost:8545` | `https://evm-rpc.bitbadges.io` |
| Chain ID | `90123` | `50024` |
| Currency symbol | `BADGE` | `BADGE` |
| Block explorer | (leave blank) | `https://explorer.bitbadges.io` |
For testnet the values are `BitBadges Testnet`, `https://evm-rpc-testnet.bitbadges.io`, chain ID `50025`, symbol `BADGE`. Testnet is offline as of September 2026.
Use port 8545 (EVM JSON-RPC), not 26657 (Tendermint RPC).
### Fund a Local Account
After starting your local chain, fund your MetaMask account:
```bash
# Your MetaMask address is a 0x address; its bech32 form is the same 20 bytes.
# Convert it once, then send from the local validator key (alice).
bb debug addr 092bb4851ae26850588243e7bef22a56287f4739
# Bech32 Acc: bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue
bb tx bank send \
$(bb keys show alice -a --keyring-backend test) \
bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue \
1000000000ubadge \
--chain-id bitbadges-1 \
--keyring-backend test \
--yes
```
A genesis account for the same address also works: add it with `bb genesis add-genesis-account bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue 1000000000000ubadge` before the first start.
## Minimal dApp
### Project Structure
```text
my-dapp/
├── contracts/ # Solidity contracts
│ ├── MyContract.sol
│ └── interfaces/
│ └── ITokenizationPrecompile.sol
├── scripts/ # Deployment scripts
│ └── deploy.ts
├── app/ # Frontend (Next.js, React, etc.)
│ └── page.tsx
└── package.json
```
### Contract Template
A minimal contract that uses the tokenization precompile through the JSON helper library:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract MyTokenContract {
// Precompile address
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
uint256 public collectionId;
constructor(uint256 _collectionId) {
collectionId = _collectionId;
}
function transfer(
address to,
uint256 amount,
uint256 tokenId
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId);
string memory ownershipTimesJson = TokenizationJSONHelpers.uintRangeToJson(
1, TokenizationJSONHelpers.FOREVER
);
return precompile.transferTokens(
TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIdsJson, ownershipTimesJson
)
);
}
function getBalance(address user, uint256 tokenId) external view returns (uint256) {
string memory json = TokenizationJSONHelpers.getBalanceAmountJSON(
collectionId, user, tokenId, block.timestamp * 1000
);
return precompile.getBalanceAmount(json);
}
}
```
The `TokenizationTypes.UintRange` struct form of this contract still exists in `TokenizationHelpers.sol` for building structs, but every precompile method takes JSON. Never pass `type(uint256).max` as a range end; use `FOREVER`.
### Deploy Script (ethers.js)
```typescript
import { ethers } from "ethers";
import * as fs from "fs";
async function deploy() {
// Connect to EVM JSON-RPC
const provider = new ethers.JsonRpcProvider("http://localhost:8545");
// Get deployer wallet
const privateKey = process.env.PRIVATE_KEY || "";
if (!privateKey) {
throw new Error("PRIVATE_KEY environment variable required");
}
const wallet = new ethers.Wallet(privateKey, provider);
console.log("Deployer address:", wallet.address);
// Check balance
const balance = await provider.getBalance(wallet.address);
console.log("Balance:", ethers.formatEther(balance), "BADGE");
// Deploy contract from the Hardhat artifact; the constructor takes the collection ID
const artifact = JSON.parse(
fs.readFileSync("artifacts/contracts/MyContract.sol/MyTokenContract.json", "utf8")
);
const collectionId = 1n;
const contractFactory = new ethers.ContractFactory(
artifact.abi,
artifact.bytecode,
wallet
);
const contract = await contractFactory.deploy(collectionId);
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log("Contract deployed at:", address);
// Save deployment info
fs.writeFileSync(
"deployed.json",
JSON.stringify({ address, abi: artifact.abi }, null, 2)
);
}
deploy().catch(console.error);
```
### Frontend Hook (React + ethers.js)
```typescript
import { ethers } from "ethers";
import { useState, useEffect } from "react";
import deployed from "../deployed.json"; // written by scripts/deploy.ts
export function useContract() {
const [contract, setContract] = useState(null);
const [provider, setProvider] = useState(null);
useEffect(() => {
if (typeof window.ethereum === "undefined") return;
const provider = new ethers.BrowserProvider(window.ethereum);
setProvider(provider);
// Load deployed contract
provider.getSigner().then((signer) => {
setContract(new ethers.Contract(deployed.address, deployed.abi, signer));
});
}, []);
const transfer = async (to: string, amount: bigint, tokenId: bigint) => {
if (!contract) throw new Error("Contract not loaded");
const tx = await contract.transfer(to, amount, tokenId);
await tx.wait();
};
return { contract, provider, transfer };
}
```
## Helper Library
`contracts/libraries/TokenizationHelpers.sol` builds Solidity structs and validates them:
- `UintRange` structs (single values, sequences, full ranges)
- `Balance`, `CollectionMetadata`, `TokenMetadata` structs
- Empty permission structures
- Range and balance validation
- Common patterns for ownership times and token IDs
```solidity
import "./libraries/TokenizationHelpers.sol";
contract MyContract {
function example() external {
// Create a single token ID range
TokenizationTypes.UintRange memory tokenId =
TokenizationHelpers.createSingleTokenIdRange(123);
// Create a full ownership time range (1 to max uint64)
TokenizationTypes.UintRange memory fullTime =
TokenizationHelpers.createFullOwnershipTimeRange();
// Create a token ID sequence (range from start to end)
TokenizationTypes.UintRange memory sequence =
TokenizationHelpers.createTokenIdSequence(1, 100);
// Create a UintRange array from arrays
uint256[] memory starts = new uint256[](2);
uint256[] memory ends = new uint256[](2);
starts[0] = 1; ends[0] = 10;
starts[1] = 20; ends[1] = 30;
TokenizationTypes.UintRange[] memory ranges =
TokenizationHelpers.createUintRangeArray(starts, ends);
}
}
```
See [TokenizationHelpers.sol](https://github.com/BitBadges/bitbadgeschain/blob/master/contracts/libraries/TokenizationHelpers.sol) for every helper. Convert structs to JSON with `TokenizationJSONHelpers` before calling the precompile.
## Contract Patterns
### Simple Transfer
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract SimpleTransfer {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function simpleTransfer(
uint256 collectionId,
address to,
uint256 amount,
uint256 tokenId
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(
1, TokenizationJSONHelpers.FOREVER
);
return precompile.transferTokens(
TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIdsJson, ownershipJson
)
);
}
}
```
### Batch Transfer
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract BatchTransfer {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function batchTransfer(
uint256 collectionId,
address[] calldata recipients,
uint256[] calldata amounts,
uint256 tokenId
) external returns (bool) {
require(recipients.length == amounts.length, "Arrays length mismatch");
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(
1, TokenizationJSONHelpers.FOREVER
);
// Transfer to each recipient
for (uint256 i = 0; i < recipients.length; i++) {
address[] memory singleRecipient = new address[](1);
singleRecipient[0] = recipients[i];
precompile.transferTokens(
TokenizationJSONHelpers.transferTokensJSON(
collectionId, singleRecipient, amounts[i], tokenIdsJson, ownershipJson
)
);
}
return true;
}
}
```
For one atomic call instead of a loop, use `executeMultiple` (see [API](tokenization-precompile/api.md#executemultiple)).
### Time-Limited Transfer
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract TimeLimitedTransfer {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function transferWithExpiration(
uint256 collectionId,
address to,
uint256 amount,
uint256 tokenId,
uint256 expirationTime
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(
block.timestamp, expirationTime
);
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId);
return precompile.transferTokens(
TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIdsJson, ownershipJson
)
);
}
}
```
### Struct Helpers with JSON
```solidity
import "./libraries/TokenizationHelpers.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract HelperExample {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function transferWithHelpers(
uint256 collectionId,
address to,
uint256 amount,
uint256 tokenId
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
// Build structs with the helper library, then serialize them
TokenizationTypes.UintRange memory idRange =
TokenizationHelpers.createSingleTokenIdRange(tokenId);
TokenizationTypes.UintRange memory timeRange =
TokenizationHelpers.createFullOwnershipTimeRange();
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(idRange.start, idRange.end);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(timeRange.start, timeRange.end);
return precompile.transferTokens(
TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIdsJson, ownershipJson
)
);
}
}
```
### Dynamic Store for Compliance
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract ComplianceToken {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
uint256 public kycRegistryId;
uint256 public collectionId;
function initialize(uint256 _collectionId) external {
collectionId = _collectionId;
// Create KYC registry (default: false = not KYC'd)
kycRegistryId = precompile.createDynamicStore(
TokenizationJSONHelpers.createDynamicStoreJSON(
false,
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json", // URI
"" // customData
)
);
}
function setKYC(address user, bool status) external {
precompile.setDynamicStoreValue(
TokenizationJSONHelpers.setDynamicStoreValueJSON(kycRegistryId, user, status)
);
kycMirror[user] = status;
}
// Mirror of the on-chain store, kept in sync by setKYC. getDynamicStoreValue
// returns protobuf bytes (see the API page), so a local mirror is the cheap
// way to read the flag inside a contract.
mapping(address => bool) private kycMirror;
function isKYCd(address user) public view returns (bool) {
return kycMirror[user];
}
function transfer(address to, uint256 amount, uint256 tokenId) external returns (bool) {
require(isKYCd(msg.sender), "Sender not KYC'd");
require(isKYCd(to), "Recipient not KYC'd");
address[] memory recipients = new address[](1);
recipients[0] = to;
return precompile.transferTokens(
TokenizationJSONHelpers.transferTokensJSON(
collectionId,
recipients,
amount,
TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId),
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER)
)
);
}
}
```
A cheaper alternative for gating is a [dynamic store challenge](../../token-standard/approval-criteria/dynamic-store-challenges.md) in the collection approvals, which the chain checks on every transfer without a contract call.
## Complete Examples
- [counter-dapp](https://github.com/BitBadges/bitbadgeschain/tree/master/counter-dapp): a Solidity contract (`Counter.sol`), a TypeScript deploy script, a Next.js frontend with MetaMask, and setup instructions.
- [contracts/examples](https://github.com/BitBadges/bitbadgeschain/tree/master/contracts/examples): ERC-3643 style example contracts:
1. `TwoFactorSecurityToken.sol`: security token with 2FA authentication
2. `RealEstateSecurityToken.sol`: tokenized real estate with KYC/AML
3. `CarbonCreditToken.sol`: carbon credits with vintage tracking
4. `PrivateEquityToken.sol`: private equity fund tokens with lock-ups
Each example uses dynamic stores for compliance registries, time-bound ownership for lock-ups and expirations, and approval systems. The [examples README](https://github.com/BitBadges/bitbadgeschain/tree/master/contracts/examples/README.md) explains each contract.
## Troubleshooting
### Cannot Connect to RPC
- Confirm the chain is running: `bb start --json-rpc.enable --json-rpc.address 0.0.0.0:8545`
- Check that JSON-RPC is enabled in `app.toml`
- Try both ports: `http://localhost:8545` (EVM) and `http://localhost:26657` (Tendermint)
- Confirm the chain is fully synced
### MetaMask Cannot Connect
- Use the correct chain ID: `90123` local, `50025` testnet, `50024` mainnet
- Use the EVM JSON-RPC port (`http://localhost:8545`), not 26657
- Confirm your local chain is running
- Check the MetaMask console for the exact error
### Transactions fail with "insufficient funds" or revert
- Fund the account with BADGE:
```bash
bb tx bank send $(bb keys show alice -a --keyring-backend test) bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue 1000000000ubadge --chain-id bitbadges-1 --keyring-backend test --gas auto --gas-adjustment 1.3 --gas-prices 10ubadge --yes
```
- Check gas prices (adjust in MetaMask if needed)
- Confirm approvals are set for token transfers
- Read the contract logs for the precompile error message
### Contract Deployment Fails
- Confirm the deployer has enough balance for deployment gas
- Confirm the EVM JSON-RPC is enabled
- Match your Solidity version to the chain's supported version
- Read the deploy script logs for the exact error
## Related
- [Developer Guide](developer-guide.md)
- [Tokenization Precompile API](tokenization-precompile/api.md)
- [EVM RPC Endpoints](rpc-endpoints.md)
- [Run a Node](../run-a-node.md)
## File: chain/evm/solidity-quickstart.md
---
description: "Get from zero to a working Solidity contract on BitBadges precompiles in five minutes: constants, addresses, JSON helpers, common patterns, errors."
---
# Solidity Quickstart
This page gets a Solidity developer to a working contract on the tokenization precompile in a few minutes. Read it before the full [API reference](tokenization-precompile/api.md).
## The Essentials
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract MyToken {
// Precompile address (same on all BitBadges networks)
ITokenizationPrecompile constant PRECOMPILE =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function transfer(uint256 collectionId, address to, uint256 amount) external {
address[] memory recipients = new address[](1);
recipients[0] = to;
// Use FOREVER, not type(uint256).max
string memory tokenIds = TokenizationJSONHelpers.uintRangeToJson(1, 1);
string memory times = TokenizationJSONHelpers.uintRangeToJson(
1,
TokenizationJSONHelpers.FOREVER
);
string memory json = TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIds, times
);
require(PRECOMPILE.transferTokens(json), "Transfer failed");
}
}
```
## Constants
BitBadges stores IDs and timestamps as `uint64`. `type(uint256).max` fails with a range overflow error.
```solidity
// Wrong: fails with "range overflow"
TokenizationJSONHelpers.uintRangeToJson(1, type(uint256).max);
// Correct: use the FOREVER constant
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER);
```
| Constant | Value | Use |
| --- | --- | --- |
| `FOREVER` | `18446744073709551615` | Ownership that never expires |
| `MAX_TIME` | `18446744073709551615` | Maximum timestamp |
| `MAX_ID` | `18446744073709551615` | Maximum token ID |
| `MIN_ID` | `1` | Minimum ID (ranges start at 1, not 0) |
| `FOREVER_STR` | `"18446744073709551615"` | The same value as a string, for hand-built JSON |
All four numeric constants equal `type(uint64).max` except `MIN_ID`. They are defined in both `TokenizationJSONHelpers` and `TokenizationHelpers`.
## Precompile Addresses
| Precompile | Address | Purpose |
| --- | --- | --- |
| Tokenization | `0x0000000000000000000000000000000000001001` | Collections, transfers, balances |
| GAMM | `0x0000000000000000000000000000000000001002` | Liquidity pools, swaps |
| Send manager | `0x0000000000000000000000000000000000001003` | Native coin sends, alias denoms |
## Why JSON
BitBadges precompiles take JSON strings, not Solidity structs.
```solidity
// This is how BitBadges precompiles work:
precompile.transferTokens(
'{"collectionId":"1","transfers":[{"from":"bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d","toAddresses":["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"],"balances":[{"amount":"1","tokenIds":[{"start":"1","end":"1"}],"ownershipTimes":[{"start":"1","end":"18446744073709551615"}]}]}]}'
);
// Not like ERC20:
token.transfer(to, amount); // BitBadges does not use this pattern
```
The JSON maps directly to the Cosmos SDK protobuf messages, so the same shape works from the EVM, the CLI, and the SDK, and new fields do not break the interface. The helper libraries build the JSON for you; hand-written JSON is rare.
## Import the Libraries
```solidity
// Interface for calling the precompile
import "./interfaces/ITokenizationPrecompile.sol";
// Helpers for constructing JSON (always use these)
import "./libraries/TokenizationJSONHelpers.sol";
// Helpers for constructing structs
import "./libraries/TokenizationHelpers.sol";
// Type definitions
import "./types/TokenizationTypes.sol";
// Error handling
import "./libraries/TokenizationErrors.sol";
```
The files live under [`contracts/`](https://github.com/BitBadges/bitbadgeschain/tree/master/contracts) in the chain repo.
## Common Patterns
### Transfer Tokens
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract TransferExample {
ITokenizationPrecompile constant PRECOMPILE =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function transfer(
uint256 collectionId,
address to,
uint256 amount,
uint256 tokenId
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
string memory tokenIds = TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId);
string memory times = TokenizationJSONHelpers.uintRangeToJson(
1, TokenizationJSONHelpers.FOREVER
);
string memory json = TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIds, times
);
return PRECOMPILE.transferTokens(json);
}
}
```
### Check a Balance
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract BalanceExample {
ITokenizationPrecompile constant PRECOMPILE =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function balanceOf(uint256 collectionId, address user) external view returns (uint256) {
string memory json = TokenizationJSONHelpers.getBalanceAmountJSON(
collectionId,
user,
1, // tokenId
block.timestamp * 1000 // ownershipTime in milliseconds
);
return PRECOMPILE.getBalanceAmount(json);
}
}
```
`ownershipTime` is a single point in time. Pass the same unit your collection uses for ownership times (the chain convention is milliseconds, so `block.timestamp * 1000` for wall-clock times).
### Create a Collection
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract CreateCollectionExample {
ITokenizationPrecompile constant PRECOMPILE =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function createCollection() external returns (uint256) {
string memory validTokenIds = TokenizationJSONHelpers.uintRangeToJson(
1, 1000 // Token IDs 1-1000
);
string memory metadata = TokenizationJSONHelpers.collectionMetadataToJson(
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json", ""
);
string memory balances = TokenizationJSONHelpers.simpleUserBalanceStoreToJson(
true, true, false // auto-approve settings
);
string[] memory standards = new string[](0);
string memory standardsJson = TokenizationJSONHelpers.stringArrayToJson(standards);
string memory json = TokenizationJSONHelpers.createCollectionJSON(
validTokenIds,
TokenizationJSONHelpers.addressToString(address(this)), // manager
metadata,
balances,
"{}", // permissions (empty = default)
standardsJson,
"", // customData
false // isArchived
);
return PRECOMPILE.createCollection(json);
}
}
```
### KYC Registry (Dynamic Store)
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract KycRegistryExample {
ITokenizationPrecompile constant PRECOMPILE =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
address public immutable owner = msg.sender;
uint256 public kycStoreId;
function initKYC() external {
require(msg.sender == owner, "Only owner");
require(kycStoreId == 0, "Already initialized");
string memory json = TokenizationJSONHelpers.createDynamicStoreJSON(
false, // default: not KYC'd
"", "" // metadata
);
kycStoreId = PRECOMPILE.createDynamicStore(json);
}
function setKYC(address user, bool status) external {
require(msg.sender == owner, "Only owner");
require(kycStoreId != 0, "Not initialized");
string memory json = TokenizationJSONHelpers.setDynamicStoreValueJSON(
kycStoreId, user, status
);
PRECOMPILE.setDynamicStoreValue(json);
}
}
```
## Error Handling
```solidity
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationErrors.sol";
contract MyContract {
ITokenizationPrecompile constant PRECOMPILE =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function safeTransfer(uint256 collectionId, string memory json) external {
// Validate before calling precompile
TokenizationErrors.requireValidCollectionId(collectionId);
try PRECOMPILE.transferTokens(json) returns (bool success) {
require(success, "Transfer failed");
} catch Error(string memory reason) {
revert(reason);
}
}
}
```
Common errors:
- `"range overflow"`: use `FOREVER` instead of `type(uint256).max`.
- `"address cannot be empty"`: check the JSON field names.
- `"failed to unmarshal"`: invalid JSON format.
See [Errors](tokenization-precompile/errors.md) for the full list.
## UintRange
Token IDs and ownership times are arrays of inclusive ranges.
```solidity
// Single value: token ID 5
TokenizationJSONHelpers.uintRangeToJson(5, 5);
// Produces: [{"start":"5","end":"5"}]
// Range: token IDs 1-100
TokenizationJSONHelpers.uintRangeToJson(1, 100);
// Produces: [{"start":"1","end":"100"}]
// Forever ownership (never expires)
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER);
// Produces: [{"start":"1","end":"18446744073709551615"}]
// Multiple ranges
uint256[] memory starts = new uint256[](2);
uint256[] memory ends = new uint256[](2);
starts[0] = 1; ends[0] = 100;
starts[1] = 200; ends[1] = 300;
TokenizationJSONHelpers.uintRangeArrayToJson(starts, ends);
// Produces: [{"start":"1","end":"100"},{"start":"200","end":"300"}]
```
Concept page: [UintRanges](../../token-standard/concepts/uint-ranges.md).
## Address Formats
```solidity
// In Solidity, use EVM addresses (0x hex)
address user = 0x0bc63cfe31d5218eb414b142c799e20964a54a1a;
// Convert to Cosmos bech32 format if needed
string memory bech32 = PRECOMPILE.convertEvmAddressToBech32(user);
// Returns: "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
// Convert back
address evm = PRECOMPILE.convertBech32ToEvmAddress("bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d");
// Returns: 0x0bc63cfe31d5218eb414b142c799e20964a54a1a
```
Both forms name the same account. Precompiles accept either form in JSON address fields and convert to bech32 internally.
## Utility Methods
```solidity
// Address conversion
string memory bech32 = PRECOMPILE.convertEvmAddressToBech32(evmAddress);
address evm = PRECOMPILE.convertBech32ToEvmAddress(bech32Address);
// Range checks
bool inRange = PRECOMPILE.rangeContains(10, 20, 15); // true
bool overlap = PRECOMPILE.rangesOverlap(10, 20, 15, 25); // true
// Search in range array
bool found = PRECOMPILE.searchInRanges('[{"start":"1","end":"100"}]', 50);
// Get balance for specific token/time
uint256 amount = PRECOMPILE.getBalanceForIdAndTime(balancesJson, tokenId, timestamp);
// Get reserved list ID for address
string memory listId = PRECOMPILE.getReservedListId(userAddress);
```
## Quick Reference
| Library | Purpose |
| --- | --- |
| `TokenizationJSONHelpers` | Build JSON strings for precompile calls |
| `TokenizationHelpers` | Build structs, constants, utilities |
| `TokenizationErrors` | Input validation, custom errors |
| `TokenizationTypes` | Type definitions |
| Method | Returns | Purpose |
| --- | --- | --- |
| `transferTokens(json)` | `bool` | Transfer tokens |
| `createCollection(json)` | `uint256` | Create collection, returns ID |
| `getBalanceAmount(json)` | `uint256` | Get balance amount |
| `createDynamicStore(json)` | `uint256` | Create store, returns ID |
## Related
- [Tokenization Precompile API](tokenization-precompile/api.md)
- [Errors](tokenization-precompile/errors.md)
- [Example contracts](https://github.com/BitBadges/bitbadgeschain/tree/master/contracts/examples)
- [GAMM Precompile](gamm-precompile/README.md)
## File: chain/evm/tokenization-precompile/README.md
---
description: "The tokenization precompile at 0x...1001: the token standard from Solidity through JSON messages, with helpers, patterns, and executeMultiple."
---
# Tokenization Precompile
The tokenization precompile exposes the whole `x/tokenization` module to Solidity contracts: transfers, collections, approvals, dynamic stores, address lists, votes, and every query. Address: `0x0000000000000000000000000000000000001001`.
## Example
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract MyTokenContract {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
// Transfer tokens using JSON helper
function transfer(
uint256 collectionId,
address to,
uint256 amount,
uint256 tokenId
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
// Build JSON using helpers
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId);
string memory ownershipTimesJson = TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER);
string memory transferJson = TokenizationJSONHelpers.transferTokensJSON(
collectionId,
recipients,
amount,
tokenIdsJson,
ownershipTimesJson
);
return TOKENIZATION.transferTokens(transferJson);
}
// Query balance using JSON helper
function balanceOf(
uint256 collectionId,
address user,
uint256 tokenId
) external view returns (uint256) {
string memory balanceJson = TokenizationJSONHelpers.getBalanceAmountJSON(
collectionId,
user,
tokenId,
block.timestamp * 1000
);
return TOKENIZATION.getBalanceAmount(balanceJson);
}
}
```
## How It Works
Every method takes one `string calldata msgJson`. The JSON is the protobuf JSON of the matching `x/tokenization` message or query request, the same shape the CLI and SDK use. See [Messages](../../../token-standard/messages/README.md) and [Queries](../../../token-standard/queries/README.md) for the field references.
```solidity
// Correct: JSON string
string memory json = TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIdsJson, ownershipTimesJson
);
bool success = TOKENIZATION.transferTokens(json);
// Wrong: struct parameters (old interface)
TOKENIZATION.transferTokens(collectionId, recipients, amount, tokenIds, ownershipTimes);
```
On the Go side the precompile:
1. Unmarshals the JSON into the Msg with the module codec (proto JSON, camelCase keys, integers as strings).
2. Overwrites `creator` with the caller's bech32 address. A contract calling the precompile is the creator; see [Developer Guide](../developer-guide.md#precompile-caller).
3. Converts `0x` addresses in address fields (`toAddresses`, `manager`, approval criteria, address lists) to bech32.
4. Runs `ValidateBasic`, then the module msg server.
Why JSON: it maps one-to-one to the protobuf messages, new fields never break the ABI, and one shape works across the EVM, CLI, and SDK. The helper libraries build the strings so you rarely write JSON by hand.
`TokenizationJSONHelpers` covers the common calls:
```solidity
import "./libraries/TokenizationJSONHelpers.sol";
// Simple operations
string memory json = TokenizationJSONHelpers.getCollectionJSON(collectionId);
bytes memory collection = TOKENIZATION.getCollection(json);
// Complex operations with ranges
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(1, 100);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(block.timestamp, expiration);
```
Token IDs and ownership times are `UintRange` arrays:
```solidity
// Single range
string memory singleRange = TokenizationJSONHelpers.uintRangeToJson(start, end);
// Multiple ranges
uint256[] memory starts = new uint256[](2);
uint256[] memory ends = new uint256[](2);
starts[0] = 1; ends[0] = 100;
starts[1] = 200; ends[1] = 300;
string memory multiRange = TokenizationJSONHelpers.uintRangeArrayToJson(starts, ends);
```
Range ends are `uint64`. Use `TokenizationJSONHelpers.FOREVER`, never `type(uint256).max`.
## Methods
50 functions in the ABI: 25 transactions, 1 batch executor, 17 queries, 7 pure utilities. Full signatures and JSON on the [API page](api.md).
| Group | Methods |
| --- | --- |
| Transfers and approvals | `transferTokens`, `setIncomingApproval`, `setOutgoingApproval`, `deleteIncomingApproval`, `deleteOutgoingApproval`, `updateUserApprovals`, `purgeApprovals` |
| Collections | `createCollection`, `updateCollection`, `universalUpdateCollection`, `deleteCollection`, `setValidTokenIds`, `setManager`, `setCollectionMetadata`, `setTokenMetadata`, `setCustomData`, `setStandards`, `setCollectionApprovals`, `setIsArchived` |
| Dynamic stores | `createDynamicStore`, `updateDynamicStore`, `deleteDynamicStore`, `setDynamicStoreValue` |
| Lists and votes | `createAddressLists`, `castVote` |
| Batch | `executeMultiple` |
| Queries | `getCollection`, `getCollectionStats`, `getBalance`, `getBalanceAmount`, `getTotalSupply`, `getAddressList`, `getApprovalTracker`, `getChallengeTracker`, `getETHSignatureTracker`, `getDynamicStore`, `getDynamicStoreValue`, `getWrappableBalances`, `isAddressReservedProtocol`, `getAllReservedProtocolAddresses`, `getVote`, `getVotes`, `params` |
| Utilities (pure) | `convertEvmAddressToBech32`, `convertBech32ToEvmAddress`, `rangeContains`, `rangesOverlap`, `searchInRanges`, `getBalanceForIdAndTime`, `getReservedListId` |
Transaction methods return `bool success`, or a `uint256` for methods that create or update something (`createCollection`, `updateCollection`, `createDynamicStore`, the `set*` collection methods, `universalUpdateCollection`, `purgeApprovals`).
Query methods return `uint256` for `getBalanceAmount`, `getTotalSupply`, `getChallengeTracker`, `getETHSignatureTracker`, and `getWrappableBalances`; `bool` for `isAddressReservedProtocol`; `address[]` for `getAllReservedProtocolAddresses`; and protobuf-encoded `bytes` for everything else. See [Return values](#return-values).
## Patterns
### Simple Token Transfer
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract SimpleTransfer {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function transferToken(
uint256 collectionId,
address to,
uint256 amount,
uint256 tokenId
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
// Full ownership (no expiration)
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER);
string memory transferJson = TokenizationJSONHelpers.transferTokensJSON(
collectionId,
recipients,
amount,
tokenIdsJson,
ownershipJson
);
return TOKENIZATION.transferTokens(transferJson);
}
}
```
### Time-Bound Transfer
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract TimeBoundTransfer {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function transferWithExpiration(
uint256 collectionId,
address to,
uint256 amount,
uint256 tokenId,
uint256 expirationTime
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
// Time-bound ownership
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(tokenId, tokenId);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(
block.timestamp,
expirationTime
);
string memory transferJson = TokenizationJSONHelpers.transferTokensJSON(
collectionId,
recipients,
amount,
tokenIdsJson,
ownershipJson
);
return TOKENIZATION.transferTokens(transferJson);
}
}
```
Concept: [Balances](../../../token-standard/concepts/balances.md) (ownership times).
### KYC Registry with a Dynamic Store
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract KycRegistry {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
uint256 public kycRegistryId;
function initializeKYCRegistry() external {
string memory createJson = TokenizationJSONHelpers.createDynamicStoreJSON(
false, // defaultValue: not KYC'd by default
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"{\"type\":\"kyc\"}"
);
kycRegistryId = TOKENIZATION.createDynamicStore(createJson);
}
function setKYCStatus(address user, bool isKYCd) external {
string memory setValueJson = TokenizationJSONHelpers.setDynamicStoreValueJSON(
kycRegistryId,
user,
isKYCd
);
TOKENIZATION.setDynamicStoreValue(setValueJson);
}
function kycValueBytes(address user) external view returns (bytes memory) {
string memory getValueJson = TokenizationJSONHelpers.getDynamicStoreValueJSON(
kycRegistryId,
user
);
// Protobuf-encoded QueryGetDynamicStoreValueResponse. Decode off-chain,
// or let the chain enforce the store with a dynamic store challenge.
return TOKENIZATION.getDynamicStoreValue(getValueJson);
}
}
```
To enforce the registry on transfers without a contract in the loop, add a [dynamic store challenge](../../../token-standard/approval-criteria/dynamic-store-challenges.md) to the collection approvals.
### Create a Collection
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract CollectionFactory {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function createMyCollection(
string memory name,
string memory symbol
) external returns (uint256) {
// Build JSON components
string memory validTokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(1, 1000);
string memory defaultBalancesJson = TokenizationJSONHelpers.simpleUserBalanceStoreToJson(
true, // autoApproveSelfInitiatedOutgoingTransfers
true, // autoApproveSelfInitiatedIncomingTransfers
false // autoApproveAllIncomingTransfers
);
string memory metadataJson = TokenizationJSONHelpers.collectionMetadataToJson(
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
string(abi.encodePacked("{\"name\":\"", name, "\",\"symbol\":\"", symbol, "\"}"))
);
string[] memory standards = new string[](1);
standards[0] = "ERC-3643";
string memory standardsJson = TokenizationJSONHelpers.stringArrayToJson(standards);
// Build complete JSON
string memory createJson = TokenizationJSONHelpers.createCollectionJSON(
validTokenIdsJson,
TokenizationJSONHelpers.addressToString(address(this)), // manager
metadataJson,
defaultBalancesJson,
"{}", // collectionPermissions (empty)
standardsJson,
"", // customData
false // isArchived
);
return TOKENIZATION.createCollection(createJson);
}
}
```
Invariants and cosmos coin wrapper paths can be set at creation through `createCollectionWithInvariantsJSON` or by writing the `invariants` and `cosmosCoinWrapperPathsToAdd` fields yourself. See [MsgCreateCollection](../../../token-standard/messages/msg-create-collection.md).
### Create a Collection and Transfer in One Transaction
`executeMultiple` runs several messages atomically in one call.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract CreateAndTransfer {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function createAndTransfer(
string memory name,
address recipient,
uint256 amount
) external returns (uint256 collectionId) {
// Prepare messages array
ITokenizationPrecompile.MessageInput[] memory messages = new ITokenizationPrecompile.MessageInput[](2);
// Message 1: Create Collection
string memory validTokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(1, 1000);
string memory defaultBalancesJson = TokenizationJSONHelpers.simpleUserBalanceStoreToJson(true, true, false);
string memory metadataJson = TokenizationJSONHelpers.collectionMetadataToJson(
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
string(abi.encodePacked("{\"name\":\"", name, "\"}"))
);
string[] memory standards = new string[](0);
string memory standardsJson = TokenizationJSONHelpers.stringArrayToJson(standards);
string memory createJson = TokenizationJSONHelpers.createCollectionJSON(
validTokenIdsJson,
TokenizationJSONHelpers.addressToString(address(this)),
metadataJson,
defaultBalancesJson,
"{}",
standardsJson,
"",
false
);
messages[0] = ITokenizationPrecompile.MessageInput({
messageType: "createCollection",
msgJson: createJson
});
// Message 2: Transfer Tokens (collectionId 0 = the collection created in the previous message)
address[] memory recipients = new address[](1);
recipients[0] = recipient;
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(1, 1);
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER);
string memory transferJson = TokenizationJSONHelpers.transferTokensJSON(
0, // collectionId = 0 means "use previous collection" (auto-prev)
recipients,
amount,
tokenIdsJson,
ownershipJson
);
messages[1] = ITokenizationPrecompile.MessageInput({
messageType: "transferTokens",
msgJson: transferJson
});
// Execute both messages atomically
(bool success, bytes[] memory results) = TOKENIZATION.executeMultiple(messages);
require(success, "Multi-message execution failed");
// Decode collectionId from first result
collectionId = abi.decode(results[0], (uint256));
// Verify transfer succeeded (second result is bool)
bool transferSuccess = abi.decode(results[1], (bool));
require(transferSuccess, "Transfer failed");
return collectionId;
}
}
```
Rules for `executeMultiple`:
- Atomic: all messages succeed or the whole call reverts, with the failing index and type in the error.
- Sequential, in array order. At most 50 messages per batch (`MaxMessagesPerBatch`).
- `messageType` is the method name string (`"createCollection"`, `"transferTokens"`, and so on). Any transaction method is accepted.
- Results come back as `bytes[]`. Decode each with `abi.decode(results[i], (bool))` or `(uint256)` according to the method's return type.
- `collectionId: "0"` in a later message refers to the collection created earlier in the same batch (the module's auto-prev rule).
## Helper Library
`TokenizationJSONHelpers` has a builder for every method:
| Group | Functions |
| --- | --- |
| Transfers | `transferTokensJSON`, `uintRangeToJson`, `uintRangeArrayToJson`, `balanceToJson` |
| Collections | `createCollectionJSON`, `createCollectionWithInvariantsJSON`, `collectionMetadataToJson`, `simpleUserBalanceStoreToJson`, `stringArrayToJson`, `deleteCollectionJSON`, `setValidTokenIdsJSON`, `setManagerJSON`, `setCollectionMetadataJSON`, `setTokenMetadataJSON`, `setCustomDataJSON`, `setStandardsJSON`, `setCollectionApprovalsJSON`, `setIsArchivedJSON` |
| Approvals | `setIncomingApprovalJSON`, `setOutgoingApprovalJSON`, `deleteIncomingApprovalJSON`, `deleteOutgoingApprovalJSON`, `updateUserApprovalsJSON`, `purgeApprovalsJSON` |
| Dynamic stores | `createDynamicStoreJSON`, `updateDynamicStoreJSON`, `deleteDynamicStoreJSON`, `setDynamicStoreValueJSON`, `getDynamicStoreJSON`, `getDynamicStoreValueJSON` |
| Lists and votes | `createAddressListsJSON`, `getAddressListJSON`, `castVoteJSON`, `getVoteJSON`, `getVotesJSON` |
| Queries | `getCollectionJSON`, `getCollectionStatsJSON`, `getBalanceJSON`, `getBalanceAmountJSON`, `getTotalSupplyJSON`, `getApprovalTrackerJSON`, `getChallengeTrackerJSON`, `getWrappableBalancesJSON`, `isAddressReservedProtocolJSON`, `getAllReservedProtocolAddressesJSON`, `paramsJSON` |
| Utilities | `uintToString`, `addressToString`, constants `FOREVER`, `MAX_TIME`, `MAX_ID`, `MIN_ID`, `FOREVER_STR` |
The precompile's own pure utilities:
```solidity
// Address conversion
string memory bech32 = TOKENIZATION.convertEvmAddressToBech32(evmAddress);
address evm = TOKENIZATION.convertBech32ToEvmAddress("bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d");
// Range utilities
bool inRange = TOKENIZATION.rangeContains(10, 20, 15); // true
bool overlap = TOKENIZATION.rangesOverlap(10, 20, 15, 25); // true
bool found = TOKENIZATION.searchInRanges('[{"start":"1","end":"100"}]', 50); // true
// Balance utilities
uint256 amount = TOKENIZATION.getBalanceForIdAndTime(balancesJson, tokenId, timestamp);
// List ID utilities
string memory listId = TOKENIZATION.getReservedListId(user); // returns the bech32 form, e.g. bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d
```
## Return Values
`uint256` directly:
```solidity
uint256 balance = TOKENIZATION.getBalanceAmount(balanceJson);
uint256 supply = TOKENIZATION.getTotalSupply(supplyJson);
uint256 collectionId = TOKENIZATION.createCollection(createJson);
```
Protobuf `bytes` from `getCollection`, `getBalance`, `getCollectionStats`, `getAddressList`, `getApprovalTracker`, `getDynamicStore`, `getDynamicStoreValue`, `getVote`, `getVotes`, and `params`. The bytes are the marshalled gRPC response (for example `QueryGetCollectionResponse`). Options:
1. Prefer a direct query when one exists (`getBalanceAmount`, `getTotalSupply`, `getChallengeTracker`).
2. Extract single fields on-chain with `TokenizationDecoders` (`parseHolderCountFromStats`, `readVarint`). `contracts/test/MaxUniqueHoldersChecker.sol` shows this for an invariant.
3. Decode off-chain with the TypeScript SDK and pass the values back in as parameters, or emit the bytes in an event for an indexer.
Full protobuf decoding in Solidity is not implemented; the `decodeCollection`, `decodeBalance`, `decodeAddressList`, and `decodeDynamicStore` stubs revert.
## Security
1. `creator` is set from `msg.sender` on the Go side. A JSON `creator` value is ignored.
2. Invalid JSON reverts with a structured error and field path.
3. Array sizes are capped (100 recipients, 100 ranges, and so on). See [Security](security.md).
4. Cache JSON strings you reuse; construction costs gas.
## Best Practices
Use the helpers:
```solidity
// Good: type-safe and readable
string memory json = TokenizationJSONHelpers.transferTokensJSON(
collectionId, recipients, amount, tokenIdsJson, ownershipTimesJson
);
// Bad: error-prone manual construction
string memory json = string(abi.encodePacked(
'{"collectionId":"', TokenizationJSONHelpers.uintToString(collectionId),
'","transfers":[{"toAddresses":["', TokenizationJSONHelpers.addressToString(recipients[0]),
'"],"balances":[{"amount":"', TokenizationJSONHelpers.uintToString(amount),
'","tokenIds":', tokenIdsJson, ',"ownershipTimes":', ownershipTimesJson, '}]}]}'
));
```
Cache JSON you reuse:
```solidity
// Good: cache for reuse
string memory tokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(1, 1000);
// Use tokenIdsJson multiple times
// Bad: reconstruct every time
// Rebuilding JSON on every call wastes gas
```
Validate before building:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract ValidatedTransfer {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function transfer(uint256 collectionId, address to, uint256 amount) external {
require(collectionId > 0, "Invalid collection");
require(to != address(0), "Invalid recipient");
require(amount > 0, "Invalid amount");
// Now build JSON
address[] memory recipients = new address[](1);
recipients[0] = to;
string memory json = TokenizationJSONHelpers.transferTokensJSON(
collectionId,
recipients,
amount,
TokenizationJSONHelpers.uintRangeToJson(1, 1),
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER)
);
TOKENIZATION.transferTokens(json);
}
}
```
Check results:
```solidity
bool success = TOKENIZATION.transferTokens(transferJson);
if (!success) {
// Handle failure - check events or revert with custom error
revert TransferFailed();
}
```
Validate the batch in `executeMultiple`:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract ValidatedBatch {
ITokenizationPrecompile constant TOKENIZATION =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
// Good: validate inputs before building messages
function createAndTransfer(
string memory name,
address recipient,
uint256 amount
) external returns (uint256 collectionId) {
require(bytes(name).length > 0, "Name required");
require(recipient != address(0), "Invalid recipient");
require(amount > 0, "Amount must be positive");
ITokenizationPrecompile.MessageInput[] memory messages = new ITokenizationPrecompile.MessageInput[](2);
// Build messages
string[] memory standards = new string[](0);
string memory createCollectionJson = TokenizationJSONHelpers.createCollectionJSON(
TokenizationJSONHelpers.uintRangeToJson(1, 1000),
TokenizationJSONHelpers.addressToString(address(this)),
TokenizationJSONHelpers.collectionMetadataToJson(
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
string(abi.encodePacked("{\"name\":\"", name, "\"}"))
),
TokenizationJSONHelpers.simpleUserBalanceStoreToJson(true, true, false),
"{}",
TokenizationJSONHelpers.stringArrayToJson(standards),
"",
false
);
messages[0] = ITokenizationPrecompile.MessageInput({
messageType: "createCollection",
msgJson: createCollectionJson
});
address[] memory recipients = new address[](1);
recipients[0] = recipient;
string memory transferTokensJson = TokenizationJSONHelpers.transferTokensJSON(
0,
recipients,
amount,
TokenizationJSONHelpers.uintRangeToJson(1, 1),
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER)
);
messages[1] = ITokenizationPrecompile.MessageInput({
messageType: "transferTokens",
msgJson: transferTokensJson
});
// Execute atomically
(bool success, bytes[] memory results) = TOKENIZATION.executeMultiple(messages);
require(success, "Multi-message execution failed");
// Decode and validate results
require(results.length == 2, "Unexpected result count");
collectionId = abi.decode(results[0], (uint256));
require(collectionId > 0, "Invalid collection ID");
bool transferSuccess = abi.decode(results[1], (bool));
require(transferSuccess, "Transfer failed");
return collectionId;
}
}
```
## Examples
- [CarbonCreditToken](https://github.com/BitBadges/bitbadgeschain/blob/master/contracts/examples/CarbonCreditToken.sol): carbon credit tracking with vintages
- [TwoFactorSecurityToken](https://github.com/BitBadges/bitbadgeschain/blob/master/contracts/examples/TwoFactorSecurityToken.sol): 2FA-protected security tokens
- [RealEstateSecurityToken](https://github.com/BitBadges/bitbadgeschain/blob/master/contracts/examples/RealEstateSecurityToken.sol): ERC-3643 style real estate tokens
- [PrivateEquityToken](https://github.com/BitBadges/bitbadgeschain/blob/master/contracts/examples/PrivateEquityToken.sol): private equity fund tokens
- [ERC3643Template](https://github.com/BitBadges/bitbadgeschain/blob/master/contracts/templates/ERC3643Template.sol): the ERC-3643 template
## Related
- [API reference](api.md)
- [Errors](errors.md)
- [Gas](gas.md)
- [Security](security.md)
## File: chain/evm/tokenization-precompile/api.md
---
description: "Every function on the tokenization precompile from abi.json: 25 transactions, executeMultiple, 17 queries, 7 utilities, with signatures and JSON."
---
# Tokenization Precompile API
Reference for all 50 functions on the tokenization precompile at `0x0000000000000000000000000000000000001001`, regenerated from `x/tokenization/precompile/abi.json`. Each method takes one `string calldata msgJson`, except `executeMultiple` and the pure utilities.
## Interface
```solidity
interface ITokenizationPrecompile {
struct MessageInput {
string messageType; // e.g., "createCollection", "transferTokens"
string msgJson; // JSON matching the protobuf format
}
// Transaction methods
function transferTokens(string calldata msgJson) external returns (bool success);
function setIncomingApproval(string calldata msgJson) external returns (bool success);
function setOutgoingApproval(string calldata msgJson) external returns (bool success);
function deleteIncomingApproval(string calldata msgJson) external returns (bool success);
function deleteOutgoingApproval(string calldata msgJson) external returns (bool success);
function updateUserApprovals(string calldata msgJson) external returns (bool success);
function purgeApprovals(string calldata msgJson) external returns (uint256 numPurged);
function createCollection(string calldata msgJson) external returns (uint256 collectionId);
function updateCollection(string calldata msgJson) external returns (uint256 collectionId);
function universalUpdateCollection(string calldata msgJson) external returns (uint256 collectionId);
function deleteCollection(string calldata msgJson) external returns (bool success);
function setValidTokenIds(string calldata msgJson) external returns (uint256 collectionId);
function setManager(string calldata msgJson) external returns (uint256 collectionId);
function setCollectionMetadata(string calldata msgJson) external returns (uint256 collectionId);
function setTokenMetadata(string calldata msgJson) external returns (uint256 collectionId);
function setCustomData(string calldata msgJson) external returns (uint256 collectionId);
function setStandards(string calldata msgJson) external returns (uint256 collectionId);
function setCollectionApprovals(string calldata msgJson) external returns (uint256 collectionId);
function setIsArchived(string calldata msgJson) external returns (uint256 collectionId);
function createDynamicStore(string calldata msgJson) external returns (uint256 storeId);
function updateDynamicStore(string calldata msgJson) external returns (bool success);
function deleteDynamicStore(string calldata msgJson) external returns (bool success);
function setDynamicStoreValue(string calldata msgJson) external returns (bool success);
function createAddressLists(string calldata msgJson) external returns (bool success);
function castVote(string calldata msgJson) external returns (bool success);
function executeMultiple(MessageInput[] calldata messages) external returns (bool success, bytes[] memory results);
// Query methods
function getCollection(string calldata msgJson) external view returns (bytes memory collection);
function getCollectionStats(string calldata msgJson) external view returns (bytes memory stats);
function getBalance(string calldata msgJson) external view returns (bytes memory balance);
function getBalanceAmount(string calldata msgJson) external view returns (uint256 amount);
function getTotalSupply(string calldata msgJson) external view returns (uint256 amount);
function getAddressList(string calldata msgJson) external view returns (bytes memory list);
function getApprovalTracker(string calldata msgJson) external view returns (bytes memory tracker);
function getChallengeTracker(string calldata msgJson) external view returns (uint256 numUsed);
function getETHSignatureTracker(string calldata msgJson) external view returns (uint256 numUsed);
function getDynamicStore(string calldata msgJson) external view returns (bytes memory store);
function getDynamicStoreValue(string calldata msgJson) external view returns (bytes memory value);
function getWrappableBalances(string calldata msgJson) external view returns (uint256 amount);
function isAddressReservedProtocol(string calldata msgJson) external view returns (bool isReserved);
function getAllReservedProtocolAddresses(string calldata msgJson) external view returns (address[] memory addresses);
function getVote(string calldata msgJson) external view returns (bytes memory vote);
function getVotes(string calldata msgJson) external view returns (bytes memory votes);
function params(string calldata msgJson) external view returns (bytes memory params);
// Utility methods (pure)
function convertEvmAddressToBech32(address evmAddress) external pure returns (string memory bech32Address);
function convertBech32ToEvmAddress(string calldata bech32Address) external pure returns (address evmAddress);
function rangeContains(uint256 start, uint256 end, uint256 value) external pure returns (bool contains);
function rangesOverlap(uint256 start1, uint256 end1, uint256 start2, uint256 end2) external pure returns (bool overlap);
function searchInRanges(string calldata rangesJson, uint256 value) external pure returns (bool found);
function getBalanceForIdAndTime(string calldata balancesJson, uint256 tokenId, uint256 time) external pure returns (uint256 amount);
function getReservedListId(address addr) external pure returns (string memory listId);
}
```
The full interface with events and doc comments is [`contracts/interfaces/ITokenizationPrecompile.sol`](https://github.com/BitBadges/bitbadgeschain/blob/master/contracts/interfaces/ITokenizationPrecompile.sol).
## JSON Rules
- The JSON is the protobuf JSON of the `x/tokenization` message or query request, decoded with the module codec. Field names are camelCase, exactly as on the [message pages](../../../token-standard/messages/README.md).
- Numbers are strings (`"123"`, never `123`). Booleans are raw (`true`). Arrays and objects are standard JSON.
- Addresses may be `0x` hex or `bb1` bech32. The precompile converts hex to bech32 in `toAddresses`, `manager`, approval criteria, address lists, and query address fields.
- `creator` is set from `msg.sender`. A value in the JSON is ignored.
- Unknown fields, wrong types, and missing required fields revert with code 1. See [Errors](errors.md).
- Invariants and cosmos coin wrapper paths are settable at creation (`createCollectionWithInvariantsJSON`); the chain README notes some deeply nested items may be skipped silently on conversion, so verify with `getCollection` after creation.
{% hint style="info" %}
Ask your agent:
```text
Build the MsgTransferTokens JSON that sends 1 of token ID 1 in collection 1 from alice to bob, with no prioritized approvals, so I can pass it as msgJson to transferTokens.
```
The `bb build transfer` command and the MCP builder tools emit the same camelCase JSON the precompile accepts; strip the outer `typeUrl`/`value` envelope and the `creator` field.
{% endhint %}
## Transaction Methods
### transferTokens
Transfer tokens from the caller (or from an address that has approved the caller) to one or more recipients. Message: [MsgTransferTokens](../../../token-standard/messages/msg-transfer-tokens.md).
```solidity
function transferTokens(string calldata msgJson) external returns (bool success)
```
```json
{
"collectionId": "123",
"transfers": [
{
"from": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"toAddresses": ["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"],
"balances": [
{
"amount": "1000",
"tokenIds": [{"start": "1", "end": "1"}],
"ownershipTimes": [{"start": "1", "end": "18446744073709551615"}]
}
],
"prioritizedApprovals": [],
"onlyCheckPrioritizedCollectionApprovals": false,
"onlyCheckPrioritizedIncomingApprovals": false,
"onlyCheckPrioritizedOutgoingApprovals": false
}
]
}
```
`from` defaults to the caller. Set it to another address only when that address has granted the contract an outgoing approval. Other `Transfer` fields (`precalculateBalancesFromApproval`, `merkleProofs`, `ethSignatureProofs`, `memo`) are accepted as in the message.
Helper (the common single-balance case):
```solidity
address[] memory recipients = new address[](1);
recipients[0] = 0x092bb4851ae26850588243e7bef22a56287f4739;
string memory json = TokenizationJSONHelpers.transferTokensJSON(
1, // collectionId
recipients, // address[] recipients
1000, // uint256 amount
TokenizationJSONHelpers.uintRangeToJson(1, 1), // tokenIdsJson
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER) // ownershipTimesJson
);
```
Explicit `from` and balances with `balanceToJson`:
```solidity
string memory balancesJson = TokenizationJSONHelpers.balanceToJson(
amount,
TokenizationJSONHelpers.uintRangeToJson(1, 1), // tokenIds
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER) // ownershipTimes
);
string memory transferJson = string(abi.encodePacked(
'{"collectionId":"', TokenizationJSONHelpers.uintToString(collectionId),
'","transfers":[{"from":"bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d","toAddresses":["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"],',
'"balances":[', balancesJson, ']}]}'
));
bool success = TOKENIZATION.transferTokens(transferJson);
```
Emits `precompile_transfer_tokens` with `collection_id`, `from`, `to_addresses`, `amount`, `token_ids`, `ownership_times`.
### setIncomingApproval
Set or replace one incoming approval on the caller's balance store. Message: [MsgSetIncomingApproval](../../../token-standard/messages/msg-set-incoming-approval.md).
```solidity
function setIncomingApproval(string calldata msgJson) external returns (bool success)
```
```json
{
"collectionId": "123",
"approval": {
"fromListId": "All",
"initiatedByListId": "All",
"transferTimes": [{"start": "1", "end": "18446744073709551615"}],
"tokenIds": [{"start": "1", "end": "100"}],
"ownershipTimes": [{"start": "1", "end": "18446744073709551615"}],
"approvalId": "accept-all",
"approvalCriteria": {}
}
}
```
Helper: `setIncomingApprovalJSON(collectionId, approvalJson)`; build the approval with `userIncomingApprovalToJson`. Addresses in `approvalCriteria` are converted from hex. Emits `precompile_set_incoming_approval`.
### setOutgoingApproval
Set or replace one outgoing approval on the caller's balance store. Message: [MsgSetOutgoingApproval](../../../token-standard/messages/msg-set-outgoing-approval.md).
```solidity
function setOutgoingApproval(string calldata msgJson) external returns (bool success)
```
```json
{
"collectionId": "123",
"approval": {
"toListId": "All",
"initiatedByListId": "bb1t77myv2k0zh7evm87qedj0my9ajpsz4rp7vqsa",
"transferTimes": [{"start": "1", "end": "18446744073709551615"}],
"tokenIds": [{"start": "1", "end": "100"}],
"ownershipTimes": [{"start": "1", "end": "18446744073709551615"}],
"approvalId": "allow-contract",
"approvalCriteria": {}
}
}
```
Helper: `setOutgoingApprovalJSON(collectionId, approvalJson)` with `userOutgoingApprovalToJson`. Emits `precompile_set_outgoing_approval`.
### deleteIncomingApproval and deleteOutgoingApproval
Delete an approval by ID. Messages: [MsgDeleteIncomingApproval](../../../token-standard/messages/msg-delete-incoming-approval.md), [MsgDeleteOutgoingApproval](../../../token-standard/messages/msg-delete-outgoing-approval.md).
```solidity
function deleteIncomingApproval(string calldata msgJson) external returns (bool success)
function deleteOutgoingApproval(string calldata msgJson) external returns (bool success)
```
```json
{
"collectionId": "123",
"approvalId": "approval-123"
}
```
```solidity
string memory json = TokenizationJSONHelpers.deleteIncomingApprovalJSON(
collectionId,
approvalId
);
string memory json = TokenizationJSONHelpers.deleteOutgoingApprovalJSON(
collectionId,
approvalId
);
```
### updateUserApprovals
Replace the caller's outgoing approvals, incoming approvals, auto-approve flags, and user permissions in one message. Each group has an `update*` flag. Message: [MsgUpdateUserApprovals](../../../token-standard/messages/msg-update-user-approvals.md).
```solidity
function updateUserApprovals(string calldata msgJson) external returns (bool success)
```
```json
{
"collectionId": "123",
"updateOutgoingApprovals": true,
"outgoingApprovals": [],
"updateIncomingApprovals": false,
"incomingApprovals": [],
"updateAutoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedOutgoingTransfers": true,
"updateAutoApproveSelfInitiatedIncomingTransfers": false,
"autoApproveSelfInitiatedIncomingTransfers": true,
"updateAutoApproveAllIncomingTransfers": false,
"autoApproveAllIncomingTransfers": false,
"updateUserPermissions": false,
"userPermissions": {}
}
```
Helper: `updateUserApprovalsJSON(collectionId, updateOutgoingApprovals, outgoingApprovalsJson, updateIncomingApprovals, incomingApprovalsJson, updateAutoApproveSelfInitiatedOutgoingTransfers, autoApproveSelfInitiatedOutgoingTransfers, updateAutoApproveSelfInitiatedIncomingTransfers, autoApproveSelfInitiatedIncomingTransfers, updateAutoApproveAllIncomingTransfers, autoApproveAllIncomingTransfers, updateUserPermissions, userPermissionsJson)`.
### purgeApprovals
Remove expired approvals, or counterparty approvals that name the caller, from a balance store. Returns the number purged. Message: [MsgPurgeApprovals](../../../token-standard/messages/msg-purge-approvals.md).
```solidity
function purgeApprovals(string calldata msgJson) external returns (uint256 numPurged)
```
```json
{
"collectionId": "123",
"purgeExpired": true,
"approverAddress": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"purgeCounterpartyApprovals": false,
"approvalsToPurge": []
}
```
Helper: `purgeApprovalsJSON(collectionId, purgeExpired, approverAddress, purgeCounterpartyApprovals, approvalsToPurgeJson)`.
### createCollection
Create a collection. The caller becomes the creator; `manager` may be any address. Returns the new collection ID. Message: [MsgCreateCollection](../../../token-standard/messages/msg-create-collection.md).
```solidity
function createCollection(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"validTokenIds": [{"start": "1", "end": "1000"}],
"manager": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionMetadata": {
"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"customData": "{\"name\":\"My Token\"}"
},
"defaultBalances": {
"balances": [],
"outgoingApprovals": [],
"incomingApprovals": [],
"autoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedIncomingTransfers": true,
"autoApproveAllIncomingTransfers": false,
"userPermissions": {
"canUpdateOutgoingApprovals": [],
"canUpdateIncomingApprovals": [],
"canUpdateAutoApproveSelfInitiatedOutgoingTransfers": [],
"canUpdateAutoApproveSelfInitiatedIncomingTransfers": [],
"canUpdateAutoApproveAllIncomingTransfers": []
}
},
"standards": ["ERC-3643"],
"isArchived": false
}
```
Other accepted fields: `collectionPermissions`, `tokenMetadata`, `customData`, `collectionApprovals`, `mintEscrowCoinsToTransfer`, `cosmosCoinWrapperPathsToAdd`, `invariants`, `aliasPathsToAdd`.
```solidity
string memory json = TokenizationJSONHelpers.createCollectionJSON(
validTokenIdsJson, // Use uintRangeToJson or uintRangeArrayToJson
manager, // address string (0x or bb1)
collectionMetadataJson, // Use collectionMetadataToJson
defaultBalancesJson, // Use simpleUserBalanceStoreToJson or custom JSON
collectionPermissionsJson, // "{}" for empty
standardsJson, // Use stringArrayToJson
customData, // Optional string
isArchived // bool
);
```
```solidity
string memory validTokenIdsJson = TokenizationJSONHelpers.uintRangeToJson(1, 1000);
string memory metadataJson = TokenizationJSONHelpers.collectionMetadataToJson(
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"{\"name\":\"My Token\"}"
);
string memory defaultBalancesJson = TokenizationJSONHelpers.simpleUserBalanceStoreToJson(
true, true, false
);
string[] memory standards = new string[](1);
standards[0] = "ERC-3643";
string memory standardsJson = TokenizationJSONHelpers.stringArrayToJson(standards);
string memory createJson = TokenizationJSONHelpers.createCollectionJSON(
validTokenIdsJson,
TokenizationJSONHelpers.addressToString(address(this)),
metadataJson,
defaultBalancesJson,
"{}",
standardsJson,
"",
false
);
uint256 collectionId = TOKENIZATION.createCollection(createJson);
```
Emits `CollectionCreated`.
### updateCollection
Update the fields of a collection that the caller (the manager) is permitted to change. Each field has an `update*` flag. Returns the collection ID. Message: [MsgUpdateCollection](../../../token-standard/messages/msg-update-collection.md).
```solidity
function updateCollection(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"updateCollectionMetadata": true,
"collectionMetadata": {"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json", "customData": ""},
"updateManager": false,
"updateValidTokenIds": false,
"updateCollectionPermissions": false,
"updateTokenMetadata": false,
"updateCustomData": false,
"updateCollectionApprovals": false,
"updateStandards": false,
"updateIsArchived": false
}
```
Emits `CollectionUpdated`.
### universalUpdateCollection
The superset message: create (`collectionId` `"0"`) or update a collection, with `defaultBalances` and every `update*` flag from `updateCollection`. Returns the collection ID. Message: [MsgUniversalUpdateCollection](../../../token-standard/messages/msg-universal-update-collection.md).
```solidity
function universalUpdateCollection(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "0",
"defaultBalances": {
"balances": [],
"outgoingApprovals": [],
"incomingApprovals": [],
"autoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedIncomingTransfers": true,
"autoApproveAllIncomingTransfers": false,
"userPermissions": {
"canUpdateOutgoingApprovals": [],
"canUpdateIncomingApprovals": [],
"canUpdateAutoApproveSelfInitiatedOutgoingTransfers": [],
"canUpdateAutoApproveSelfInitiatedIncomingTransfers": [],
"canUpdateAutoApproveAllIncomingTransfers": []
}
},
"updateValidTokenIds": true,
"validTokenIds": [{"start": "1", "end": "100"}],
"updateManager": true,
"manager": "0x0bc63cfe31d5218eb414b142c799e20964a54a1a",
"updateCollectionMetadata": true,
"collectionMetadata": {"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json", "customData": ""},
"updateCollectionPermissions": false,
"updateTokenMetadata": false,
"updateCustomData": false,
"updateCollectionApprovals": false,
"updateStandards": false,
"updateIsArchived": false
}
```
### deleteCollection
Delete a collection. Only the manager with the `canDeleteCollection` permission can delete. Message: [MsgDeleteCollection](../../../token-standard/messages/msg-delete-collection.md).
```solidity
function deleteCollection(string calldata msgJson) external returns (bool success)
```
```json
{
"collectionId": "123"
}
```
```solidity
string memory json = TokenizationJSONHelpers.deleteCollectionJSON(collectionId);
```
Emits `CollectionDeleted`.
### setValidTokenIds
Set the collection's valid token ID ranges and, optionally, lock the permission. Returns the collection ID. Message: [MsgSetValidTokenIds](../../../token-standard/messages/msg-set-valid-token-ids.md).
```solidity
function setValidTokenIds(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"validTokenIds": [{"start": "1", "end": "2000"}],
"canUpdateValidTokenIds": []
}
```
Helper: `setValidTokenIdsJSON(collectionId, validTokenIdsJson, canUpdateValidTokenIdsJson)`.
### setManager
Transfer the manager role. Returns the collection ID. Message: [MsgSetManager](../../../token-standard/messages/msg-set-manager.md).
```solidity
function setManager(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"manager": "0x0bc63cfe31d5218eb414b142c799e20964a54a1a",
"canUpdateManager": []
}
```
Helper: `setManagerJSON(collectionId, manager, canUpdateManagerJson)`. The hex manager address is converted to bech32.
### setCollectionMetadata
Set the collection metadata (`uri`, `customData`). Returns the collection ID. Message: [MsgSetCollectionMetadata](../../../token-standard/messages/msg-set-collection-metadata.md).
```solidity
function setCollectionMetadata(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"collectionMetadata": {"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json", "customData": ""},
"canUpdateCollectionMetadata": []
}
```
Helper: `setCollectionMetadataJSON(collectionId, collectionMetadataJson, canUpdateCollectionMetadataJson)`. URIs and customData are capped at 10,000 characters.
### setTokenMetadata
Set per-token-ID metadata. Returns the collection ID. Message: [MsgSetTokenMetadata](../../../token-standard/messages/msg-set-token-metadata.md).
```solidity
function setTokenMetadata(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"tokenMetadata": [
{"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/{id}.json", "customData": "", "tokenIds": [{"start": "1", "end": "100"}]}
],
"canUpdateTokenMetadata": []
}
```
Helper: `setTokenMetadataJSON(collectionId, tokenMetadataJson, canUpdateTokenMetadataJson)` with `tokenMetadataToJson(uri, customData)`.
### setCustomData
Set the collection's `customData` string. Returns the collection ID. Message: [MsgSetCustomData](../../../token-standard/messages/msg-set-custom-data.md).
```solidity
function setCustomData(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"customData": "{\"symbol\":\"MYT\"}",
"canUpdateCustomData": []
}
```
Helper: `setCustomDataJSON(collectionId, customData, canUpdateCustomDataJson)`.
### setStandards
Set the collection's standards list. Returns the collection ID. Message: [MsgSetStandards](../../../token-standard/messages/msg-set-standards.md).
```solidity
function setStandards(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"standards": ["ERC-3643"],
"canUpdateStandards": []
}
```
Helper: `setStandardsJSON(collectionId, standardsJson, canUpdateStandardsJson)`.
### setCollectionApprovals
Replace the collection-level approvals. Returns the collection ID. Message: [MsgSetCollectionApprovals](../../../token-standard/messages/msg-set-collection-approvals.md).
```solidity
function setCollectionApprovals(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"collectionApprovals": [
{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "bb1t77myv2k0zh7evm87qedj0my9ajpsz4rp7vqsa",
"transferTimes": [{"start": "1", "end": "18446744073709551615"}],
"tokenIds": [{"start": "1", "end": "100"}],
"ownershipTimes": [{"start": "1", "end": "18446744073709551615"}],
"approvalId": "mint-by-contract",
"approvalCriteria": {"overridesFromOutgoingApprovals": true}
}
],
"canUpdateCollectionApprovals": []
}
```
Helper: `setCollectionApprovalsJSON(collectionId, collectionApprovalsJson, canUpdateCollectionApprovalsJson)` with `collectionApprovalToJson` and `collectionApprovalArrayToJson`. Hex addresses inside approvals and criteria are converted. Criteria reference: [Approval Criteria](../../../token-standard/approval-criteria/README.md).
### setIsArchived
Archive or unarchive a collection. Returns the collection ID. Message: [MsgSetIsArchived](../../../token-standard/messages/msg-set-is-archived.md).
```solidity
function setIsArchived(string calldata msgJson) external returns (uint256 collectionId)
```
```json
{
"collectionId": "123",
"isArchived": false,
"canArchiveCollection": []
}
```
Helper: `setIsArchivedJSON(collectionId, isArchived, canArchiveCollectionJson)`.
### createDynamicStore
Create a dynamic boolean store (for example a KYC registry). Returns the store ID. Message: [MsgCreateDynamicStore](../../../token-standard/messages/msg-create-dynamic-store.md).
```solidity
function createDynamicStore(string calldata msgJson) external returns (uint256 storeId)
```
```json
{
"defaultValue": false,
"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"customData": "{\"type\":\"kyc\"}"
}
```
```solidity
string memory json = TokenizationJSONHelpers.createDynamicStoreJSON(
defaultValue, // bool
uri, // string
customData // string
);
```
```solidity
string memory createJson = TokenizationJSONHelpers.createDynamicStoreJSON(
false,
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"{\"type\":\"kyc\"}"
);
uint256 storeId = TOKENIZATION.createDynamicStore(createJson);
```
Emits `DynamicStoreCreated`.
### updateDynamicStore
Update a store's default value, global enabled flag, or metadata. Only the store creator may update. Message: [MsgUpdateDynamicStore](../../../token-standard/messages/msg-update-dynamic-store.md).
```solidity
function updateDynamicStore(string calldata msgJson) external returns (bool success)
```
```json
{
"storeId": "123",
"defaultValue": false,
"globalEnabled": true,
"uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"customData": ""
}
```
Helper: `updateDynamicStoreJSON(storeId, defaultValue, globalEnabled, uri, customData)`.
### deleteDynamicStore
Delete a store. Only the creator may delete. Message: [MsgDeleteDynamicStore](../../../token-standard/messages/msg-delete-dynamic-store.md).
```solidity
function deleteDynamicStore(string calldata msgJson) external returns (bool success)
```
```json
{
"storeId": "123"
}
```
Helper: `deleteDynamicStoreJSON(storeId)`.
### setDynamicStoreValue
Set the boolean for an address in a store. Only the creator may set. Message: [MsgSetDynamicStoreValue](../../../token-standard/messages/msg-set-dynamic-store-value.md).
```solidity
function setDynamicStoreValue(string calldata msgJson) external returns (bool success)
```
```json
{
"storeId": "123",
"address": "0x0bc63cfe31d5218eb414b142c799e20964a54a1a",
"value": true
}
```
```solidity
string memory json = TokenizationJSONHelpers.setDynamicStoreValueJSON(
storeId, // uint256
address_, // address
value // bool
);
```
```solidity
string memory setValueJson = TokenizationJSONHelpers.setDynamicStoreValueJSON(
kycRegistryId,
user,
true
);
TOKENIZATION.setDynamicStoreValue(setValueJson);
```
### createAddressLists
Create one or more address lists. Message: [MsgCreateAddressLists](../../../token-standard/messages/msg-create-address-lists.md).
```solidity
function createAddressLists(string calldata msgJson) external returns (bool success)
```
```json
{
"addressLists": [
{
"listId": "my-allowlist",
"addresses": ["0x0bc63cfe31d5218eb414b142c799e20964a54a1a", "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"],
"whitelist": true,
"uri": "",
"customData": ""
}
]
}
```
Helper: `createAddressListsJSON(addressListsJson)` with `addressListInputToJson(listId, addressesJson, whitelist, uri, customData)`. At most 1,000 addresses per list. Emits `AddressListsCreated`. Concept: [Address Lists](../../../token-standard/concepts/address-lists.md).
### castVote
Cast a vote on a voting challenge attached to an approval. Message: [MsgCastVote](../../../token-standard/messages/msg-cast-vote.md).
```solidity
function castVote(string calldata msgJson) external returns (bool success)
```
```json
{
"collectionId": "123",
"approvalLevel": "collection",
"approverAddress": "",
"approvalId": "gated-transfer",
"proposalId": "proposal-1",
"yesWeight": "1"
}
```
Helper: `castVoteJSON(collectionId, approvalLevel, approverAddress, approvalId, proposalId, yesWeight)`. Criteria: [Voting Challenges](../../../token-standard/approval-criteria/voting-challenges.md).
### executeMultiple
Run several transaction messages in order, atomically, in one call.
```solidity
function executeMultiple(MessageInput[] calldata messages) external returns (bool success, bytes[] memory results)
```
```solidity
struct MessageInput {
string messageType; // Method name: "createCollection", "transferTokens", ...
string msgJson; // JSON for that method
}
```
```solidity
ITokenizationPrecompile.MessageInput[] memory messages = new ITokenizationPrecompile.MessageInput[](2);
// Message 1: Create Collection
string[] memory standards = new string[](0);
string memory createJson = TokenizationJSONHelpers.createCollectionJSON(
TokenizationJSONHelpers.uintRangeToJson(1, 1000),
TokenizationJSONHelpers.addressToString(address(this)),
TokenizationJSONHelpers.collectionMetadataToJson("ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json", ""),
TokenizationJSONHelpers.simpleUserBalanceStoreToJson(true, true, false),
"{}",
TokenizationJSONHelpers.stringArrayToJson(standards),
"",
false
);
messages[0] = ITokenizationPrecompile.MessageInput({
messageType: "createCollection",
msgJson: createJson
});
// Message 2: Transfer Tokens (using collectionId = 0 for auto-prev)
address[] memory recipients = new address[](1);
recipients[0] = 0x092bb4851ae26850588243e7bef22a56287f4739;
string memory transferJson = TokenizationJSONHelpers.transferTokensJSON(
0, // collectionId = 0 means "use previous collection" (auto-prev)
recipients,
1,
TokenizationJSONHelpers.uintRangeToJson(1, 1),
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER)
);
messages[1] = ITokenizationPrecompile.MessageInput({
messageType: "transferTokens",
msgJson: transferJson
});
// Execute both messages atomically
(bool success, bytes[] memory results) = TOKENIZATION.executeMultiple(messages);
require(success, "Multi-message execution failed");
// Decode results
uint256 collectionId = abi.decode(results[0], (uint256));
bool transferSuccess = abi.decode(results[1], (bool));
```
Behavior:
- Every transaction method name is a valid `messageType`.
- Atomic: any failure reverts the whole batch. The error names the failing index and type.
- Sequential, in array order. At most 50 messages (`MaxMessagesPerBatch`).
- `collectionId: "0"` refers to the collection created earlier in the same transaction (the module's auto-prev rule, `resolveCollectionIdWithAutoPrev`).
- Each result is ABI-encoded like the method's own return: `abi.decode(results[i], (bool))` or `(uint256)`.
- Gas: 10,000 base + 1,000 per message + 100 per 32-byte input chunk, then the transaction buffer. See [Gas](gas.md#executemultiple).
## Query Methods
Most getters return the protobuf-encoded gRPC response as `bytes`. See [Return values](README.md#return-values) for how to use them. The request JSON is the query request type from the [queries reference](../../../token-standard/queries/README.md); `0x` addresses are converted.
### getCollection
Query: [GetCollection](../../../token-standard/queries/get-collection.md).
```solidity
function getCollection(string calldata msgJson) external view returns (bytes memory collection)
```
```json
{
"collectionId": "123"
}
```
```solidity
string memory json = TokenizationJSONHelpers.getCollectionJSON(collectionId);
```
```solidity
string memory queryJson = TokenizationJSONHelpers.getCollectionJSON(collectionId);
bytes memory collection = TOKENIZATION.getCollection(queryJson);
// Protobuf-encoded QueryGetCollectionResponse; decode off-chain
```
### getCollectionStats
Holder count and circulating supply. Query: [GetCollectionStats](../../../token-standard/queries/get-collection-stats.md).
```solidity
function getCollectionStats(string calldata msgJson) external view returns (bytes memory stats)
```
```json
{
"collectionId": "123"
}
```
| Field | Type | Description |
| --- | --- | --- |
| `collectionId` | string | Collection ID (uint as string) |
```solidity
string memory queryJson = string(abi.encodePacked(
'{"collectionId":"', TokenizationJSONHelpers.uintToString(collectionId), '"}'
));
bytes memory stats = TOKENIZATION.getCollectionStats(queryJson);
uint256 holders = TokenizationDecoders.parseHolderCountFromStats(stats);
```
`parseHolderCountFromStats` reads the holder count from the protobuf bytes on-chain. `contracts/test/MaxUniqueHoldersChecker.sol` uses it to enforce a holder cap as an invariant.
### getBalance
The full balance store for an address: balances, approvals, permissions. Query: [GetBalance](../../../token-standard/queries/get-balance.md).
```solidity
function getBalance(string calldata msgJson) external view returns (bytes memory balance)
```
```json
{
"collectionId": "123",
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
}
```
`userAddress` is accepted as an alias of `address`. Helper: `getBalanceJSON(collectionId, userAddress)`. For an amount, use `getBalanceAmount` instead.
### getBalanceAmount
The amount held for one `(tokenId, ownershipTime)` pair. Returns `uint256` directly. For range queries, use `getBalance` and process the store off-chain, or pass the balances JSON through `getBalanceForIdAndTime`.
```solidity
function getBalanceAmount(string calldata msgJson) external view returns (uint256 amount)
```
```json
{
"collectionId": "123",
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"tokenId": "1",
"ownershipTime": "1609459200000"
}
```
| Field | Type | Description |
| --- | --- | --- |
| `collectionId` | string | Collection ID (uint as string) |
| `address` | string | User address (bech32 or 0x hex) |
| `tokenId` | string | Single token ID to query (uint as string) |
| `ownershipTime` | string | Single ownership time to query (uint as string, typically a ms timestamp) |
```solidity
// Build JSON manually or use a helper
string memory balanceJson = string(abi.encodePacked(
'{"collectionId":"', TokenizationJSONHelpers.uintToString(collectionId),
'","address":"', TokenizationJSONHelpers.addressToString(userAddress),
'","tokenId":"', TokenizationJSONHelpers.uintToString(tokenId),
'","ownershipTime":"', TokenizationJSONHelpers.uintToString(block.timestamp * 1000),
'"}'
));
uint256 balance = TOKENIZATION.getBalanceAmount(balanceJson);
```
Helper: `getBalanceAmountJSON(collectionId, userAddress, tokenId, ownershipTime)`. Emits `precompile_get_balance_amount`. Reverts with code 7 if the amount exceeds `uint256`.
### getTotalSupply
Total minted supply for one `(tokenId, ownershipTime)` pair. Returns `uint256`.
```solidity
function getTotalSupply(string calldata msgJson) external view returns (uint256 amount)
```
```json
{
"collectionId": "123",
"tokenId": "1",
"ownershipTime": "1609459200000"
}
```
| Field | Type | Description |
| --- | --- | --- |
| `collectionId` | string | Collection ID (uint as string) |
| `tokenId` | string | Single token ID to query (uint as string) |
| `ownershipTime` | string | Single ownership time to query (uint as string, typically a ms timestamp) |
```solidity
string memory supplyJson = string(abi.encodePacked(
'{"collectionId":"', TokenizationJSONHelpers.uintToString(collectionId),
'","tokenId":"', TokenizationJSONHelpers.uintToString(tokenId),
'","ownershipTime":"', TokenizationJSONHelpers.uintToString(block.timestamp * 1000),
'"}'
));
uint256 supply = TOKENIZATION.getTotalSupply(supplyJson);
```
Helper: `getTotalSupplyJSON(collectionId, tokenId, ownershipTime)`. Concept: [Minting and Supply](../../../token-standard/concepts/minting-and-supply.md).
### getAddressList
Query: [GetAddressList](../../../token-standard/queries/get-address-list.md).
```solidity
function getAddressList(string calldata msgJson) external view returns (bytes memory list)
```
```json
{
"listId": "my-list-id"
}
```
```solidity
string memory json = TokenizationJSONHelpers.getAddressListJSON(listId);
```
### getApprovalTracker
Tallied amounts and transfer counts for an approval tracker. Query: [GetApprovalTracker](../../../token-standard/queries/get-approval-tracker.md).
```solidity
function getApprovalTracker(string calldata msgJson) external view returns (bytes memory tracker)
```
```json
{
"collectionId": "123",
"approvalLevel": "collection",
"approverAddress": "",
"approvalId": "mint",
"amountTrackerId": "mint",
"trackerType": "overall",
"approvedAddress": ""
}
```
Helper: `getApprovalTrackerJSON(collectionId, approvalLevel, approverAddress, approvalId, trackerType, trackedAddress)` (it reuses `approvalId` as the `amountTrackerId`). `approverAddress` and `approvedAddress` accept hex. Criteria: [Approval Trackers](../../../token-standard/approval-criteria/approval-trackers.md).
### getChallengeTracker
How many times a merkle challenge leaf has been used. Returns `uint256`. Query: [GetChallengeTracker](../../../token-standard/queries/get-challenge-tracker.md).
```solidity
function getChallengeTracker(string calldata msgJson) external view returns (uint256 numUsed)
```
```json
{
"collectionId": "123",
"approvalLevel": "collection",
"approverAddress": "",
"approvalId": "claim",
"challengeTrackerId": "claim",
"leafIndex": "0"
}
```
Helper: `getChallengeTrackerJSON(collectionId, approvalLevel, approverAddress, approvalId, challengeId, leafIndex)`. Criteria: [Merkle Challenges](../../../token-standard/approval-criteria/merkle-challenges.md).
### getETHSignatureTracker
How many times an ETH signature has been used against an approval. Returns `uint256`. Query: [GetETHSignatureTracker](../../../token-standard/queries/get-eth-signature-tracker.md).
```solidity
function getETHSignatureTracker(string calldata msgJson) external view returns (uint256 numUsed)
```
```json
{
"collectionId": "123",
"approvalLevel": "collection",
"approverAddress": "",
"approvalId": "signed-claim",
"challengeTrackerId": "signed-claim",
"signature": "0xdbcfa79865ccab596d4e5fbc06e34ba09befd01cfd833a73427bd38d5cab2e77dcdbfee5f745b5b082bd17b9ae41a3cffb160ee9343fd777de4a3fbd682397301b"
}
```
Criteria: [ETH Signature Challenges](../../../token-standard/approval-criteria/eth-signature-challenges.md).
### getDynamicStore
Store configuration: creator, default value, global enabled flag, metadata. Query: [GetDynamicStore](../../../token-standard/queries/get-dynamic-store.md).
```solidity
function getDynamicStore(string calldata msgJson) external view returns (bytes memory store)
```
```json
{
"storeId": "123"
}
```
Helper: `getDynamicStoreJSON(storeId)`.
### getDynamicStoreValue
The boolean for an address in a store, as protobuf bytes. Query: [GetDynamicStoreValue](../../../token-standard/queries/get-dynamic-store-value.md).
```solidity
function getDynamicStoreValue(string calldata msgJson) external view returns (bytes memory value)
```
```json
{
"storeId": "123",
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
}
```
```solidity
string memory json = TokenizationJSONHelpers.getDynamicStoreValueJSON(
storeId,
userAddress
);
```
```solidity
string memory getValueJson = TokenizationJSONHelpers.getDynamicStoreValueJSON(
kycRegistryId,
user
);
bytes memory result = TOKENIZATION.getDynamicStoreValue(getValueJson);
// Protobuf-encoded QueryGetDynamicStoreValueResponse. Decode off-chain, or
// enforce the store on-chain with a dynamic store challenge instead.
```
`userAddress` is accepted as an alias of `address`.
### getWrappableBalances
How much of a wrapped denom the address can unwrap back into collection tokens. Returns `uint256`. Query: [GetWrappableBalances](../../../token-standard/queries/get-wrappable-balances.md).
```solidity
function getWrappableBalances(string calldata msgJson) external view returns (uint256 amount)
```
```json
{
"denom": "badges:1:utoken",
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
}
```
Helper: `getWrappableBalancesJSON(denom, address)`. Concept: [Cosmos Coin Wrapper Paths](../../../token-standard/ibc/cosmos-coin-wrapper-paths.md).
### isAddressReservedProtocol
Whether an address is a reserved protocol address. Returns `bool`. The zero address returns `false`. Query: [IsAddressReservedProtocol](../../../token-standard/queries/is-address-reserved-protocol.md).
```solidity
function isAddressReservedProtocol(string calldata msgJson) external view returns (bool isReserved)
```
```json
{
"address": "0x0bc63cfe31d5218eb414b142c799e20964a54a1a"
}
```
Helper: `isAddressReservedProtocolJSON(address)`.
### getAllReservedProtocolAddresses
All reserved protocol addresses, returned as EVM addresses. Query: [GetAllReservedProtocolAddresses](../../../token-standard/queries/get-all-reserved-protocol-addresses.md).
```solidity
function getAllReservedProtocolAddresses(string calldata msgJson) external view returns (address[] memory addresses)
```
```json
{}
```
Helper: `getAllReservedProtocolAddressesJSON()`. Pass `"{}"` or an empty string.
### getVote
One voter's vote on a proposal. Query: [GetVote](../../../token-standard/queries/get-vote.md).
```solidity
function getVote(string calldata msgJson) external view returns (bytes memory vote)
```
```json
{
"collectionId": "123",
"approvalLevel": "collection",
"approverAddress": "",
"approvalId": "gated-transfer",
"proposalId": "proposal-1",
"voterAddress": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf"
}
```
Helper: `getVoteJSON(collectionId, approvalLevel, approverAddress, approvalId, proposalId, voterAddress)`.
### getVotes
All votes on a proposal. Query: [GetVotes](../../../token-standard/queries/get-votes.md).
```solidity
function getVotes(string calldata msgJson) external view returns (bytes memory votes)
```
```json
{
"collectionId": "123",
"approvalLevel": "collection",
"approverAddress": "",
"approvalId": "gated-transfer",
"proposalId": "proposal-1"
}
```
Helper: `getVotesJSON(collectionId, approvalLevel, approverAddress, approvalId, proposalId)`.
### params
Module parameters. Query: [Params](../../../token-standard/queries/params.md).
```solidity
function params(string calldata msgJson) external view returns (bytes memory params)
```
```json
{}
```
Helper: `paramsJSON()`. Also the cheapest connectivity check: `precompile.params("{}")`.
## Utility Methods
Pure functions with no state access.
### convertEvmAddressToBech32
```solidity
function convertEvmAddressToBech32(address evmAddress) external pure returns (string memory bech32Address)
```
```solidity
string memory bech32 = TOKENIZATION.convertEvmAddressToBech32(0x0bc63cfe31d5218eb414b142c799e20964a54a1a);
// Returns: "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
```
### convertBech32ToEvmAddress
```solidity
function convertBech32ToEvmAddress(string calldata bech32Address) external pure returns (address evmAddress)
```
```solidity
address evm = TOKENIZATION.convertBech32ToEvmAddress("bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d");
// Returns: 0x0bc63cfe31d5218eb414b142c799e20964a54a1a
```
### rangeContains
Inclusive check.
```solidity
function rangeContains(uint256 start, uint256 end, uint256 value) external pure returns (bool contains)
```
```solidity
bool isInRange = TOKENIZATION.rangeContains(10, 20, 15);
// Returns: true (15 is in [10, 20])
bool notInRange = TOKENIZATION.rangeContains(10, 20, 25);
// Returns: false (25 is not in [10, 20])
```
### rangesOverlap
```solidity
function rangesOverlap(uint256 start1, uint256 end1, uint256 start2, uint256 end2) external pure returns (bool overlap)
```
```solidity
bool overlap = TOKENIZATION.rangesOverlap(10, 20, 15, 25);
// Returns: true (ranges [10,20] and [15,25] overlap)
bool noOverlap = TOKENIZATION.rangesOverlap(10, 20, 25, 35);
// Returns: false (ranges [10,20] and [25,35] don't overlap)
```
### searchInRanges
Whether a value falls inside any range of a JSON range array.
```solidity
function searchInRanges(string calldata rangesJson, uint256 value) external pure returns (bool found)
```
```solidity
string memory rangesJson = '[{"start":"1","end":"100"},{"start":"200","end":"300"}]';
bool found = TOKENIZATION.searchInRanges(rangesJson, 50);
// Returns: true (50 is in [1,100])
bool notFound = TOKENIZATION.searchInRanges(rangesJson, 150);
// Returns: false (150 is not in any range)
```
### getBalanceForIdAndTime
The amount for a token ID and time inside a JSON balances array. Useful for balances you obtained off-chain or built yourself. This pure helper parses the legacy `badgeIds` key for the token ID ranges (the handler in `precompile.go` reads `badgeIds`, not `tokenIds`).
```solidity
function getBalanceForIdAndTime(string calldata balancesJson, uint256 tokenId, uint256 time) external pure returns (uint256 amount)
```
```solidity
string memory balancesJson = '[{"amount":"100","badgeIds":[{"start":"1","end":"10"}],"ownershipTimes":[{"start":"0","end":"18446744073709551615"}]}]';
uint256 amount = TOKENIZATION.getBalanceForIdAndTime(balancesJson, 5, block.timestamp * 1000);
// Returns: 100 (token ID 5 is in range [1,10] and time is in [0, max])
uint256 notFound = TOKENIZATION.getBalanceForIdAndTime(balancesJson, 15, block.timestamp * 1000);
// Returns: 0 (token ID 15 is not in any range)
```
### getReservedListId
The reserved address list ID for an address, which is its bech32 form. Every address has an implicit list containing only itself.
```solidity
function getReservedListId(address addr) external pure returns (string memory listId)
```
```solidity
string memory listId = TOKENIZATION.getReservedListId(0x0bc63cfe31d5218eb414b142c799e20964a54a1a);
// Returns: "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d" (the bech32 address)
```
To test whether a list ID is `"All"`, compare the string: `keccak256(bytes(listId)) == keccak256(bytes("All"))`. Reserved IDs: [Address Lists](../../../token-standard/concepts/address-lists.md).
## Helper Library Reference
Building blocks in `TokenizationJSONHelpers`:
```solidity
string memory json = TokenizationJSONHelpers.uintRangeToJson(1, 100);
// Returns: [{"start":"1","end":"100"}]
```
```solidity
uint256[] memory starts = new uint256[](2);
uint256[] memory ends = new uint256[](2);
starts[0] = 1; ends[0] = 100;
starts[1] = 200; ends[1] = 300;
string memory json = TokenizationJSONHelpers.uintRangeArrayToJson(starts, ends);
// Returns: [{"start":"1","end":"100"},{"start":"200","end":"300"}]
```
```solidity
string memory json = TokenizationJSONHelpers.collectionMetadataToJson(
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json",
"{\"name\":\"My Token\"}"
);
```
```solidity
string memory json = TokenizationJSONHelpers.simpleUserBalanceStoreToJson(
true, // autoApproveSelfInitiatedOutgoingTransfers
true, // autoApproveSelfInitiatedIncomingTransfers
false // autoApproveAllIncomingTransfers
);
```
```solidity
string[] memory standards = new string[](2);
standards[0] = "ERC-3643";
standards[1] = "Security Token";
string memory json = TokenizationJSONHelpers.stringArrayToJson(standards);
// Returns: ["ERC-3643","Security Token"]
```
```solidity
string memory str = TokenizationJSONHelpers.uintToString(123);
// Returns: "123"
```
Further builders: `balanceToJson`, `balanceArrayToJson`, `tokenMetadataToJson`, `collectionApprovalToJson`, `userOutgoingApprovalToJson`, `userIncomingApprovalToJson`, `collectionPermissionsToJson`, `userPermissionsToJson`, `evmQueryChallengeToJson`, `collectionInvariantsToJson`, `cosmosCoinWrapperPathToJson`, `aliasPathToJson`, `denomUnitToJson`, `addressListInputToJson`, `approvalIdentifierDetailsToJson`. Source: [`TokenizationJSONHelpers.sol`](https://github.com/BitBadges/bitbadgeschain/blob/master/contracts/libraries/TokenizationJSONHelpers.sol).
## Events
The precompile emits Cosmos events (`precompile_transfer_tokens`, `precompile_set_incoming_approval`, `precompile_set_outgoing_approval`, `precompile_get_balance_amount`) with `module=evm_precompile`. The Solidity interface declares the matching EVM events `TransferTokens`, `SetIncomingApproval`, `SetOutgoingApproval`, `CollectionCreated`, `CollectionUpdated`, `CollectionDeleted`, `AddressListsCreated`, `DynamicStoreCreated`. The module's own events are emitted too; see [WebSocket Events](../../websocket-events.md).
## Related
- [Tokenization Precompile](README.md)
- [Errors](errors.md)
- [Gas](gas.md)
- [Messages](../../../token-standard/messages/README.md)
## File: chain/evm/tokenization-precompile/errors.md
---
description: "Tokenization precompile errors: the nine codes from errors.go, the uint64 range trap, common failures with fixes, Solidity handling, debugging."
---
# Errors
Every tokenization precompile failure reverts with a `PrecompileError` in the form `precompile error [code=N]: message: details`. This page lists the codes, the common failures, and how to handle them in Solidity.
```solidity
try precompile.transferTokens(json) returns (bool success) {
require(success, "Transfer failed");
} catch Error(string memory reason) {
// reason = "precompile error [code=4]: transfer failed: ..."
revert(reason);
}
```
## Error Codes
From `x/tokenization/precompile/errors.go`.
| Code | Name | Message | Raised when |
| --- | --- | --- | --- |
| 1 | `InvalidInput` | `invalid input parameters` | Bad JSON, missing or mistyped field, failed `ValidateBasic`, zero address, invalid range, amount zero, size limit exceeded |
| 2 | `CollectionNotFound` | `collection not found` | The collection ID does not exist (`ErrCollectionNotExists`, `ErrInvalidCollectionID`) |
| 3 | `BalanceNotFound` | `balance not found` | The user balance store does not exist (`ErrUserBalanceNotExists`) |
| 4 | `TransferFailed` | `transfer failed` | Disallowed transfer, underflow (insufficient balance), overflow (`ErrDisallowedTransfer`, `ErrUnderflow`, `ErrOverflow`) |
| 5 | `ApprovalFailed` | `approval operation failed` | An approval message failed in the keeper |
| 6 | `QueryFailed` | `query failed` | Account, address list, or approval not found; other keeper query errors (`ErrAccountNotFound`, `ErrAddressListNotFound`, `ErrApprovalNotFound`, `ErrNotFound`) |
| 7 | `InternalError` | `internal error` | Nil response, marshal failure, value above `uint256` |
| 8 | `Unauthorized` | `unauthorized operation` | Zero-address caller, inadequate approvals, manager or permission checks (`ErrInadequateApprovals`, `ErrUnauthorized`) |
| 9 | `CollectionArchived` | `collection is archived (read-only)` | Write to an archived collection (`ErrCollectionIsArchived`) |
Keeper errors are mapped by `MapCosmosErrorToPrecompileError`; unmapped errors take the default code of the handler that caught them. Details are sanitized and truncated to 500 characters. Field-level input errors carry a path such as `field 'transfers[0].balances[0].amount': ...`.
## The uint64 Trap
The most common error for new developers. IDs and timestamps are `uint64` on the chain.
```solidity
// Wrong: causes a range overflow error
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(1, type(uint256).max);
// Correct: use the FOREVER constant
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(
1,
TokenizationJSONHelpers.FOREVER // = type(uint64).max = 18446744073709551615
);
```
| Constant | Value | Use |
| --- | --- | --- |
| `FOREVER` | `18446744073709551615` | Ownership times that never expire |
| `MAX_TIME` | `18446744073709551615` | Maximum valid timestamp |
| `MAX_ID` | `18446744073709551615` | Maximum valid token ID |
| `MIN_ID` | `1` | Minimum valid ID (ranges start at 1) |
| `FOREVER_STR` | `"18446744073709551615"` | For direct JSON string use |
Defined in both `TokenizationJSONHelpers` and `TokenizationHelpers`.
## Handling Errors in Solidity
Basic:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ITokenizationPrecompile.sol";
import "./libraries/TokenizationJSONHelpers.sol";
contract SafeTransfer {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function safeTransfer(string memory json) external {
try precompile.transferTokens(json) returns (bool success) {
require(success, "Transfer failed");
} catch Error(string memory reason) {
// Precompile error with message
revert(reason);
} catch (bytes memory lowLevelData) {
// Low-level error - decode if possible
revert("Unknown precompile error");
}
}
}
```
With the `TokenizationErrors` library, which validates before the call:
```solidity
import "./libraries/TokenizationErrors.sol";
contract MyContract {
using TokenizationErrors for *;
function transfer(uint256 collectionId, string memory json) external {
// Validate inputs before calling precompile
TokenizationErrors.requireValidCollectionId(collectionId);
TokenizationErrors.requireNonEmptyString(json, "JSON");
bool success = precompile.transferTokens(json);
require(success, "Transfer failed");
}
}
```
## Common Failures
### Range Value Overflow
```text
precompile error [code=1]: invalid input parameters: message validation failed: invalid balance times:
range at index 0 has end 115792089237316195423570985008687907853269984665640564039457584007913129639935
greater than max 18446744073709551615
```
Cause: `type(uint256).max` instead of `type(uint64).max`.
```solidity
// Use the FOREVER constant
string memory ownershipJson = TokenizationJSONHelpers.uintRangeToJson(
1,
TokenizationJSONHelpers.FOREVER
);
// Or use the MAX_TIME/MAX_ID constants from TokenizationHelpers
uint64 maxTime = TokenizationHelpers.MAX_TIME;
```
### Address Cannot Be Empty
```text
precompile error [code=1]: invalid input parameters: address cannot be empty
```
Cause: a wrong field name, or an empty address. Query requests accept `userAddress` as an alias of `address` only for `getBalance` and `getDynamicStoreValue`; messages do not.
```solidity
// Wrong: "userAddress" is not a field of MsgSetDynamicStoreValue
'{"storeId":"1","userAddress":"0x0bc63cfe31d5218eb414b142c799e20964a54a1a","value":true}'
// Correct: use "address"
'{"storeId":"1","address":"0x0bc63cfe31d5218eb414b142c799e20964a54a1a","value":true}'
// Or use the helper
string memory json = TokenizationJSONHelpers.setDynamicStoreValueJSON(
storeId,
userAddress, // address type, not string
true
);
```
### Failed to Unmarshal JSON
```text
failed to unmarshal JSON for method X: precompile error [code=1]: invalid input parameters: ...
```
Causes: invalid JSON syntax (missing quotes, commas, brackets); wrong field types (numbers as raw numbers, booleans as strings); missing required fields; unknown fields.
| Type | Correct | Wrong |
| --- | --- | --- |
| Numbers | `"123"` | `123` |
| Booleans | `true` / `false` | `"true"` / `"false"` |
| Addresses | `"0x0bc63cfe31d5218eb414b142c799e20964a54a1a"` | `0x0bc63cfe31d5218eb414b142c799e20964a54a1a` (unquoted) |
| Ranges | `[{"start":"1","end":"100"}]` | `[{start:1,end:100}]` |
```json
{
"storeId": "123",
"address": "0x0bc63cfe31d5218eb414b142c799e20964a54a1a",
"value": true
}
```
### Collection Not Found
```text
precompile error [code=2]: collection not found: collectionId: 12345
```
```solidity
// Check collection exists before operations
try precompile.getCollection(TokenizationJSONHelpers.getCollectionJSON(collectionId)) {
// Collection exists, proceed
} catch {
revert("Collection does not exist");
}
```
### Insufficient Balance
```text
precompile error [code=4]: transfer failed: underflow error: ...
```
Cause: the transfer exceeds what the sender holds for those token IDs and ownership times.
```solidity
// Check balance before transfer
uint256 balance = precompile.getBalanceAmount(
TokenizationJSONHelpers.getBalanceAmountJSON(
collectionId,
msg.sender,
tokenId,
block.timestamp
)
);
require(balance >= amount, "Insufficient balance");
```
Inside a contract, `msg.sender` here is the contract's own caller, but the precompile transfers from the contract's balance. Check the address that actually holds the tokens.
### Not Authorized
```text
precompile error [code=8]: unauthorized operation: ...
```
Causes: the caller is not the collection manager; no approval covers the transfer; the permission is locked in the collection permissions; the time window for the operation has passed.
Debugging steps:
1. Check who the collection manager is (`getCollection`).
2. Verify the outgoing, incoming, and collection approvals cover the transfer.
3. Check whether the permission is locked (`permanentlyForbiddenTimes`).
4. Verify the current time is inside the allowed window.
```solidity
// Get collection to check manager
bytes memory collectionBytes = precompile.getCollection(
TokenizationJSONHelpers.getCollectionJSON(collectionId)
);
// Decode off-chain and check the manager field
// For transfers, ensure approvals are set
// Check outgoing approval from sender
// Check incoming approval for recipient
```
Concepts: [Transferability](../../../token-standard/concepts/transferability.md), [Permissions](../../../token-standard/concepts/permissions.md).
### Collection Archived
```text
precompile error [code=9]: collection is archived (read-only): collectionId: 123
```
```solidity
// If you are the manager and need to modify, unarchive first
string memory json = TokenizationJSONHelpers.setIsArchivedJSON(
collectionId,
false, // unarchive
"[]" // canArchiveCollection permission
);
precompile.setIsArchived(json);
```
### Invalid Range (Start > End)
```text
precompile error [code=1]: invalid input parameters: invalid range: start 100 is greater than end 50
```
```solidity
// Validate ranges before building JSON
require(startTime <= endTime, "Invalid time range");
require(startTokenId <= endTokenId, "Invalid token ID range");
string memory rangeJson = TokenizationJSONHelpers.uintRangeToJson(startTime, endTime);
```
### EVM Query Challenge Failed
A transfer gated by an [EVM query challenge](../../../token-standard/approval-criteria/evm-query-challenges.md) or an invariant fails as a transfer or authorization error (code 4 or 8) with the challenge result in the details, for example `contract returned 0, expected >= 1`.
Debugging:
1. Check which contract the challenge calls.
2. Verify the calldata (including `$sender` style placeholders).
3. Check the expected result and the comparison operator.
4. Confirm the target contract is deployed and returns a value.
```solidity
// Test the EVM query manually before using in invariants
(bool success, bytes memory result) = targetContract.staticcall(callData);
require(success, "EVM call failed");
uint256 value = abi.decode(result, (uint256));
require(value >= 1, "challenge would fail: contract returned 0, expected >= 1");
```
### Dynamic Store Not Found
```text
precompile error [code=6]: query failed: ... store 999 does not exist
```
```solidity
// Verify store exists
try precompile.getDynamicStore(TokenizationJSONHelpers.getDynamicStoreJSON(storeId)) {
// Store exists
} catch {
// Create the store first
uint256 newStoreId = precompile.createDynamicStore(
TokenizationJSONHelpers.createDynamicStoreJSON(false, "", "")
);
}
```
### Approval Not Found
```text
precompile error [code=6]: query failed: approval "my-approval" does not exist
```
Cause: deleting or referencing an approval ID that does not exist.
```solidity
// Approvals are identified by approvalId string. Delete inside try/catch so a
// missing approval does not revert the whole call.
try precompile.deleteOutgoingApproval(
TokenizationJSONHelpers.deleteOutgoingApprovalJSON(collectionId, "my-approval")
) returns (bool) {
// deleted
} catch Error(string memory reason) {
// "precompile error [code=6]: query failed: approval \"my-approval\" does not exist"
emit ApprovalMissing(reason);
}
```
### Invalid Approval Criteria
```text
precompile error [code=1]: invalid input parameters: message validation failed: merkle root is required for merkle challenge
```
Common causes: a merkle challenge without a root; voting criteria without a proposal ID; an approval tracker without limits. Field reference: [Approval Criteria](../../../token-standard/approval-criteria/README.md).
## Debugging
Log the JSON before sending:
```solidity
// Emit event with JSON for debugging (remove in production)
event DebugJSON(string json);
function debugTransfer(uint256 collectionId, address to, uint256 amount) external {
address[] memory recipients = new address[](1);
recipients[0] = to;
string memory json = TokenizationJSONHelpers.transferTokensJSON(
collectionId,
recipients,
amount,
TokenizationJSONHelpers.uintRangeToJson(1, 1),
TokenizationJSONHelpers.uintRangeToJson(1, TokenizationJSONHelpers.FOREVER)
);
emit DebugJSON(json);
precompile.transferTokens(json);
}
```
Validate the JSON outside the chain: copy it from the log, check it in a JSON validator, and compare field names against the [message page](../../../token-standard/messages/README.md).
Test components on their own:
```solidity
// Test range construction
string memory rangeJson = TokenizationJSONHelpers.uintRangeToJson(1, 100);
// Expected: [{"start":"1","end":"100"}]
// Test address conversion
string memory addrStr = TokenizationJSONHelpers.addressToString(0x0bc63cfe31d5218eb414b142c799e20964a54a1a);
// Expected: 0x0bc63cfe31d5218eb414b142c799e20964a54a1a
```
Check that the precompile responds:
```solidity
// Simple connectivity test
try precompile.params("{}") {
// Precompile is responding
} catch {
revert("Precompile not available");
}
```
Convert the caller for a Cosmos-side check:
```solidity
// Convert EVM address to bech32 for checking on Cosmos side
string memory bech32 = precompile.convertEvmAddressToBech32(0x0bc63cfe31d5218eb414b142c799e20964a54a1a);
// Returns: bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d
```
## JSON Quick Reference
| Field type | JSON format | Example |
| --- | --- | --- |
| `uint256` | String | `"123456789"` |
| `uint64` | String | `"18446744073709551615"` |
| `bool` | Raw boolean | `true` or `false` |
| `address` | Hex string or bech32 | `"0x0bc63cfe31d5218eb414b142c799e20964a54a1a"` or `"bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"` |
| `string` | Quoted string | `"hello world"` |
| `UintRange[]` | Object array | `[{"start":"1","end":"100"}]` |
| `string[]` | String array | `["a","b","c"]` |
| Empty object | `{}` | `{}` |
| Empty array | `[]` | `[]` |
## Related
- [API reference](api.md)
- [Security](security.md)
- [Example contracts](https://github.com/BitBadges/bitbadgeschain/tree/master/contracts/examples)
- [Errors source](https://github.com/BitBadges/bitbadgeschain/blob/master/x/tokenization/precompile/errors.go)
## File: chain/evm/tokenization-precompile/gas.md
---
description: "Tokenization precompile gas: per-method base costs, the fixed buffers RequiredGas adds, executeMultiple and input-size costs, per-element constants."
---
# Gas
The tokenization precompile charges a base cost per method plus a fixed buffer, computed in `RequiredGas` from the method ID and input size. This page lists the numbers from `x/tokenization/precompile/precompile.go` and `gas.go`.
```go
// RequiredGas, simplified
baseGas := baseGasForMethod(methodID) // the per-method constant from the tables below
if isTransaction {
return baseGas + 200_000
}
return baseGas + 50_000
```
The base is deducted before the precompile runs. The keeper's own work (state reads and writes, approval checks, events) then draws from the remaining gas of the call. The buffer exists so `eth_estimateGas` converges on a value that works.
## Transaction Methods
| Method | Base gas | Charged up front |
| --- | --- | --- |
| `transferTokens` | 30,000 | 230,000 |
| `setIncomingApproval` | 20,000 | 220,000 |
| `setOutgoingApproval` | 20,000 | 220,000 |
| `createCollection` | 50,000 | 250,000 |
| `updateCollection` | 40,000 | 240,000 |
| `universalUpdateCollection` | 50,000 | 250,000 |
| `deleteCollection` | 20,000 | 220,000 |
| `createAddressLists` | 30,000 | 230,000 |
| `updateUserApprovals` | 30,000 | 230,000 |
| `deleteIncomingApproval` | 15,000 | 215,000 |
| `deleteOutgoingApproval` | 15,000 | 215,000 |
| `purgeApprovals` | 25,000 | 225,000 |
| `createDynamicStore` | 20,000 | 220,000 |
| `updateDynamicStore` | 20,000 | 220,000 |
| `deleteDynamicStore` | 15,000 | 215,000 |
| `setDynamicStoreValue` | 15,000 | 215,000 |
| `setValidTokenIds` | 20,000 | 220,000 |
| `setManager` | 15,000 | 215,000 |
| `setCollectionMetadata` | 15,000 | 215,000 |
| `setTokenMetadata` | 20,000 | 220,000 |
| `setCustomData` | 15,000 | 215,000 |
| `setStandards` | 15,000 | 215,000 |
| `setCollectionApprovals` | 30,000 | 230,000 |
| `setIsArchived` | 15,000 | 215,000 |
| `castVote` | 15,000 | 215,000 |
### executeMultiple
```text
base = 10,000
+ 1,000 * min(messageCount, 50)
+ 100 * floor(len(input) / 32)
charged = base + 200,000
```
The per-chunk term prices JSON parsing by input size, so a large batch cannot be under-charged. Each message's own keeper work still draws from the remaining gas.
## Query Methods
| Method | Base gas | Charged up front |
| --- | --- | --- |
| `getCollection` | 3,000 | 53,000 |
| `getBalance` | 3,000 | 53,000 |
| `getBalanceAmount` | 3,000 | 53,000 |
| `getTotalSupply` | 3,000 | 53,000 |
| `getCollectionStats` | 3,000 | 53,000 |
| `getAddressList` | 5,000 | 55,000 |
| `getApprovalTracker` | 5,000 | 55,000 |
| `getChallengeTracker` | 5,000 | 55,000 |
| `getETHSignatureTracker` | 5,000 | 55,000 |
| `getDynamicStore` | 5,000 | 55,000 |
| `getDynamicStoreValue` | 5,000 | 55,000 |
| `getWrappableBalances` | 5,000 | 55,000 |
| `getAllReservedProtocolAddresses` | 5,000 | 55,000 |
| `getVote` | 5,000 | 55,000 |
| `getVotes` | 5,000 | 55,000 |
| `isAddressReservedProtocol` | 2,000 | 52,000 |
| `params` | 2,000 | 52,000 |
## Utility Methods
Pure functions, no state access. They still receive the `+50,000` query buffer in `RequiredGas`.
| Method | Base gas | Extra |
| --- | --- | --- |
| `convertEvmAddressToBech32` | 500 | |
| `convertBech32ToEvmAddress` | 500 | |
| `rangeContains` | 200 | |
| `rangesOverlap` | 200 | |
| `searchInRanges` | 500 | + 100 per 32-byte input chunk |
| `getBalanceForIdAndTime` | 500 | + 100 per 32-byte input chunk |
| `getReservedListId` | 300 | |
## Per-Element Constants
`gas.go` defines calculators for size-proportional estimates. `RequiredGas` does not add these on top of the base today; the module's own gas metering covers the real per-element work. They are useful for off-chain estimation and may be applied in a future version.
| Constant | Gas | Used by |
| --- | --- | --- |
| `GasPerRecipient` | 5,000 | `CalculateTransferGas` |
| `GasPerTokenIdRange` | 1,000 | `CalculateTransferGas`, `CalculateApprovalGas` |
| `GasPerOwnershipTimeRange` | 1,000 | `CalculateTransferGas`, `CalculateApprovalGas` |
| `GasPerApprovalField` | 500 | `CalculateApprovalGas` (per transfer time range) |
| `GasPerQueryRange` | 500 | `CalculateQueryGas` |
| `GasPerInputChunk` | 100 | `executeMultiple`, `searchInRanges`, `getBalanceForIdAndTime` |
| `GasPerMessageInBatch` | 1,000 | `executeMultiple` |
```go
// CalculateTransferGas
gas := GasTransferTokensBase
gas += len(toAddresses) * GasPerRecipient
gas += len(tokenIdsRanges) * GasPerTokenIdRange
gas += len(ownershipTimesRanges) * GasPerOwnershipTimeRange
```
## Limits That Affect Gas
| Limit | Value |
| --- | --- |
| `MaxMessagesPerBatch` | 50 |
| `MaxQueryArraySize` (token ID or ownership time ranges in a query) | 1,000 |
| `MaxRecipients`, `MaxTokenIdRanges`, `MaxOwnershipTimeRanges`, `MaxApprovalRanges` | 100 |
Full list on [Security](security.md).
## Spending Less
- Fewer recipients per transfer; merge adjacent token ID and ownership time ranges.
- Use `getBalanceAmount` and `getTotalSupply` (direct `uint256`) instead of `getBalance` when you only need an amount.
- Batch with `executeMultiple` instead of separate transactions; one buffer instead of N.
- Use `rangeContains` and `searchInRanges` instead of hand-written range loops in Solidity.
- Cache JSON strings you reuse across calls.
- Simple operations are cheaper as native Cosmos messages than through the EVM. See [Developer Guide](../developer-guide.md).
## Related
- [API reference](api.md)
- [Security](security.md)
- [GAMM Precompile API](../gamm-precompile/api.md#gas)
## File: chain/evm/tokenization-precompile/security.md
---
description: "The tokenization precompile security model from security.go: caller checks, overflow, validation, DoS limits, error sanitization, threat model."
---
# Security
The tokenization precompile's protections live in `x/tokenization/precompile/security.go` and `validation.go`. This page states what the code enforces so contract authors know what they still have to check themselves.
```go
// Every transaction method starts here
caller := contract.Caller()
if err := VerifyCaller(caller); err != nil {
return nil, err
}
```
## What the Precompile Enforces
### Caller Verification
- `contract.Caller()` is the address that made the call. The EVM sets it; a contract cannot forge it.
- The caller becomes the `creator` on tokenization messages. A transfer's `from` remains the requested sender and is subject to approvals. Any `creator` in the JSON is overwritten.
- A zero-address caller is rejected by `VerifyCaller` with error code 8.
The caller is the immediate caller, not the transaction origin. See [Developer Guide](../developer-guide.md#precompile-caller).
### Reentrancy
- Transaction atomicity rolls back failed state changes; it is not a reentrancy guard.
- Approval criteria and collection invariants can invoke EVM contracts through [EVM query challenges](../../../token-standard/approval-criteria/evm-query-challenges.md). Review these callbacks when reasoning about a transfer's call graph.
- Contracts must protect their own state around external calls, using checks-effects-interactions and a reentrancy guard where needed. The EVM call stack does not provide this protection automatically. See [Solidity's reentrancy guidance](https://docs.solidity.org/en/latest/security-considerations.html#reentrancy).
### Overflow
- Every `big.Int` is validated before conversion to `sdkmath.Uint`.
- `CheckOverflow` rejects nil, negative, and values above `2^256-1` (`MaxUint256`), so nothing is silently truncated on the way back to Solidity.
- Ranges must satisfy `start <= end`; amounts must be greater than zero.
- IDs and times are `uint64` on the chain. Values above `18446744073709551615` fail validation. Use `FOREVER`.
```go
func CheckOverflow(value *big.Int, fieldName string) error {
if value == nil {
return ErrInvalidInput(fmt.Sprintf("%s cannot be nil", fieldName))
}
if value.Sign() < 0 {
return ErrInvalidInput(fmt.Sprintf("%s cannot be negative", fieldName))
}
if value.Cmp(MaxUint256) > 0 {
return ErrInvalidInput(fmt.Sprintf("%s overflow: value exceeds maximum uint256 (2^256-1)", fieldName))
}
return nil
}
```
### Input Validation
- Zero addresses are rejected (`ValidateAddress`).
- Empty arrays are rejected where the field is required (`ValidateArraySize`).
- Invalid ranges are rejected (`ValidateBigIntRanges`).
- Collection IDs must be non-zero (`ValidateCollectionId`); zero IDs in queries are rejected by `validateQueryRequest`.
- Required strings must be non-empty (`ValidateString`).
- Every message runs its `ValidateBasic` before the keeper call.
### DoS Limits
Array sizes are capped so a single call cannot exhaust the node.
| Field | Maximum |
| --- | --- |
| Recipients per transfer (`MaxRecipients`) | 100 |
| Token ID ranges (`MaxTokenIdRanges`) | 100 |
| Ownership time ranges (`MaxOwnershipTimeRanges`) | 100 |
| Approval ranges (`MaxApprovalRanges`) | 100 |
| Denom units per path (`MaxDenomUnits`) | 50 |
| Merkle challenges per approval (`MaxMerkleChallenges`) | 20 |
| Coin transfers per approval (`MaxCoinTransfers`) | 50 |
| Dynamic store challenges (`MaxDynamicStoreChallenges`) | 20 |
| ETH signature challenges (`MaxETHSignatureChallenges`) | 20 |
| Voting challenges (`MaxVotingChallenges`) | 20 |
| EVM query challenges (`MaxEVMQueryChallenges`) | 10 |
| Must-own-tokens rules (`MaxMustOwnTokens`) | 50 |
| Addresses per address list (`MaxAddressListEntries`) | 1,000 |
| Metadata string length, URI or customData (`MaxMetadataLength`) | 10,000 characters |
| Messages per `executeMultiple` (`MaxMessagesPerBatch`) | 50 |
| Ranges per query array (`MaxQueryArraySize`) | 1,000 |
Input size also adds gas (`GasPerInputChunk`) on `executeMultiple`, `searchInRanges`, and `getBalanceForIdAndTime`, so large JSON cannot be under-priced. See [Gas](gas.md).
### Error Handling
- Errors are structured `PrecompileError` values with a code, message, and details.
- Details are sanitized: file paths, Go internals, module paths, and IP addresses are redacted; messages longer than 500 characters are truncated.
- Codes let a contract branch without parsing text. See [Errors](errors.md).
### State Consistency
- All writes go through the module keeper.
- A transaction either applies every change or none. A failing `executeMultiple` message reverts the whole batch.
## Threat Model
| Threat | Protection |
| --- | --- |
| Reentrancy | Review EVM query callbacks and protect application state around external calls; atomicity alone is insufficient |
| Integer overflow | `CheckOverflow`, range validation, `sdkmath.Uint` arithmetic |
| Invalid input | Validation of every field before the keeper call |
| DoS through large inputs | Array size limits, input-size gas |
| Information leakage | Sanitized, truncated error details |
| State corruption | Atomic transactions, keeper validation |
| Caller spoofing | `contract.Caller()`, creator overwritten on the Go side |
## Known Limitations
- No rate limiting at the precompile level. Add it at the chain or contract level if you need it.
- Gas price manipulation is handled by the EVM module, not the precompile.
- Access control (who may transfer, who may update a collection) is the tokenization module's approval and permission system. The precompile does not add its own authorization layer.
- Full protobuf decoding of query responses is not available in Solidity. See [Return values](README.md#return-values).
## What Your Contract Must Still Do
1. Validate inputs before building JSON. The precompile rejects bad input, but a revert after JSON construction wastes gas.
2. Check return values. Transaction methods return `bool success` or an ID.
3. Wrap calls in `try`/`catch` where you want to recover instead of revert.
4. Use `TokenizationJSONHelpers` and `TokenizationErrors` so the JSON matches the schema.
5. Review the collection's approvals and permissions. A transfer from a contract is subject to the same rules as any other sender. See [Transferability](../../../token-standard/concepts/transferability.md) and [Permissions](../../../token-standard/concepts/permissions.md).
6. Do authorization in the contract. The precompile sees the contract as the caller, never the user behind it.
## Related
- [Errors](errors.md)
- [Gas](gas.md)
- [Developer Guide](../developer-guide.md)
- [Security source](https://github.com/BitBadges/bitbadgeschain/blob/master/x/tokenization/precompile/security.go)
## File: chain/modules/README.md
---
description: "Every BitBadges-specific Cosmos SDK module: x/tokenization (the token standard), gamm, poolmanager, sendmanager, managersplitter, and the IBC middleware."
---
# Chain Modules
`x/tokenization` is the token standard and has its own tab. The modules below surround it on the BitBadges chain and are what a DEX integrator, a delegated manager, or a chain developer touches next.
```bash
# Every module's REST surface hangs off the mainnet LCD
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools?pagination.limit=1
curl https://lcd.bitbadges.io/osmosis/poolmanager/v1beta1/num_pools
curl https://lcd.bitbadges.io/bitbadges/bitbadgeschain/managersplitter
curl https://lcd.bitbadges.io/bitbadges/bitbadgeschain/sendmanager/balance/bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d/badgeslp:64:utoken
```
| Module | Path in chain | What it does | Read when |
| --- | --- | --- | --- |
| [x/tokenization](../../token-standard/README.md) | `x/tokenization` | The token standard: collections, balances, approvals, permissions, wrapping | You create or move tokens. Documented in the Token Standard tab |
| [x/gamm](gamm/README.md) | `x/gamm` | Balancer and stableswap AMM pools, forked from Osmosis, with native token support through alias denoms | You trade, provide liquidity, or create pools |
| [x/poolmanager](pool-manager.md) | `x/poolmanager` | Pool ID registry, swap routing across pool types, taker fees, estimate queries | You need multi-pool routing or the canonical swap estimate endpoints |
| [x/sendmanager](send-manager.md) | `x/sendmanager` | Routes `sdk.Coins` by denom prefix to x/bank or `x/tokenization`; the send precompile uses it | You move alias denoms with bank-style calls, from Cosmos or Solidity |
| [x/managersplitter](manager-splitter/README.md) | `x/managersplitter` | A proxy manager address whose permissions are split across approved addresses | A collection needs role-based management |
| [x/custom-hooks](../../token-standard/ibc/transfer-tokens-hook.md) | `x/custom-hooks` | IBC memo hooks: `transfer_tokens` and `swap_and_action` | An inbound IBC transfer should mint, transfer, or swap |
| [x/ibc-rate-limit](ibc-rate-limit.md) | `x/ibc-rate-limit` | Per channel and denom flow caps | You hit a rate limit or draft a governance change |
The chain also ships standard Cosmos SDK modules (bank, staking, gov, authz, feegrant, distribution) and the Cosmos EVM module set. The EVM side is documented under [EVM](../evm/README.md).
## Related
- [Token Standard](../../token-standard/README.md)
- [IBC and x/bank Compatibility](../../token-standard/ibc/README.md)
- [Integrate the Module](../../token-standard/integrate/README.md)
## File: chain/modules/gamm/README.md
---
description: "The x/gamm AMM module, forked from Osmosis. How pools hold native tokens through badgeslp alias denoms and why pool trades still obey collection approvals."
---
# x/gamm
`x/gamm` (Generalized Automated Market Maker) is the DEX module on the BitBadges chain. It is a fork of Osmosis `x/gamm` with changes that let liquidity pools hold `x/tokenization` tokens and enforce each collection's transferability rules on every pool operation. Read this page before the [messages](messages.md) and [queries](queries.md) references. For a task-shaped walkthrough see [Trade on the DEX](../../../guides/trade-on-the-dex.md).
```bash
# Pool 1 on mainnet: a native token (alias denom) paired with BADGE
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools/1
```
```json
{
"pool": {
"@type": "/gamm.poolmodels.balancer.Pool",
"address": "bb19e2mf7cywkv7zaug6nk5f87d07fxrdgrladvymh2gwv5crvm3vnsy5m66z",
"id": "1",
"pool_params": { "swap_fee": "0.003000000000000000", "exit_fee": "0.000000000000000000" },
"total_shares": { "denom": "gamm/pool/1", "amount": "100000000000000000000" },
"pool_assets": [
{ "token": { "denom": "badgeslp:64:utoken", "amount": "800" }, "weight": "1073741824" },
{ "token": { "denom": "ubadge", "amount": "12523362993" }, "weight": "1073741824" }
],
"total_weight": "2147483648"
}
}
```
{% hint style="info" %}
Ask your agent:
```text
Create a 50/50 pool between badges:1:utoken and ubadge with 1,000 of each and show me the pool id.
```
The liquidity-pools skill, `bb pools`, and `bb swap` cover pool creation, joins, and swaps.
{% endhint %}
## Concepts
An AMM prices trades with a formula over pool reserves instead of an orderbook. A liquidity pool holds a set of assets and issues LP shares (`gamm/pool/`) to depositors. Each pool has:
- Pool assets: the coins it holds, with weights for balancer pools.
- Pool shares: LP tokens that represent ownership of the pool.
- Swap fee: charged on each trade.
- Exit fee: charged when exiting the pool.
Pool types: balancer pools (configurable weights) are the primary type. The stableswap pool model is also compiled in (`MsgCreateStableswapPool`, `MsgStableSwapAdjustScalingFactors`). Pool creation can be disabled per collection with the `disablePoolCreation` invariant.
## Differences from Osmosis
Interface changes:
- Removed `smoothWeightChangeParams` and other unused parameters.
- Updated type definitions to fit the BitBadges codebase.
- Removed governance proposal handling and the future pool governor.
- Removed pool creation fee requirements.
Native token integration. A pool asset can be a native token expressed as an [alias denom](../../../token-standard/ibc/alias-denoms.md), `badgeslp::`. The pool treats it as a "ghost denom": there is no mint or burn behind the scenes, and the coin is really backed by core `x/tokenization` balances. The collection's `aliasPaths` (`conversion.sideB`) define the rate between one alias unit and `Balances[]`.
```text
Token: badgeslp:21:utoken
Collection ID: 21
Base denom: utoken
Alias path conversion: [{ amount: 1n, tokenIds: [{ start: 1n, end: 1n }], ownershipTimes: UintRangeArray.FullRanges() }]
1 badgeslp:21:utoken = [{ amount: 1n, tokenIds: [{ start: 1n, end: 1n }], ownershipTimes: UintRangeArray.FullRanges() }]
2 badgeslp:21:utoken = [{ amount: 2n, tokenIds: [{ start: 1n, end: 1n }], ownershipTimes: UintRangeArray.FullRanges() }]
```
The conversion happens before and after each swap and on every join or exit that touches a native asset, so existing pool code works unchanged.
## Transferability and Compliance
A pool operation that moves a native token is a `MsgTransferTokens` under the hood. It must satisfy the collection's approvals at every level (collection, outgoing, incoming). The transfer is treated as initiated by the user and approved by the pool address.
That means the approval system gates the DEX:
- User-gated pools: restrict who may join, exit, or swap.
- Rate limiting: daily or weekly caps on pool exits.
- KYC or AML: require verification before pool participation.
- Geographic or address-based restrictions.
- Time windows: allow pool operations only at certain times.
- Any other criteria the approval system can express.
See [Approval Criteria](../../../token-standard/approval-criteria/README.md) and [Compliance Zones](../../../token-standard/concepts/compliance-zones.md).
## Reference
- [Messages](messages.md): all 12 `Msg` types with fields and examples.
- [Queries](queries.md): all 16 gRPC and LCD queries.
- [GAMM Precompile](../../evm/gamm-precompile/README.md): the same operations from Solidity at `0x0000000000000000000000000000000000001002` (`0x...1002`).
- Proto definitions: `proto/gamm/` in [bitbadgeschain](https://github.com/bitbadges/bitbadgeschain/tree/master/proto/gamm).
## Related
- [Trade on the DEX](../../../guides/trade-on-the-dex.md)
- [Alias Denoms](../../../token-standard/ibc/alias-denoms.md)
- [x/poolmanager](../pool-manager.md)
- [Swaps in the API](../../../api/swaps.md)
## File: chain/modules/gamm/messages.md
---
description: "All 12 x/gamm messages with fields and a JSON example each: pool creation, join, exit, swaps, single-asset joins and exits, stableswap, IBC swap."
---
# x/gamm Messages
Every message is signed by `sender`. Amounts are integer strings in the coin's base unit. Native tokens appear as `badgeslp::` alias denoms and are subject to the collection's approvals (see [x/gamm](README.md)). Type URLs use the proto package: `/gamm.v1beta1.Msg`, except pool creation which lives under `/gamm.poolmodels.balancer.` and `/gamm.poolmodels.stableswap.`.
| Message | Purpose |
| --- | --- |
| [MsgCreateBalancerPool](#msgcreatebalancerpool) | Create a weighted pool with initial liquidity |
| [MsgJoinPool](#msgjoinpool) | Deposit all assets proportionally for LP shares |
| [MsgExitPool](#msgexitpool) | Burn LP shares for all assets proportionally |
| [MsgSwapExactAmountIn](#msgswapexactamountin) | Swap an exact input for at least a minimum output |
| [MsgSwapExactAmountOut](#msgswapexactamountout) | Swap at most a maximum input for an exact output |
| [MsgSwapExactAmountInWithIBCTransfer](#msgswapexactamountinwithibctransfer) | Swap, then send the output over IBC |
| [MsgJoinSwapExternAmountIn](#msgjoinswapexternamountin) | Single-asset join with an exact token amount |
| [MsgJoinSwapShareAmountOut](#msgjoinswapshareamountout) | Single-asset join for an exact share amount |
| [MsgExitSwapShareAmountIn](#msgexitswapshareamountin) | Exit an exact share amount into one asset |
| [MsgExitSwapExternAmountOut](#msgexitswapexternamountout) | Exit into an exact single-asset amount |
| [MsgCreateStableswapPool](#msgcreatestableswappool) | Create a stableswap pool |
| [MsgStableSwapAdjustScalingFactors](#msgstableswapadjustscalingfactors) | Adjust a stableswap pool's scaling factors |
{% hint style="info" %}
Ask your agent:
```text
Swap 1 BADGE for badgeslp:64:utoken through pool 1 with 1% slippage and show me the transaction before you sign it.
```
The `bb swap estimate ubadge badgeslp:64:utoken 1000000 --slippage 1` command and the liquidity-pools skill produce the `MsgSwapExactAmountIn` on this page.
{% endhint %}
## MsgCreateBalancerPool
Creates a balancer pool. The pool ID is assigned at execution and returned in the response. The creator supplies the initial liquidity and sets the fees and weights; a dedicated module account is created for the pool.
```json
{
"@type": "/gamm.poolmodels.balancer.MsgCreateBalancerPool",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_params": {
"swap_fee": "0.003000000000000000",
"exit_fee": "0.000000000000000000"
},
"pool_assets": [
{
"token": { "denom": "ubadge", "amount": "1000000" },
"weight": "50"
},
{
"token": { "denom": "badgeslp:21:utoken", "amount": "5000000" },
"weight": "50"
}
]
}
```
```proto
message MsgCreateBalancerPool {
option (amino.name) = "gamm/CreateBalancerPool";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
gamm.poolmodels.balancer.PoolParams pool_params = 2;
repeated gamm.poolmodels.balancer.PoolAsset pool_assets = 3;
}
// Returns the poolID
message MsgCreateBalancerPoolResponse {
uint64 pool_id = 1;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Pool creator; funds the initial liquidity |
| `pool_params.swap_fee` | LegacyDec string | yes | Fee taken on each swap, for example `0.003` for 0.3%. Some serializers render this as an 18-decimal integer string such as `3000000000000000`. |
| `pool_params.exit_fee` | LegacyDec string | yes | Fee taken on exit |
| `pool_assets[].token` | Coin | yes | Denom and initial amount |
| `pool_assets[].weight` | Int string | yes | Relative weight of the asset |
Behavior: pool creation fails when any native asset's collection sets the `disablePoolCreation` invariant. There is no pool creation fee. The response `pool_id` is also emitted in transaction events.
## MsgJoinPool
Joins a pool by depositing every asset in the current pool ratio. LP shares are minted to the sender. Internally this is "join pool, no swap".
```json
{
"@type": "/gamm.v1beta1.MsgJoinPool",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_id": "1",
"share_out_amount": "1000000",
"token_in_maxs": [
{ "denom": "badgeslp:64:utoken", "amount": "10" },
{ "denom": "ubadge", "amount": "160000000" }
]
}
```
```proto
message MsgJoinPool {
option (amino.name) = "gamm/JoinPool";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
uint64 pool_id = 2;
string share_out_amount = 3; // math.Int
repeated cosmos.base.v1beta1.Coin token_in_maxs = 4;
}
message MsgJoinPoolResponse {
string share_out_amount = 1; // math.Int
repeated cosmos.base.v1beta1.Coin token_in = 2;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Depositor |
| `pool_id` | uint64 | yes | Target pool |
| `share_out_amount` | Int string | yes | Exact LP shares to receive |
| `token_in_maxs` | Coin[] | yes | Slippage cap: the most of each asset the join may consume |
Behavior: tokens must be provided in the pool's current proportions; otherwise the transaction fails. Shares are minted to `sender`, pool liquidity rises, and the sender starts earning swap fees. Shares can be held, staked in yield programs, or used for governance where a pool supports it.
## MsgExitPool
Burns LP shares and returns every underlying asset in proportion to the share. The sender cannot pick which assets to receive.
```json
{
"@type": "/gamm.v1beta1.MsgExitPool",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_id": "1",
"share_in_amount": "100000",
"token_out_mins": [
{ "denom": "badgeslp:64:utoken", "amount": "1" },
{ "denom": "ubadge", "amount": "15000000" }
]
}
```
```proto
message MsgExitPool {
option (amino.name) = "gamm/ExitPool";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
uint64 pool_id = 2;
string share_in_amount = 3; // math.Int
repeated cosmos.base.v1beta1.Coin token_out_mins = 4;
}
message MsgExitPoolResponse {
repeated cosmos.base.v1beta1.Coin token_out = 1;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Share holder |
| `pool_id` | uint64 | yes | Pool to exit |
| `share_in_amount` | Int string | yes | LP shares to burn |
| `token_out_mins` | Coin[] | yes | Slippage floor: the least of each asset that must come back |
Behavior: shares are burned, the exit fee (if the pool has one) is deducted, and the assets are returned proportionally. A native asset's collection approvals must allow the transfer from the pool to the sender.
## MsgSwapExactAmountIn
Swaps an exact `token_in` for at least `token_out_min_amount` of the last route's output denom.
```json
{
"@type": "/gamm.v1beta1.MsgSwapExactAmountIn",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"routes": [
{ "pool_id": "1", "token_out_denom": "badgeslp:64:utoken" }
],
"token_in": { "denom": "ubadge", "amount": "1000000000" },
"token_out_min_amount": "60",
"affiliates": [
{ "basis_points_fee": "10", "address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf" }
]
}
```
```proto
message MsgSwapExactAmountIn {
option (amino.name) = "gamm/SwapExactAmountIn";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
repeated poolmanager.v1beta1.SwapAmountInRoute routes = 2;
cosmos.base.v1beta1.Coin token_in = 3;
string token_out_min_amount = 4; // math.Int
repeated poolmanager.v1beta1.Affiliate affiliates = 5;
}
message MsgSwapExactAmountInResponse {
string token_out_amount = 1; // math.Int
}
message Affiliate {
string basis_points_fee = 1;
string address = 2;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Trader |
| `routes` | `SwapAmountInRoute[]` | yes | Ordered hops; each names a `pool_id` and `token_out_denom` |
| `token_in` | Coin | yes | Exact input |
| `token_out_min_amount` | Int string | yes | Slippage floor on the final output |
| `affiliates` | `Affiliate[]` | no | Fee recipients paid from the swap output |
Behavior: multi-hop swaps run through each pool in sequence. Each pool's swap fee is deducted from the input before the swap. Price impact depends on pool liquidity. Affiliate fees are optional, given in basis points (1 basis point = 0.01%, 100 = 1%), computed on the output, and several affiliates may be listed. With an output of 1,000,000 and one affiliate at 10 basis points, the affiliate receives 1,000 and the sender 999,000.
## MsgSwapExactAmountOut
Swaps up to `token_in_max_amount` of the first route's input denom for exactly `token_out`.
```json
{
"@type": "/gamm.v1beta1.MsgSwapExactAmountOut",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"routes": [
{ "pool_id": "1", "token_in_denom": "ubadge" }
],
"token_in_max_amount": "2000000000",
"token_out": { "denom": "badgeslp:64:utoken", "amount": "1" }
}
```
```proto
message MsgSwapExactAmountOut {
option (amino.name) = "gamm/SwapExactAmountOut";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
repeated poolmanager.v1beta1.SwapAmountOutRoute routes = 2;
string token_in_max_amount = 3; // math.Int
cosmos.base.v1beta1.Coin token_out = 4;
}
message MsgSwapExactAmountOutResponse {
string token_in_amount = 1; // math.Int
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Trader |
| `routes` | `SwapAmountOutRoute[]` | yes | Ordered hops; each names a `pool_id` and `token_in_denom` |
| `token_in_max_amount` | Int string | yes | Slippage cap on the input |
| `token_out` | Coin | yes | Exact output |
Behavior: the response reports the input actually spent. No affiliate field exists on this message. Use it when the output amount matters more than the input, for example buying exactly one token.
## MsgSwapExactAmountInWithIBCTransfer
Runs `MsgSwapExactAmountIn`, then sends the output to another chain over IBC in the same transaction. If any step fails, everything is rolled back.
```json
{
"@type": "/gamm.v1beta1.MsgSwapExactAmountInWithIBCTransfer",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"routes": [
{ "pool_id": "1", "token_out_denom": "ubadge" },
{ "pool_id": "4", "token_out_denom": "ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701" }
],
"token_in": { "denom": "badgeslp:64:utoken", "amount": "10" },
"token_out_min_amount": "900000",
"ibc_transfer_info": {
"source_channel": "channel-3",
"receiver": "cosmos1py4mfpg6uf59qkyzg0nmau322c5873ee8df8qg",
"memo": "",
"timeout_timestamp": "1788825600000000000"
},
"affiliates": [
{ "basis_points_fee": "10", "address": "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf" }
]
}
```
```ts
export interface iIBCTransferInfo {
sourceChannel: string;
receiver: string;
memo: string;
timeoutTimestamp: T;
}
export interface iMsgSwapExactAmountInWithIBCTransfer {
sender: string;
routes: iSwapAmountInRoute[];
tokenIn: iCosmosCoin;
tokenOutMinAmount: T;
ibcTransferInfo: iIBCTransferInfo;
affiliates?: iAffiliate[];
}
export interface iAffiliate {
basisPointsFee: string;
address: string;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Trader and IBC sender |
| `routes` | `SwapAmountInRoute[]` | yes | Swap hops, executed in order before the transfer |
| `token_in` | Coin | yes | Exact input |
| `token_out_min_amount` | Int string | yes | Slippage floor |
| `ibc_transfer_info.source_channel` | string | yes | IBC channel to send through |
| `ibc_transfer_info.receiver` | string | yes | Destination chain address |
| `ibc_transfer_info.memo` | string | no | Memo attached to the IBC transfer |
| `ibc_transfer_info.timeout_timestamp` | uint64 | yes | Nanosecond timestamp after which the IBC transfer times out |
| `affiliates` | `Affiliate[]` | no | Fee recipients, paid from the output before the IBC transfer |
Behavior, in order: swap through the routes, check the minimum output, deduct affiliate fees (on the output, before transfer), send the remainder over IBC. Each pool's swap fee comes off the input; the IBC transfer fee comes off the output. With an output of 1,000,000 and an affiliate at 10 basis points, 1,000 goes to the affiliate and 999,000 is transferred. The output denom must be an x/bank coin, since native `x/tokenization` balances cannot cross IBC.
## MsgJoinSwapExternAmountIn
Joins a pool with a single asset. The pool swaps part of the deposit internally so the join stays balanced. The sender specifies the exact `token_in` and a minimum share amount.
```json
{
"@type": "/gamm.v1beta1.MsgJoinSwapExternAmountIn",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_id": "1",
"token_in": { "denom": "ubadge", "amount": "1000000000" },
"share_out_min_amount": "1000000000000000"
}
```
```proto
message MsgJoinSwapExternAmountIn {
option (amino.name) = "gamm/JoinSwapExternAmountIn";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
uint64 pool_id = 2;
cosmos.base.v1beta1.Coin token_in = 3;
string share_out_min_amount = 4; // math.Int
}
message MsgJoinSwapExternAmountInResponse {
string share_out_amount = 1; // math.Int
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Depositor |
| `pool_id` | uint64 | yes | Target pool |
| `token_in` | Coin | yes | Exact single-asset deposit; its denom must be a pool asset |
| `share_out_min_amount` | Int string | yes | Slippage floor on shares received |
Behavior: the response reports the shares minted. The internal swap pays the pool's swap fee on the swapped portion.
## MsgJoinSwapShareAmountOut
Single-asset join for an exact number of shares. The pool computes how much of `token_in_denom` it needs, capped by `token_in_max_amount`.
```json
{
"@type": "/gamm.v1beta1.MsgJoinSwapShareAmountOut",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_id": "1",
"token_in_denom": "ubadge",
"share_out_amount": "1000000000000000",
"token_in_max_amount": "1200000000"
}
```
```proto
message MsgJoinSwapShareAmountOut {
option (amino.name) = "gamm/JoinSwapShareAmountOut";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
uint64 pool_id = 2;
string token_in_denom = 3;
string share_out_amount = 4; // math.Int
string token_in_max_amount = 5; // math.Int
}
message MsgJoinSwapShareAmountOutResponse {
string token_in_amount = 1; // math.Int
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Depositor |
| `pool_id` | uint64 | yes | Target pool |
| `token_in_denom` | string | yes | The one asset to deposit |
| `share_out_amount` | Int string | yes | Exact shares to receive |
| `token_in_max_amount` | Int string | yes | Slippage cap on the deposit |
Behavior: the response reports the input actually taken.
## MsgExitSwapShareAmountIn
Burns an exact number of shares and receives a single asset. The pool swaps the other assets into `token_out_denom` internally.
```json
{
"@type": "/gamm.v1beta1.MsgExitSwapShareAmountIn",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_id": "1",
"token_out_denom": "ubadge",
"share_in_amount": "1000000000000000",
"token_out_min_amount": "900000000"
}
```
```proto
message MsgExitSwapShareAmountIn {
option (amino.name) = "gamm/ExitSwapShareAmountIn";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
uint64 pool_id = 2;
string token_out_denom = 3;
string share_in_amount = 4; // math.Int
string token_out_min_amount = 5; // math.Int
}
message MsgExitSwapShareAmountInResponse {
string token_out_amount = 1; // math.Int
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Share holder |
| `pool_id` | uint64 | yes | Pool to exit |
| `token_out_denom` | string | yes | The one asset to receive |
| `share_in_amount` | Int string | yes | Exact shares to burn |
| `token_out_min_amount` | Int string | yes | Slippage floor on the output |
Behavior: the exit fee and the swap fee on the internally swapped portion both apply. The response reports the output amount.
## MsgExitSwapExternAmountOut
Receives an exact single-asset `token_out` by burning at most `share_in_max_amount` shares.
```json
{
"@type": "/gamm.v1beta1.MsgExitSwapExternAmountOut",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_id": "1",
"token_out": { "denom": "ubadge", "amount": "1000000000" },
"share_in_max_amount": "1200000000000000"
}
```
```proto
message MsgExitSwapExternAmountOut {
option (amino.name) = "gamm/ExitSwapExternAmountOut";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
uint64 pool_id = 2;
cosmos.base.v1beta1.Coin token_out = 3;
string share_in_max_amount = 4; // math.Int
}
message MsgExitSwapExternAmountOutResponse {
string share_in_amount = 1; // math.Int
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Share holder |
| `pool_id` | uint64 | yes | Pool to exit |
| `token_out` | Coin | yes | Exact single-asset output |
| `share_in_max_amount` | Int string | yes | Slippage cap on shares burned |
Behavior: the response reports the shares actually burned.
## MsgCreateStableswapPool
Creates a stableswap pool for assets that should trade near parity. Scaling factors normalize assets with different decimals.
```json
{
"@type": "/gamm.poolmodels.stableswap.MsgCreateStableswapPool",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_params": {
"swap_fee": "0.001000000000000000",
"exit_fee": "0.000000000000000000"
},
"initial_pool_liquidity": [
{ "denom": "ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8", "amount": "1000000000" },
{ "denom": "ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349", "amount": "1000000000" }
],
"scaling_factors": ["1", "1"],
"future_pool_governor": "",
"scaling_factor_controller": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
}
```
```proto
message MsgCreateStableswapPool {
option (amino.name) = "gamm/CreateStableswapPool";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
PoolParams pool_params = 2;
repeated cosmos.base.v1beta1.Coin initial_pool_liquidity = 3;
repeated uint64 scaling_factors = 4;
string future_pool_governor = 5;
string scaling_factor_controller = 6;
}
message MsgCreateStableswapPoolResponse {
uint64 pool_id = 1;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Pool creator |
| `pool_params.swap_fee` | LegacyDec string | yes | Swap fee |
| `pool_params.exit_fee` | LegacyDec string | yes | Exit fee |
| `initial_pool_liquidity` | Coin[] | yes | Initial deposit, one entry per asset |
| `scaling_factors` | uint64[] | yes | One factor per asset, in the same order as the liquidity |
| `future_pool_governor` | string | no | Retained from Osmosis; governor functionality is removed on BitBadges |
| `scaling_factor_controller` | string | no | Address allowed to send `MsgStableSwapAdjustScalingFactors` |
Behavior: the pool ID is returned in the response. The same `disablePoolCreation` invariant check applies to native assets.
## MsgStableSwapAdjustScalingFactors
Adjusts the scaling factors of a stableswap pool. The sender must be the pool's `scaling_factor_controller`.
```json
{
"@type": "/gamm.poolmodels.stableswap.MsgStableSwapAdjustScalingFactors",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"pool_id": "3",
"scaling_factors": ["1", "1000"]
}
```
```proto
message MsgStableSwapAdjustScalingFactors {
option (amino.name) = "gamm/StableSwapAdjustScalingFactors";
option (cosmos.msg.v1.signer) = "sender";
string sender = 1;
uint64 pool_id = 2;
repeated uint64 scaling_factors = 3;
}
message MsgStableSwapAdjustScalingFactorsResponse {}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | string | yes | Must equal the pool's `scaling_factor_controller` |
| `pool_id` | uint64 | yes | Stableswap pool |
| `scaling_factors` | uint64[] | yes | New factors, one per asset |
Behavior: the transaction fails with an unauthorized error when `sender` is not the controller.
## Related
- [x/gamm Queries](queries.md)
- [Trade on the DEX](../../../guides/trade-on-the-dex.md)
- [GAMM Precompile](../../evm/gamm-precompile/README.md)
- [IBC hooks](../../../token-standard/ibc/transfer-tokens-hook.md)
## File: chain/modules/gamm/queries.md
---
description: "All 16 x/gamm queries with LCD paths and live mainnet examples: pools, liquidity, shares, spot price, join and exit calculators, estimates, params."
---
# x/gamm Queries
The gamm query service exposes 16 methods. Most are reachable on the LCD under `/osmosis/gamm/v1beta1/` (the paths keep the Osmosis prefix). Several are marked deprecated in favor of [x/poolmanager](../pool-manager.md) equivalents but still work. Examples below run against mainnet.
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools/1/total_pool_liquidity
```
```json
{"liquidity":[{"denom":"badgeslp:64:utoken","amount":"800"},{"denom":"ubadge","amount":"12523362993"}]}
```
{% hint style="info" %}
Ask your agent:
```text
Show me pool 1 on mainnet: its assets, total shares, and the spot price of badgeslp:64:utoken in BADGE.
```
The `bb pools show 1` and `bb price` commands read the queries on this page.
{% endhint %}
## All Queries
| Query | LCD path | Status | Returns |
| --- | --- | --- | --- |
| `Pools` | `GET /osmosis/gamm/v1beta1/pools` | current | Paginated list of pools (`Any`, `PoolI`) |
| `NumPools` | `GET /osmosis/gamm/v1beta1/num_pools` | deprecated, use poolmanager | Pool count |
| `TotalLiquidity` | `GET /osmosis/gamm/v1beta1/total_liquidity` | current | Sum of liquidity across all pools |
| `PoolsWithFilter` | `GET /osmosis/gamm/v1beta1/filtered_pools` | current | Pools matching `min_liquidity` and `pool_type` |
| `Pool` | `GET /osmosis/gamm/v1beta1/pools/{pool_id}` | deprecated, use poolmanager | One pool |
| `PoolType` | `GET /osmosis/gamm/v1beta1/pool_type/{pool_id}` | current | `"Balancer"` or `"Stableswap"` |
| `CalcJoinPoolNoSwapShares` | gRPC only | current | Shares and tokens for a proportional join |
| `CalcJoinPoolShares` | `GET /osmosis/gamm/v1beta1/pools/{pool_id}/join_swap_exact_in` | current | Shares out and tokens out for a join |
| `CalcExitPoolCoinsFromShares` | `GET /osmosis/gamm/v1beta1/pools/{pool_id}/exit_swap_share_amount_in` | current | Coins returned for a share amount |
| `PoolParams` | `GET /osmosis/gamm/v1beta1/pools/{pool_id}/params` | current | Swap fee and exit fee |
| `TotalPoolLiquidity` | `GET /osmosis/gamm/v1beta1/pools/{pool_id}/total_pool_liquidity` | deprecated, use poolmanager | Reserves of one pool |
| `TotalShares` | `GET /osmosis/gamm/v1beta1/pools/{pool_id}/total_shares` | current | LP share supply |
| `SpotPrice` | `GET /osmosis/gamm/v1beta1/pools/{pool_id}/prices` | deprecated, use v2 or poolmanager | Price of base in quote |
| `EstimateSwapExactAmountIn` | `GET /osmosis/gamm/v1beta1/{pool_id}/estimate/swap_exact_amount_in` | deprecated, use poolmanager | Output for an exact input |
| `EstimateSwapExactAmountOut` | `GET /osmosis/gamm/v1beta1/{pool_id}/estimate/swap_exact_amount_out` | deprecated, use poolmanager | Input for an exact output |
| `Params` | `GET /osmosis/gamm/v1beta1/params` | current | Module params (currently empty) |
A v2 service adds `SpotPrice` at `GET /osmosis/gamm/v2/pools/{pool_id}/prices` with the same request fields.
{% hint style="info" %}
`CalcJoinPoolShares`, `CalcExitPoolCoinsFromShares`, and the two estimate queries take `Coin` or `Int` request fields. The LCD gateway cannot encode those as URL query parameters and returns `unsupported field type`. Call them over gRPC, or use the poolmanager estimate endpoints, which accept plain strings.
{% endhint %}
## Pools
```bash
curl "https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools?pagination.limit=1"
```
```json
{
"pools": [
{
"@type": "/gamm.poolmodels.balancer.Pool",
"address": "bb19e2mf7cywkv7zaug6nk5f87d07fxrdgrladvymh2gwv5crvm3vnsy5m66z",
"id": "1",
"pool_params": { "swap_fee": "0.003000000000000000", "exit_fee": "0.000000000000000000" },
"total_shares": { "denom": "gamm/pool/1", "amount": "100000000000000000000" },
"pool_assets": [
{ "token": { "denom": "badgeslp:64:utoken", "amount": "800" }, "weight": "1073741824" },
{ "token": { "denom": "ubadge", "amount": "12523362993" }, "weight": "1073741824" }
],
"total_weight": "2147483648"
}
],
"pagination": { "next_key": "AAAAAAAAAAI=", "total": "0" }
}
```
Request: `pagination` (`PageRequest`). Response: `pools` (`Any[]`), `pagination`.
## NumPools
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/num_pools
```
```json
{"num_pools":"7"}
```
Deprecated. Prefer `GET /osmosis/poolmanager/v1beta1/num_pools`.
## TotalLiquidity
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/total_liquidity
```
```json
{"liquidity":[{"denom":"badges:49:chaosnet","amount":"761389759510602"},{"denom":"badgeslp:64:utoken","amount":"800"},{"denom":"badgeslp:73:cubadge","amount":"6991373129902"},{"denom":"ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701","amount":"1169539"},{"denom":"ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8","amount":"4655"},{"denom":"ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518","amount":"31960810"},{"denom":"ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349","amount":"14765962"},{"denom":"ubadge","amount":"63007504431337"}]}
```
No request fields. Response: `liquidity` (`Coin[]`) summed across all pools.
## PoolsWithFilter
```bash
curl "https://lcd.bitbadges.io/osmosis/gamm/v1beta1/filtered_pools?min_liquidity=1ubadge&pool_type=Balancer"
```
| Request field | Type | Description |
| --- | --- | --- |
| `min_liquidity` | string | Coins in one comma-separated string, for example `10uatom,100uosmo` |
| `pool_type` | string | `Balancer` or `Stableswap` |
| `pagination` | `PageRequest` | Optional |
Response: `pools` (`Any[]`), `pagination`.
## Pool
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools/1
```
Response: `pool` (`Any`, a `/gamm.poolmodels.balancer.Pool` or stableswap pool). Deprecated; prefer `GET /osmosis/poolmanager/v1beta1/pools/{pool_id}`.
## PoolType
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pool_type/1
```
```json
{"pool_type":"Balancer"}
```
Errors if the pool cannot be type-cast.
## CalcJoinPoolNoSwapShares
gRPC only: `gamm.v1beta1.Query/CalcJoinPoolNoSwapShares`. Simulates a proportional join.
| Request field | Type | Description |
| --- | --- | --- |
| `pool_id` | uint64 | Pool |
| `tokens_in` | `Coin[]` | Assets you intend to deposit |
Response: `tokens_out` (`Coin[]`, the deposit actually consumed) and `shares_out` (Int).
```bash
grpcurl -d '{"pool_id":"1","tokens_in":[{"denom":"ubadge","amount":"1000000"},{"denom":"badgeslp:64:utoken","amount":"1"}]}' \
grpc.bitbadges.io:443 gamm.v1beta1.Query/CalcJoinPoolNoSwapShares
```
## CalcJoinPoolShares
Path: `GET /osmosis/gamm/v1beta1/pools/{pool_id}/join_swap_exact_in`. Request: `pool_id`, `tokens_in` (`Coin[]`). Response: `share_out_amount` (Int), `tokens_out` (`Coin[]`). Use gRPC; see the hint above.
## CalcExitPoolCoinsFromShares
Path: `GET /osmosis/gamm/v1beta1/pools/{pool_id}/exit_swap_share_amount_in`. Request: `pool_id`, `share_in_amount` (Int). Response: `tokens_out` (`Coin[]`). Use gRPC; see the hint above.
## PoolParams
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools/1/params
```
```json
{"params":{"@type":"/gamm.poolmodels.balancer.PoolParams","swap_fee":"0.003000000000000000","exit_fee":"0.000000000000000000"}}
```
## TotalPoolLiquidity
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools/1/total_pool_liquidity
```
```json
{"liquidity":[{"denom":"badgeslp:64:utoken","amount":"800"},{"denom":"ubadge","amount":"12523362993"}]}
```
Deprecated; prefer `GET /osmosis/poolmanager/v1beta1/pools/{pool_id}/total_pool_liquidity`.
## TotalShares
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools/1/total_shares
```
```json
{"total_shares":{"denom":"gamm/pool/1","amount":"100000000000000000000"}}
```
## SpotPrice
```bash
curl "https://lcd.bitbadges.io/osmosis/gamm/v1beta1/pools/1/prices?base_asset_denom=badgeslp:64:utoken"e_asset_denom=ubadge"
```
```json
{"spot_price":"0.000000063880605000"}
```
| Request field | Type | Description |
| --- | --- | --- |
| `pool_id` | uint64 | Pool |
| `base_asset_denom` | string | Denom being priced |
| `quote_asset_denom` | string | Denom the price is expressed in |
The v1beta1 response is expressed as quote per base with the pool's raw weights. The v2 endpoint returns the inverse orientation for the same pair:
```bash
curl "https://lcd.bitbadges.io/osmosis/gamm/v2/pools/1/prices?base_asset_denom=badgeslp:64:utoken"e_asset_denom=ubadge"
```
```json
{"spot_price":"15654203.741250000000000000"}
```
Check which orientation you need against a known pool before relying on either. For routing, prefer `GET /osmosis/poolmanager/v1beta1/pools/{pool_id}/prices`.
## EstimateSwapExactAmountIn
Path: `GET /osmosis/gamm/v1beta1/{pool_id}/estimate/swap_exact_amount_in`. Request: `sender`, `pool_id`, `token_in` (string such as `1000ubadge`), `routes` (`SwapAmountInRoute[]`). Response: `token_out_amount` (Int). Deprecated; the poolmanager equivalent takes plain strings:
```bash
curl "https://lcd.bitbadges.io/osmosis/poolmanager/v1beta1/1/estimate/single_pool_swap_exact_amount_in?pool_id=1&token_in=1000000000ubadge&token_out_denom=badgeslp:64:utoken"
```
## EstimateSwapExactAmountOut
Path: `GET /osmosis/gamm/v1beta1/{pool_id}/estimate/swap_exact_amount_out`. Request: `sender`, `pool_id`, `routes` (`SwapAmountOutRoute[]`), `token_out` (string). Response: `token_in_amount` (Int). Deprecated; use the poolmanager `estimate/single_pool_swap_exact_amount_out` endpoint.
## Params
```bash
curl https://lcd.bitbadges.io/osmosis/gamm/v1beta1/params
```
```json
{"params":{}}
```
## Related
- [x/gamm Messages](messages.md)
- [x/poolmanager](../pool-manager.md)
- [Swaps in the API](../../../api/swaps.md)
- [Trade on the DEX](../../../guides/trade-on-the-dex.md)
## File: chain/modules/ibc-rate-limit.md
---
description: "The x/ibc-rate-limit middleware: per channel and denom caps on supply shift, unique senders, and per-address transfers, plus the governance messages."
---
# IBC Rate Limits
`x/ibc-rate-limit` is IBC middleware that caps token flow over IBC channels so that a burst of inflows or outflows cannot destabilize supply. Governance sets the limits per channel and denom. Node operators and integrators need this page to understand why a transfer was rejected and how limits are shaped.
```json
{
"channel_id": "channel-2",
"denom": "ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349",
"supply_shift_limits": [
{ "max_amount": "1000000000000", "timeframe_type": "TIMEFRAME_TYPE_DAY", "timeframe_duration": "1" }
],
"unique_sender_limits": [
{ "max_unique_senders": "500", "timeframe_type": "TIMEFRAME_TYPE_HOUR", "timeframe_duration": "1" }
],
"address_limits": [
{ "max_transfers": "20", "max_amount": "100000000000", "timeframe_type": "TIMEFRAME_TYPE_DAY", "timeframe_duration": "1" }
]
}
```
## How It Works
The module wraps the transfer application. Its hooks:
1. Check limits before an inbound packet is processed (`OnRecvPacketOverride`).
2. Check limits before an outbound packet is sent (`SendPacketOverride`).
3. Record transfer statistics after a successful transfer.
Three kinds of limit exist:
- Supply shift: the absolute value of net flow (inflows minus outflows) over a timeframe.
- Unique senders: how many distinct addresses may send through a channel in a timeframe.
- Per address: how many transfers and how much total amount one address may send in a timeframe.
Each config targets one `channel_id` and `denom`. An empty `channel_id` applies to every channel; `denom` is required. Configs are checked in order and the first match is used. If no config matches, the transfer is allowed. Inside a matching config every listed limit is checked; if any would be exceeded, the transfer is rejected.
Timeframes:
| `timeframe_type` | `timeframe_duration` means |
| --- | --- |
| `TIMEFRAME_TYPE_BLOCK` | number of blocks |
| `TIMEFRAME_TYPE_HOUR` | hours, converted to blocks using block time |
| `TIMEFRAME_TYPE_DAY` | days, converted to blocks using block time |
A `max_amount`, `max_unique_senders`, or `max_transfers` of `0` disables that particular limit.
## Params
```proto
message Params {
// Configurations are checked in order, and the first matching config is used
// If no config matches, the transfer is allowed (no rate limit)
repeated RateLimitConfig rate_limits = 1;
}
message RateLimitConfig {
string channel_id = 1; // empty = all channels
string denom = 2; // required
repeated TimeframeLimit supply_shift_limits = 5;
repeated UniqueSenderLimit unique_sender_limits = 6;
repeated AddressLimit address_limits = 7;
}
message TimeframeLimit {
string max_amount = 1; // math.Int as string; 0 disables
TimeframeType timeframe_type = 2;
int64 timeframe_duration = 3;
}
message UniqueSenderLimit {
int64 max_unique_senders = 1; // 0 disables
TimeframeType timeframe_type = 2;
int64 timeframe_duration = 3;
}
message AddressLimit {
int64 max_transfers = 1; // 0 disables
string max_amount = 2; // 0 disables
TimeframeType timeframe_type = 3;
int64 timeframe_duration = 4;
}
```
Tracked state per channel: `ChannelFlow.net_flow` (positive means more inflow than outflow), `ChannelFlowWindow` (`window_start` height and `window_duration` in blocks), `UniqueSenders.senders`, and `AddressTransferData` (`transfer_count`, `total_amount`).
## Messages
Both messages are signed by the module authority, which defaults to the `x/gov` module account (`bb10d07y265gmmuvt4z0w9aw880jnsr700jelmk2z` on mainnet). Submit them inside a governance proposal.
### MsgUpdateRateLimit
Adds or replaces one config. If a config with the same `channel_id` and `denom` exists, it is updated in place; otherwise the config is appended.
```json
{
"@type": "/ibcratelimit.MsgUpdateRateLimit",
"authority": "bb10d07y265gmmuvt4z0w9aw880jnsr700jelmk2z",
"rate_limit": {
"channel_id": "channel-40",
"denom": "ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8",
"supply_shift_limits": [
{ "max_amount": "5000000000000", "timeframe_type": "TIMEFRAME_TYPE_DAY", "timeframe_duration": "1" }
],
"unique_sender_limits": [],
"address_limits": []
}
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `authority` | string | yes | Module authority (governance account) |
| `rate_limit` | `RateLimitConfig` | yes | Config to add or update |
### MsgUpdateParams
Replaces the whole parameter set. All parameters must be supplied.
```json
{
"@type": "/ibcratelimit.MsgUpdateParams",
"authority": "bb10d07y265gmmuvt4z0w9aw880jnsr700jelmk2z",
"params": {
"rate_limits": [
{
"channel_id": "",
"denom": "ubadge",
"supply_shift_limits": [
{ "max_amount": "100000000000000", "timeframe_type": "TIMEFRAME_TYPE_DAY", "timeframe_duration": "1" }
],
"unique_sender_limits": [],
"address_limits": []
}
]
}
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `authority` | string | yes | Module authority (governance account) |
| `params.rate_limits` | `RateLimitConfig[]` | yes | Full ordered list of configs |
The module has no query service. Read the live configuration from the module's genesis export or from the governance proposal that set it.
## Related
- [IBC and x/bank Compatibility](../../token-standard/ibc/README.md)
- [Supported Denoms](../supported-denoms.md)
- [Cosmos Coin Wrapper Paths](../../token-standard/ibc/cosmos-coin-wrapper-paths.md)
## File: chain/modules/manager-splitter/README.md
---
description: "x/managersplitter: a module-derived proxy address that acts as collection manager while each permission is delegated to approved addresses."
---
# x/managersplitter
A manager splitter is a permissioned proxy for collection management. Its module-derived address is set as the collection's manager, and an admin decides which addresses may exercise which manager permission. Use it when one address should be able to update metadata but not delete the collection, or when several teams share management of one collection.
```json
{
"@type": "/managersplitter.MsgCreateManagerSplitter",
"admin": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"permissions": {
"canUpdateCollectionMetadata": { "approvedAddresses": ["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"] },
"canUpdateTokenMetadata": { "approvedAddresses": ["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue", "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf"] },
"canUpdateCollectionApprovals": { "approvedAddresses": ["bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf"] }
}
}
```
The response returns the splitter's `address` (`bb139jr5akhnvum2t2qgg3tmku9ty6a3lxey425sz8auwgzn5j2u5rsyxffly` for splitter ID 1). Set that address as the collection's `manager`, and from then on route every collection update through `MsgExecuteUniversalUpdateCollection`.
## Shape
| Field | Type | Description |
| --- | --- | --- |
| `address` | string | Module address derived from the module name and the splitter ID (`ModuleAddress(ModuleName, ID_bytes)`). Deterministic. |
| `admin` | string | Permanent admin. Full control, always passes every permission check, cannot be changed after creation. |
| `permissions` | `ManagerSplitterPermissions` | One `PermissionCriteria` per collection permission |
```proto
message ManagerSplitter {
string address = 1;
string admin = 2;
ManagerSplitterPermissions permissions = 3;
}
message PermissionCriteria {
repeated string approvedAddresses = 1;
}
message ManagerSplitterPermissions {
PermissionCriteria canDeleteCollection = 1;
PermissionCriteria canArchiveCollection = 2;
PermissionCriteria canUpdateStandards = 3;
PermissionCriteria canUpdateCustomData = 4;
PermissionCriteria canUpdateManager = 5;
PermissionCriteria canUpdateCollectionMetadata = 6;
PermissionCriteria canUpdateValidTokenIds = 7;
PermissionCriteria canUpdateTokenMetadata = 8;
PermissionCriteria canUpdateCollectionApprovals = 9;
PermissionCriteria canAddMoreAliasPaths = 10;
PermissionCriteria canAddMoreCosmosCoinWrapperPaths = 11;
}
```
The permission set mirrors the collection's `CollectionPermissions` (see [Permissions](../../../token-standard/concepts/permissions.md)). Each entry maps a permission to the addresses allowed to exercise it. A permission with no criteria is denied to everyone except the admin. The only supported criteria type today is an approved-address list.
## How It Works
1. The admin creates a splitter and receives its address.
2. The admin (or the current manager) sets that address as the collection manager with `MsgSetManager` or `MsgUniversalUpdateCollection`.
3. An approved address submits `MsgExecuteUniversalUpdateCollection` with the inner `MsgUniversalUpdateCollection`. The module checks one permission per update flag that is set, then executes the inner message with the splitter address as creator.
4. The admin adjusts approved addresses with `MsgUpdateManagerSplitter` and can delete the splitter with `MsgDeleteManagerSplitter`.
Rules:
- Only the admin may create, update, or delete a splitter.
- Every action checks permissions before execution; checks and execution are atomic.
- `updateCollectionPermissions` is admin-only and can never be delegated.
- All addresses are validated as Bech32 before use.
- One admin per splitter, immutable.
- Only `MsgUniversalUpdateCollection` can be executed through a splitter.
- No permission inheritance: each permission is granted separately.
Use cases: multi-party management, role-based access, delegating a single task (for example, only deletion) to a trusted address, and governance-controlled collections.
## Alternatives
A splitter is one option beyond a single manager address. Others: the Cosmos SDK `x/group` module (multisigs, DAOs, voting), external DAO tooling, or a custom EVM contract as the manager.
## Reference
- [Messages and queries](messages.md): the five `Msg` types and three queries.
- Proto: `proto/managersplitter/` in [bitbadgeschain](https://github.com/bitbadges/bitbadgeschain/tree/master/proto/managersplitter).
## Related
- [Permissions](../../../token-standard/concepts/permissions.md)
- [MsgUniversalUpdateCollection](../../../token-standard/messages/msg-universal-update-collection.md)
- [MsgSetManager](../../../token-standard/messages/msg-set-manager.md)
## File: chain/modules/manager-splitter/messages.md
---
description: "x/managersplitter messages (create, update, delete, execute, update params) and the three queries, with fields, permission checks, and JSON examples."
---
# x/managersplitter Messages and Queries
Four messages are signed by the splitter's admin or an approved executor; `MsgUpdateParams` is signed by the governance authority. Type URLs use the `managersplitter` proto package, for example `/managersplitter.MsgCreateManagerSplitter`.
| Message | Signer | Purpose |
| --- | --- | --- |
| [MsgCreateManagerSplitter](#msgcreatemanagersplitter) | `admin` | Create a splitter and get its address |
| [MsgUpdateManagerSplitter](#msgupdatemanagersplitter) | `admin` | Replace the permission set |
| [MsgDeleteManagerSplitter](#msgdeletemanagersplitter) | `admin` | Delete a splitter |
| [MsgExecuteUniversalUpdateCollection](#msgexecuteuniversalupdatecollection) | `executor` | Run a collection update through the splitter |
| [MsgUpdateParams](#msgupdateparams) | `authority` | Governance: set module params |
## MsgCreateManagerSplitter
Creates a splitter with the given permissions and returns its module-derived address.
```json
{
"@type": "/managersplitter.MsgCreateManagerSplitter",
"admin": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"permissions": {
"canUpdateCollectionMetadata": {
"approvedAddresses": ["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"]
},
"canUpdateTokenMetadata": {
"approvedAddresses": ["bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf"]
},
"canUpdateValidTokenIds": {
"approvedAddresses": []
}
}
}
```
```proto
message MsgCreateManagerSplitter {
option (cosmos.msg.v1.signer) = "admin";
option (amino.name) = "managersplitter/CreateManagerSplitter";
string admin = 1;
ManagerSplitterPermissions permissions = 2;
}
message MsgCreateManagerSplitterResponse {
string address = 1;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `admin` | string | yes | Permanent admin. Full control, can always execute every permission, can update and delete the splitter, cannot be changed. Valid Bech32. |
| `permissions` | `ManagerSplitterPermissions` | no | Per-permission approved address lists. Nil creates an empty set: everything denied except for the admin. |
Each `PermissionCriteria.approvedAddresses` is a Bech32 list. An empty list means only the admin can execute that permission.
Behavior:
1. Validate that `admin` is a valid Bech32 address.
2. Check that the derived address does not already exist (a safety check).
3. Store the splitter with the next available ID, the admin, and the permissions (or empty).
4. Increment the next splitter ID.
Response `address` is deterministic from the ID, derived as `address.Module("managersplitter", []byte(id.String()))`, and is the value to set as a collection manager. Splitter ID 1 is `bb139jr5akhnvum2t2qgg3tmku9ty6a3lxey425sz8auwgzn5j2u5rsyxffly`.
## MsgUpdateManagerSplitter
Replaces the whole permission set. Only the admin can send it.
```json
{
"@type": "/managersplitter.MsgUpdateManagerSplitter",
"admin": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"address": "bb139jr5akhnvum2t2qgg3tmku9ty6a3lxey425sz8auwgzn5j2u5rsyxffly",
"permissions": {
"canUpdateCollectionMetadata": {
"approvedAddresses": ["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue", "bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf"]
},
"canUpdateTokenMetadata": {
"approvedAddresses": ["bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf"]
}
}
}
```
```proto
message MsgUpdateManagerSplitter {
option (cosmos.msg.v1.signer) = "admin";
option (amino.name) = "managersplitter/UpdateManagerSplitter";
string admin = 1;
string address = 2;
ManagerSplitterPermissions permissions = 3;
}
message MsgUpdateManagerSplitterResponse {}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `admin` | string | yes | Must match the stored admin |
| `address` | string | yes | Splitter address returned at creation |
| `permissions` | `ManagerSplitterPermissions` | yes | New full permission set |
Behavior: the update is a full replacement. Any permission not present in the new set is removed and therefore denied by default. A mismatched `admin` fails with an unauthorized error. The response is empty on success.
## MsgDeleteManagerSplitter
Deletes a splitter. Only the admin can send it.
```json
{
"@type": "/managersplitter.MsgDeleteManagerSplitter",
"admin": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"address": "bb139jr5akhnvum2t2qgg3tmku9ty6a3lxey425sz8auwgzn5j2u5rsyxffly"
}
```
```proto
message MsgDeleteManagerSplitter {
option (cosmos.msg.v1.signer) = "admin";
option (amino.name) = "managersplitter/DeleteManagerSplitter";
string admin = 1;
string address = 2;
}
message MsgDeleteManagerSplitterResponse {}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `admin` | string | yes | Must match the stored admin |
| `address` | string | yes | Splitter to delete |
Behavior: a mismatched `admin` fails with an unauthorized error. Deleting a splitter that is still set as a collection manager leaves that collection with a manager address nobody can act through; update the collection manager first.
## MsgExecuteUniversalUpdateCollection
Runs a `MsgUniversalUpdateCollection` through the splitter. The module checks every permission the inner message needs, then executes it with the splitter address as creator and manager.
```json
{
"@type": "/managersplitter.MsgExecuteUniversalUpdateCollection",
"executor": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"managerSplitterAddress": "bb139jr5akhnvum2t2qgg3tmku9ty6a3lxey425sz8auwgzn5j2u5rsyxffly",
"universalUpdateCollectionMsg": {
"creator": "bb139jr5akhnvum2t2qgg3tmku9ty6a3lxey425sz8auwgzn5j2u5rsyxffly",
"collectionId": "1",
"updateCollectionMetadata": true,
"collectionMetadata": { "uri": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json", "customData": "" }
}
}
```
```proto
message MsgExecuteUniversalUpdateCollection {
option (cosmos.msg.v1.signer) = "executor";
option (amino.name) = "managersplitter/ExecuteUniversalUpdateCollection";
string executor = 1;
string managerSplitterAddress = 2;
tokenization.MsgUniversalUpdateCollection universalUpdateCollectionMsg = 3;
}
message MsgExecuteUniversalUpdateCollectionResponse {
string collectionId = 1;
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `executor` | string | yes | The admin, or an address approved for every permission the update needs |
| `managerSplitterAddress` | string | yes | Splitter to execute through |
| `universalUpdateCollectionMsg` | `MsgUniversalUpdateCollection` | yes | The update. Set only the fields you may update; leave every other `update*` flag `false`. |
Permission checks, by update flag on the inner message:
| Inner field | Permission required |
| --- | --- |
| `updateValidTokenIds` | `canUpdateValidTokenIds` |
| `updateCollectionPermissions` | admin only, cannot be delegated |
| `updateManager` | `canUpdateManager` |
| `updateCollectionMetadata` | `canUpdateCollectionMetadata` |
| `updateTokenMetadata` | `canUpdateTokenMetadata` |
| `updateCustomData` | `canUpdateCustomData` |
| `updateCollectionApprovals` | `canUpdateCollectionApprovals` |
| `updateStandards` | `canUpdateStandards` |
| `updateIsArchived` | `canArchiveCollection` |
| `cosmosCoinWrapperPathsToAdd` (non-empty) | `canAddMoreCosmosCoinWrapperPaths` |
| `aliasPathsToAdd` (non-empty) | `canAddMoreAliasPaths` |
Behavior:
1. Validate `executor` and `managerSplitterAddress` as Bech32.
2. Load the splitter; fail if it does not exist.
3. For each flag above, check that the executor is the admin or is in that permission's `approvedAddresses`. Any missing permission fails the whole transaction with a permission denied error.
4. Validate the inner message and execute it through `x/tokenization` with the splitter address as creator.
5. Return the updated `collectionId`.
The collection's own `collectionPermissions` still apply after the splitter check; a permanently forbidden action fails in `x/tokenization` regardless of splitter approval.
## MsgUpdateParams
Governance message that sets module params. The `Params` message currently has no fields, so this exists for forward compatibility.
```json
{
"@type": "/managersplitter.MsgUpdateParams",
"authority": "bb10d07y265gmmuvt4z0w9aw880jnsr700jelmk2z",
"params": {}
}
```
```proto
message MsgUpdateParams {
option (cosmos.msg.v1.signer) = "authority";
option (amino.name) = "managersplitter/MsgUpdateParams";
string authority = 1;
Params params = 2; // all parameters must be supplied
}
message MsgUpdateParamsResponse {}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `authority` | string | yes | Module authority; defaults to the `x/gov` module account |
| `params` | `Params` | yes | Full parameter set (currently empty) |
## Queries
| Query | LCD path | Returns |
| --- | --- | --- |
| `Params` | `GET /bitbadges/bitbadgeschain/managersplitter/params` | Module params (empty object) |
| `ManagerSplitter` | `GET /bitbadges/bitbadgeschain/managersplitter/{address}` | One splitter by address |
| `AllManagerSplitters` | `GET /bitbadges/bitbadgeschain/managersplitter` | Paginated list |
```bash
curl "https://lcd.bitbadges.io/bitbadges/bitbadgeschain/managersplitter?pagination.limit=10"
```
```json
{"managerSplitters":[],"pagination":{"next_key":null,"total":"0"}}
```
```bash
curl https://lcd.bitbadges.io/bitbadges/bitbadgeschain/managersplitter/bb139jr5akhnvum2t2qgg3tmku9ty6a3lxey425sz8auwgzn5j2u5rsyxffly
```
```json
{
"managerSplitter": {
"address": "bb139jr5akhnvum2t2qgg3tmku9ty6a3lxey425sz8auwgzn5j2u5rsyxffly",
"admin": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"permissions": {
"canUpdateCollectionMetadata": { "approvedAddresses": ["bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"] }
}
}
}
```
`AllManagerSplitters` takes `pagination` (`PageRequest`) and returns `managerSplitters` plus `pagination`. `ManagerSplitter` takes `address`.
## Related
- [x/managersplitter](README.md)
- [MsgUniversalUpdateCollection](../../../token-standard/messages/msg-universal-update-collection.md)
- [Permissions](../../../token-standard/concepts/permissions.md)
## File: chain/modules/pool-manager.md
---
description: "x/poolmanager: pool ID registry, cross-pool swap routing, split routes, taker fees, and the estimate endpoints that replace the deprecated gamm ones."
---
# x/poolmanager
`x/poolmanager` sits above [x/gamm](gamm/README.md). It owns the pool ID counter, maps each pool ID to the module that implements it, routes swaps across pools, and charges the taker fee. Most integrators only need its query endpoints for estimates and prices; traders can send its swap messages instead of the gamm ones.
```bash
curl https://lcd.bitbadges.io/osmosis/poolmanager/v1beta1/num_pools
```
```json
{"num_pools":"7"}
```
## Params
```bash
curl https://lcd.bitbadges.io/osmosis/poolmanager/v1beta1/Params
```
```json
{
"params": {
"taker_fee_params": {
"default_taker_fee": "0.001000000000000000",
"osmo_taker_fee_distribution": { "staking_rewards": "1.000000000000000000", "community_pool": "0.000000000000000000" },
"non_osmo_taker_fee_distribution": { "staking_rewards": "0.670000000000000000", "community_pool": "0.330000000000000000" },
"admin_addresses": [],
"community_pool_denom_to_swap_non_whitelisted_assets_to": "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858",
"reduced_fee_whitelist": [],
"community_pool_denom_whitelist": []
}
}
}
```
The taker fee (0.1% by default on mainnet) is charged on top of each pool's own swap fee and split between staking rewards and the community pool. Field names keep Osmosis spelling (`osmo_taker_fee_distribution` applies to the native BADGE denom).
## Messages
All signed by `sender`. Type URLs are `/poolmanager.v1beta1.Msg`.
| Message | Purpose |
| --- | --- |
| `MsgSwapExactAmountIn` | Same fields as the gamm message (`routes`, `token_in`, `token_out_min_amount`) but without `affiliates`; routes across any pool type |
| `MsgSwapExactAmountOut` | Same fields as the gamm message (`routes`, `token_in_max_amount`, `token_out`) |
| `MsgSplitRouteSwapExactAmountIn` | Splits one input across several routes: `routes[]` of `{ pools[], token_in_amount }`, plus `token_in_denom`, `token_out_min_amount` |
| `MsgSplitRouteSwapExactAmountOut` | Split-route variant for an exact output: `routes[]` of `{ pools[], token_out_amount }`, plus `token_out_denom`, `token_in_max_amount` |
| `MsgSetDenomPairTakerFee` | Sets a custom taker fee for denom pairs; sender must be in `admin_addresses` |
| `MsgSetTakerFeeShareAgreementForDenom` | Sets a `skim_percent` and `skim_address` for a denom's taker fee share; admin only |
```json
{
"@type": "/poolmanager.v1beta1.MsgSplitRouteSwapExactAmountIn",
"sender": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"routes": [
{ "pools": [{ "pool_id": "1", "token_out_denom": "badgeslp:64:utoken" }], "token_in_amount": "600000000" },
{ "pools": [{ "pool_id": "4", "token_out_denom": "badgeslp:64:utoken" }], "token_in_amount": "400000000" }
],
"token_in_denom": "ubadge",
"token_out_min_amount": "10"
}
```
Swaps that touch a native token still run through `MsgTransferTokens` and the collection's approvals, exactly as in x/gamm.
{% hint style="info" %}
Ask your agent:
```text
Estimate how much badgeslp:64:utoken I get for 1 BADGE and split the route across pools 1 and 4 if that is cheaper.
```
The `bb swap estimate ubadge badgeslp:64:utoken 1000000000` command uses the poolmanager estimate endpoints below.
{% endhint %}
## Queries
| Query | LCD path |
| --- | --- |
| `Params` | `GET /osmosis/poolmanager/v1beta1/Params` |
| `NumPools` | `GET /osmosis/poolmanager/v1beta1/num_pools` |
| `Pool` | `GET /osmosis/poolmanager/v1beta1/pools/{pool_id}` |
| `AllPools` | `GET /osmosis/poolmanager/v1beta1/all-pools` |
| `ListPoolsByDenom` | `GET /osmosis/poolmanager/v1beta1/list-pools-by-denom?denom=...` |
| `SpotPrice` | `GET /osmosis/poolmanager/pools/{pool_id}/prices?base_asset_denom=..."e_asset_denom=...` |
| `TotalPoolLiquidity` | `GET /osmosis/poolmanager/v1beta1/pools/{pool_id}/total_pool_liquidity` |
| `TotalLiquidity` | `GET /osmosis/poolmanager/v1beta1/total_liquidity` |
| `TotalVolumeForPool` | `GET /osmosis/poolmanager/v1beta1/pools/{pool_id}/total_volume` |
| `EstimateSwapExactAmountIn` | `GET /osmosis/poolmanager/v1beta1/{pool_id}/estimate/swap_exact_amount_in?token_in=...&routes=...` |
| `EstimateSinglePoolSwapExactAmountIn` | `GET /osmosis/poolmanager/v1beta1/{pool_id}/estimate/single_pool_swap_exact_amount_in?pool_id=...&token_in=...&token_out_denom=...` |
| `EstimateSwapExactAmountOut` | `GET /osmosis/poolmanager/v1beta1/{pool_id}/estimate/swap_exact_amount_out?token_out=...&routes=...` |
| `EstimateSinglePoolSwapExactAmountOut` | `GET /osmosis/poolmanager/v1beta1/{pool_id}/estimate/single_pool_swap_exact_amount_out?pool_id=...&token_out=...&token_in_denom=...` |
| `EstimateTradeBasedOnPriceImpact` | `GET /osmosis/poolmanager/v1beta1/{pool_id}/estimate_trade` |
| `TradingPairTakerFee` | `GET /osmosis/poolmanager/v1beta1/trading_pair_takerfee?denom_0=...&denom_1=...` |
| `AllTakerFeeShareAgreements`, `TakerFeeShareAgreementFromDenom`, `TakerFeeShareDenomsToAccruedValue`, `AllTakerFeeShareAccumulators` | taker fee share bookkeeping under `/osmosis/poolmanager/v1beta1/` |
| `RegisteredAlloyedPoolFromDenom`, `RegisteredAlloyedPoolFromPoolId`, `AllRegisteredAlloyedPools` | alloyed pool registry (inherited from Osmosis, unused on BitBadges) |
| `SpotPriceV2` | `GET /osmosis/poolmanager/v2/pools/{pool_id}/prices` |
The `EstimateSinglePoolSwap*` and `*WithPrimitiveTypes` variants take plain string parameters and work over the LCD, unlike the deprecated gamm calculators.
```bash
curl "https://lcd.bitbadges.io/osmosis/poolmanager/v1beta1/1/estimate/single_pool_swap_exact_amount_in?pool_id=1&token_in=1000000000ubadge&token_out_denom=badgeslp:64:utoken"
```
```json
{"token_out_amount":"58"}
```
An input too small to produce one unit of output returns `calculated token out amount must be positive`.
## Related
- [x/gamm](gamm/README.md)
- [x/gamm Queries](gamm/queries.md)
- [Swaps in the API](../../api/swaps.md)
- [Trade on the DEX](../../guides/trade-on-the-dex.md)
## File: chain/modules/send-manager.md
---
description: "x/sendmanager routes sdk.Coins by denom prefix to x/bank or x/tokenization. MsgSendWithAliasRouting, balance query, keeper API, send precompile."
---
# x/sendmanager
`x/sendmanager` is a bank-shaped send layer that understands [Alias Denoms](../../token-standard/ibc/alias-denoms.md). It inspects each coin's denom prefix: `badgeslp:` routes to `x/tokenization`, anything else routes to `x/bank`. One message or keeper call can therefore carry a native token and a standard coin side by side. The [Send Manager Precompile](../evm/send-manager-precompile.md) at `0x0000000000000000000000000000000000001003` (`0x...1003`) exposes the same message to Solidity.
```json
{
"@type": "/sendmanager.MsgSendWithAliasRouting",
"from_address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"to_address": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"amount": [
{ "denom": "badgeslp:64:utoken", "amount": "5" },
{ "denom": "ubadge", "amount": "1000000000" }
]
}
```
## Messages
### MsgSendWithAliasRouting
Mirrors `cosmos.bank.MsgSend`, signed by `from_address`.
```proto
message MsgSendWithAliasRouting {
option (cosmos.msg.v1.signer) = "from_address";
option (amino.name) = "bitbadgeschain/x/sendmanager/MsgSendWithAliasRouting";
string from_address = 1;
string to_address = 2;
repeated cosmos.base.v1beta1.Coin amount = 3;
}
message MsgSendWithAliasRoutingResponse {}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `from_address` | string | yes | Sender and signer |
| `to_address` | string | yes | Recipient |
| `amount` | Coin[] | yes | Any mix of standard denoms and `badgeslp:` alias denoms |
Behavior:
- For a `badgeslp::` coin, the integer amount converts to `Balances[]` through the collection's alias path and a `MsgTransferTokens` runs in auto-scan mode (no prioritized approvals). Collection, outgoing, and incoming approvals all apply.
- For any other denom, the coin moves through `x/bank`.
- The module does not set user-level approvals for you. If the recipient rejects incoming transfers, the alias leg fails.
### MsgUpdateParams
Governance-only. `Params` has no fields today.
```json
{
"@type": "/sendmanager.MsgUpdateParams",
"authority": "bb10d07y265gmmuvt4z0w9aw880jnsr700jelmk2z",
"params": {}
}
```
```proto
message MsgUpdateParams {
option (cosmos.msg.v1.signer) = "authority";
option (amino.name) = "bitbadgeschain/x/sendmanager/MsgUpdateParams";
string authority = 1;
Params params = 2;
}
message MsgUpdateParamsResponse {}
```
## Queries
| Query | LCD path | Returns |
| --- | --- | --- |
| `Params` | `GET /bitbadges/bitbadgeschain/sendmanager/params` | Module params |
| `Balance` | `GET /bitbadges/bitbadgeschain/sendmanager/balance/{address}/{denom}` | One coin balance with alias routing |
`Balance` is the useful one. For an alias denom it reports how many alias units the address could send, derived from its native balances and the alias path.
```bash
curl https://lcd.bitbadges.io/bitbadges/bitbadgeschain/sendmanager/balance/bb19e2mf7cywkv7zaug6nk5f87d07fxrdgrladvymh2gwv5crvm3vnsy5m66z/badgeslp:64:utoken
```
```json
{"balance":{"denom":"badgeslp:64:utoken","amount":"800"}}
```
```bash
curl https://lcd.bitbadges.io/bitbadges/bitbadgeschain/sendmanager/balance/bb1kj9kt5y64n5a8677fhjqnmcc24ht2vy9atmdls/ubadge
```
```json
{"balance":{"denom":"ubadge","amount":"101000000000"}}
```
On mainnet the `params` route currently returns `Not Implemented` (gRPC code 12); the `balance` route works.
## Keeper API
Chain developers use the keeper as a drop-in replacement for `BankKeeper` sends. Full details and the routing internals are on [Support Multiple Standards](../../token-standard/integrate/multiple-standards.md).
```go
type SendManagerKeeper interface {
SendCoinWithAliasRouting(ctx sdk.Context, fromAddressAcc sdk.AccAddress, toAddressAcc sdk.AccAddress, coin *sdk.Coin) error
SendCoinsWithAliasRouting(ctx sdk.Context, fromAddressAcc sdk.AccAddress, toAddressAcc sdk.AccAddress, coins sdk.Coins) error
FundCommunityPoolWithAliasRouting(ctx sdk.Context, fromAddressAcc sdk.AccAddress, coins sdk.Coins) error
SpendFromCommunityPoolWithAliasRouting(ctx sdk.Context, toAddressAcc sdk.AccAddress, coins sdk.Coins) error
SendCoinsFromModuleToAccountWithAliasRouting(ctx sdk.Context, moduleName string, toAddressAcc sdk.AccAddress, coins sdk.Coins) error
SendCoinsFromAccountToModuleWithAliasRouting(ctx sdk.Context, fromAddressAcc sdk.AccAddress, moduleName string, coins sdk.Coins) error
GetBalanceWithAliasRouting(ctx sdk.Context, address sdk.AccAddress, denom string) (sdk.Coin, error)
}
```
The keeper also exposes `IsICS20Compatible(ctx, denom)` and `StandardName(ctx, denom)` helpers and `GetRegisteredPrefixes()`. Modules plug in by implementing `types.AliasDenomRouter` for a prefix; `x/tokenization` registers `badgeslp:`.
## Precompile
From Solidity, call `send(string msgJson) returns (bool success)` on `0x0000000000000000000000000000000000001003` with the JSON above as `msgJson`. The precompile signs as the calling EVM account's bech32 address, and overwrites `from_address` with that address. See [Send Manager Precompile](../evm/send-manager-precompile.md).
## Related
- [Alias Denoms](../../token-standard/ibc/alias-denoms.md)
- [Support Multiple Standards](../../token-standard/integrate/multiple-standards.md)
- [Send Manager Precompile](../evm/send-manager-precompile.md)
- [MsgTransferTokens](../../token-standard/messages/msg-transfer-tokens.md)
## File: chain/run-a-node.md
---
description: "Start a BitBadges mainnet full node or validator: init, genesis, peers, timeout_commit, EVM chain ID, sync check, snapshots, Cosmovisor, JSON-RPC settings."
---
# Run a Node
This page brings up a BitBadges mainnet full node or validator, one copyable step at a time. The daemon binary is `bitbadgeschaind`; the `bb` developer CLI does not run a node. If you already run Cosmos SDK chains, the flow is the standard one. For help, ask in the `#validators` channel of the [Discord](https://discord.com/invite/TJMaEd9bar) and ping `@trevormil` for the Validator role.
```bash
bitbadgeschaind init alice --chain-id bitbadges-1
curl -L https://raw.githubusercontent.com/BitBadges/bitbadgeschain/master/genesis-711316.json \
-o ~/.bitbadgeschain/config/genesis.json
sed -i 's/^timeout_commit = "5s"/timeout_commit = "2s"/' ~/.bitbadgeschain/config/config.toml
bitbadgeschaind start
```
{% hint style="warning" %}
A validator is responsible for the security and uptime of the network. Use normal production precautions (firewalls, monitoring, sentry nodes, key management) to avoid slashing or losing staked funds.
{% endhint %}
## 1. Install the Binary
Download a release from [GitHub releases](https://github.com/BitBadges/bitbadgeschain/releases) or build from source, then confirm the binary is on your `PATH`:
```bash
bitbadgeschaind version
```
## 2. Initialize the Node
Pick a moniker (the public name of your node). `init` creates `~/.bitbadgeschain/` with a default `config/config.toml`, `config/app.toml`, and a placeholder `config/genesis.json`.
```bash
bitbadgeschaind init alice --chain-id bitbadges-1
```
## 3. Download the Canonical Genesis
Replace the placeholder with the pinned mainnet genesis (post-711316 hard fork):
```bash
curl -L https://raw.githubusercontent.com/BitBadges/bitbadgeschain/master/genesis-711316.json \
-o ~/.bitbadgeschain/config/genesis.json
```
Check that the file is non-empty and parses as JSON before you continue.
## 4. Configure Peers
Set `persistent_peers` (and or `seeds`) in `config.toml` to known-good mainnet nodes. One working peer is enough to discover the rest.
```toml
# ~/.bitbadgeschain/config/config.toml
persistent_peers = "2703c1304a70186372aa726a762d60da94c29ffe@134.122.12.165:26656,9b9dee928a174bcd0272be9127f5f455d418d6b2@bitbadges_mainnet_peer.chain.whenmoonwhenlambo.money:30001"
seeds = "ade4d8bc8cbe014af6ebdf3cb7b1e9ad36f412c0@seeds.polkachu.com:32956"
```
The values above come from the [chain registry](https://github.com/cosmos/chain-registry/blob/master/bitbadges/chain.json) `peers` section as of 2026-09-06 (a BitBadges node and a WhenMoonWhenLambo node, plus the Polkachu seed). Active validators publish their own peer IDs in their guides (see the community guides below); ask in `#validators` if none of these connect.
## 5. Set timeout_commit
`timeout_commit` must match the network or the node falls out of sync. `init` writes `5s`; mainnet uses `2s`.
```toml
# ~/.bitbadgeschain/config/config.toml
timeout_commit = "2s"
```
Non-interactively:
```bash
sed -i 's/^timeout_commit = "5s"/timeout_commit = "2s"/' \
~/.bitbadgeschain/config/config.toml
```
## 6. Set the EVM Chain ID
`evm-chain-id` in `app.toml` must match the network: `50024` on mainnet, `50025` on testnet. The default after `init` (`90123`) is wrong for both.
```toml
[evm]
evm-chain-id = 50024
```
With the wrong value, wallets such as MetaMask reject transactions with `incorrect chain-id`. See [EVM JSON-RPC configuration](#evm-json-rpc-configuration) for the rest of the EVM settings.
## 7. Start the Node
```bash
bitbadgeschaind start
```
Block heights should tick up within a minute or two once peers connect. If the node stays at height `0` or logs `No addresses added` for more than a few minutes, the peer list is wrong; revisit step 4.
## 8. Verify Sync
```bash
bitbadgeschaind status 2>&1 | jq '.sync_info'
```
`catching_up: false` means the node is synced. Compare `latest_block_height` with the [explorer](https://explorer.bitbadges.io/BitBadges%20Mainnet/staking).
## 9. Optional: Restore a Snapshot
A full sync from genesis takes hours. Restore a state snapshot from a community validator instead:
- [provewithryd: network overview and snapshots](https://docs.provewithryd.xyz/mainnet/bitbadges/network-overview)
- [nodestake: BitBadges snapshot](https://nodestake.org/bitbadges)
Restore the snapshot after steps 1 to 6 and before step 7.
## 10. Optional: Run Under Cosmovisor
Upgrades are announced in the `#chain-upgrades` Discord channel and use the Cosmos SDK `x/upgrade` module. [Cosmovisor](https://github.com/cosmos/cosmos-sdk/tree/main/tools/cosmovisor) applies them automatically at the scheduled height.
```bash
export DAEMON_HOME=$HOME/.bitbadgeschain
export DAEMON_NAME=bitbadgeschaind
cosmovisor init $(which bitbadgeschaind)
cosmovisor run start
```
For download-upgrade, backup policy, and other options follow the official Cosmovisor docs with `bitbadgeschaind` in place of the example daemon.
## Relayers
The official IBC connections BitBadges supports are in the [Cosmos chain registry](https://github.com/cosmos/chain-registry/tree/master/_IBC) and summarized on [Network](README.md).
## EVM JSON-RPC Configuration
To expose Ethereum-compatible JSON-RPC (MetaMask, ethers.js), configure the EVM settings in `app.toml`.
### EVM Chain ID
```toml
[evm]
# Set this to match your network's EVM chain ID
# Mainnet: 50024
# Testnet: 50025
# The default value (90123) will cause wallet transaction failures!
evm-chain-id = 50024
```
`evm-chain-id` feeds the `net_version` RPC method, which EIP-155 signature verification uses. If it differs from `eth_chainId` (read from chain state), wallets fail with `incorrect chain-id; expected 50024, got 90123`.
### JSON-RPC Server Options
```toml
[json-rpc]
# Enable JSON-RPC server
enable = true
# Address to listen on (use 0.0.0.0:8545 for external access)
address = "127.0.0.1:8545"
# WebSocket address for subscriptions
ws-address = "127.0.0.1:8546"
# API namespaces to enable
api = ["eth", "net", "web3"]
# Allow unprotected (non EIP-155) transactions
allow-unprotected-txs = false
# Enable custom tx indexer for better query performance
enable-indexer = true
# Maximum requests in a batch
batch-request-limit = 1000
# Maximum server response size (bytes)
batch-response-max-size = 25000000
# Max block range for eth_getLogs queries
block-range-cap = 10000
# Max results from eth_getLogs
logs-cap = 10000
# Timeout for eth_call (0 = infinite)
evm-timeout = "5s"
# Global filter cap
filter-cap = 200
# Gas cap for eth_call/estimateGas (0 = infinite)
gas-cap = 25000000
# Transaction fee cap (in BADGE)
txfee-cap = 1.0
# HTTP timeouts
http-timeout = "30s"
http-idle-timeout = "2m0s"
# Maximum simultaneous connections (0 = unlimited)
max-open-connections = 0
```
### Command-Line Flags
Every JSON-RPC option is also a flag:
| Flag | Default | Description |
| --- | --- | --- |
| `--json-rpc.enable` | `false` | Enable JSON-RPC server |
| `--json-rpc.address` | `127.0.0.1:8545` | HTTP server address |
| `--json-rpc.ws-address` | `127.0.0.1:8546` | WebSocket server address |
| `--json-rpc.api` | `eth,net,web3` | Enabled API namespaces |
| `--json-rpc.enable-indexer` | `false` | Enable custom tx indexer |
| `--json-rpc.evm-timeout` | `5s` | Timeout for eth_call |
| `--json-rpc.gas-cap` | `25000000` | Gas cap for calls |
| `--json-rpc.txfee-cap` | `1.0` | Transaction fee cap |
| `--json-rpc.filter-cap` | `200` | Max active filters |
| `--json-rpc.block-range-cap` | `10000` | Max block range for logs |
| `--json-rpc.logs-cap` | `10000` | Max log results |
| `--json-rpc.batch-request-limit` | `1000` | Max batch requests |
| `--json-rpc.batch-response-max-size` | `25000000` | Max response size |
| `--json-rpc.http-timeout` | `30s` | HTTP read and write timeout |
| `--json-rpc.http-idle-timeout` | `2m0s` | HTTP idle timeout |
| `--json-rpc.max-open-connections` | `0` | Max connections |
| `--json-rpc.allow-unprotected-txs` | `false` | Allow non-EIP155 txs |
| `--json-rpc.ws-origins` | `127.0.0.1,localhost` | WebSocket allowed origins |
### Production Recommendations
1. Put a reverse proxy (nginx, caddy) in front of JSON-RPC for TLS termination and rate limiting.
2. Set gas and fee caps to prevent resource exhaustion.
3. Enable the indexer (`--json-rpc.enable-indexer`) for faster queries.
4. Configure WebSocket origins if you accept external WS connections.
5. Monitor resource usage; JSON-RPC is resource-intensive under load.
## References
- [Binary releases](https://github.com/BitBadges/bitbadgeschain/releases)
- [Canonical genesis file](https://github.com/BitBadges/bitbadgeschain/blob/master/genesis-711316.json)
- [Block explorer](https://explorer.bitbadges.io/BitBadges%20Mainnet/staking)
- [Cosmos SDK: running a node](https://github.com/cosmos/cosmos-sdk/blob/main/docs/docs/user/run-node/01-run-node.md)
- [Cosmos tutorials: path to production](https://tutorials.cosmos.network/tutorials/9-path-to-prod/1-overview.html)
Community guides that cover the same ground: [provewithryd](https://docs.provewithryd.xyz/mainnet/bitbadges/network-overview) and [nodestake](https://nodestake.org/bitbadges). Most active validators in `#validators` have their own write-ups.
## Related
- [Network](README.md)
- [EVM RPC Endpoints](evm/rpc-endpoints.md)
- [WebSocket Events](websocket-events.md)
## File: chain/supported-denoms.md
---
description: "The x/tokenization allowed_denoms list on mainnet, the SDK coin registry, and the canonical USDC (via Injective) versus legacy USDC.n (via Noble) policy."
---
# Supported Denoms
`x/tokenization` keeps an allowlist of denoms that approval criteria (`coinTransfers`, backed paths, pricing) may reference. Governance updates it through `MsgUpdateParams`. The live list is the source of truth; the SDK's `MAINNET_COINS_REGISTRY` mirrors it with symbols and decimals.
```bash
curl https://lcd.bitbadges.io/bitbadges/bitbadgeschain/tokenization/params
```
```json
{
"params": {
"allowed_denoms": [
"ubadge",
"ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349",
"ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701",
"ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518",
"badges:49:chaosnet",
"ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8"
],
"affiliate_percentage": "0"
}
}
```
{% hint style="info" %}
Ask your agent:
```text
List every denom BitBadges accepts for payments and pools, with decimals, and tell me the current USDC price of BADGE.
```
The `bb assets list` and `bb assets price` commands read the registry and the pool prices on this page.
{% endhint %}
## Allowlist (Mainnet, 2026-09-06)
| Symbol | Denom | Decimals | Route | Status |
| --- | --- | --- | --- | --- |
| `BADGE` | `ubadge` | 9 | native | canonical |
| `USDC` | `ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8` | 6 | `transfer/channel-40/erc20:0xa00C59fF5a080D2b954d0c75e46E22a0c371235a` (Injective) | canonical, use for everything new |
| `USDC.n` | `ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349` | 6 | `transfer/channel-2/uusdc` (Noble) | legacy, deprecated for new use |
| `ATOM` | `ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701` | 6 | Cosmos Hub, `channel-3` | supported |
| `OSMO` | `ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518` | 6 | Osmosis, `channel-0` | supported |
| `CHAOS` | `badges:49:chaosnet` | 9 | wrapped collection 49 | supported, worthless test asset |
Any other `ibc/` denom that arrives (for example Osmosis's alloyed `allUSDC` or a bridged voucher of it) is unregistered: it is not allowlisted, carries no rate limits, and cannot back a collection.
## The Two USDC Routes
An IBC denom is the hash of the token's full transfer path, so the same asset arriving by two routes has two denoms. Treat them as separate balances; they never aggregate.
Canonical `USDC` is Circle's native USDC on Injective (erc20 contract `0xa00C59fF5a080D2b954d0c75e46E22a0c371235a`, CCTP-enabled), sent one IBC hop from Injective over `channel-40`. It is not a Noble voucher forwarded through Injective. The erc20 address in the trace is checksummed and the denom hash is case-sensitive. Use it for pricing, payment requests, subscriptions, prediction markets, pool creation, and backed collections. From `bitbadges@0.43.0`, the bare symbol `USDC` (in the SDK registry, in `bb --denom USDC`, in `"denom": "USDC"` JSON) resolves to `ibc/E1116484...`.
{% hint style="info" %}
The canonical route is proven and its allowlisting shipped with governance proposal 45, but circulating supply is still small. Skip Go indexes the denom on `bitbadges-1` (labeled `USDC.inj` in Skip's data, `USDC` in the BitBadges app) and can route native Injective USDC to BitBadges as a direct transfer. Routes that start from Noble or Ethereum USDC wait on Skip swap-venue coverage and pool liquidity; until then, get to native USDC on Injective first (CCTP or a swap there).
{% endhint %}
`USDC.n` is the original Noble-direct denom, kept only for existing balances and collections. The `.n` suffix is Skip Go's ecosystem-wide symbol for the Noble voucher; earlier drafts spelled it `USDC.noble`, and the CLI, SDK builders, and MCP builder tools still accept `USDC.noble` as typed input (output is always `USDC.n`).
- Existing balances stay usable and spendable, priced at $1, and Skip-supported so holders can swap out.
- `channel-2` stays open; no decommissioning is planned.
- The 16 collections with backed paths on it keep working. A backed path's escrow address derives from the denom string, so those collections cannot be repointed. That is also why a new backed collection on `USDC.n` is harmful: it would be stuck there. New backed collections use canonical `USDC`.
- Do not use it for anything new.
## SDK Registry
`MAINNET_COINS_REGISTRY` in `bitbadges` (`src/common/constants.ts`) carries the same six entries with `label`, `symbol`, `decimals`, `baseDenom`, `image`, `skipGoSupported`, and, from `0.43.0`, `deprecated` and `deprecationNote` on `USDC.n`.
```ts
import { MAINNET_COINS_REGISTRY } from 'bitbadges';
const usdc = MAINNET_COINS_REGISTRY['ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8'];
// { label: 'USDC', symbol: 'USDC', decimals: '6', skipGoSupported: true,
// baseDenom: 'ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8',
// image: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/usdc.png' }
const legacy = MAINNET_COINS_REGISTRY['ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349'];
// { label: 'USDC.n', symbol: 'USDC.n', decimals: '6', skipGoSupported: true, deprecated: true,
// baseDenom: 'ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349',
// deprecationNote: 'Legacy Noble-routed USDC. Existing balances stay fully usable, use canonical USDC (via Injective) for everything new.' }
```
`CHAOS` and `BADGE` use 9 decimals; the three IBC stablecoins and ATOM and OSMO use 6. `CHAOS` is not Skip-supported. Images point at the Cosmos chain registry.
## Related
- [Network](README.md)
- [Backed Minting](../token-standard/ibc/backed-minting.md)
- [Coin Transfers](../token-standard/approval-criteria/coin-transfers.md)
- [IBC Rate Limits](modules/ibc-rate-limit.md)
## File: chain/testnet.md
---
description: "Testnet status (offline since 2026-04-25), how to test on mainnet as a chaosnet instead, and the testnet endpoints and faucet API kept for when it returns."
---
# Testnet
The BitBadges testnet (`bitbadges-2`) is offline. This page records its status, the recommended substitute, and the endpoint and faucet shapes so they are ready when it returns.
{% hint style="warning" %}
Testnet has been offline since 2026-04-25 to reduce hosting costs while usage was minimal. Read-only checks on 2026-09-07 found DNS failures for the RPC, LCD, and EVM RPC hosts, HTTP 403 for the site, and HTTP 503 for the testnet API, and the SDK's `NETWORK_CONFIGS.testnet` is marked `disabled: true`. To ask for a relaunch, [contact the team](https://bitbadges.io/contact).
{% endhint %}
## Test on Mainnet Instead
Mainnet operates as a chaosnet: fully live, with experiments recorded on the same network as production assets.
- The public EVM RPC returned `eth_gasPrice: 0` on 2026-09-07. Query current fees before transacting; this is not a permanent zero-fee guarantee.
- Transact with worthless assets such as `CHAOS` (`badges:49:chaosnet`) instead of real-value coins.
- Contracts, transactions, and integrations run against the real network. Use dedicated accounts and test assets; state changes and any configured payments are real.
Every example in these docs targets mainnet. See [Network](README.md) for endpoints and [Supported Denoms](supported-denoms.md) for `CHAOS`.
## What Testnet Was
A separate environment isolated from production, with its own chain, database, API, and site. Nothing carried over between the two: not profiles, tokens, settings, or anything else. Third-party integrations such as claim plugins behaved the same in both.
Differences from production:
- Some features were unavailable: off-chain balances managed by BitBadges were not hosted on a CDN, BADGE credits could not be bought, no push notifications, and more.
- Some restrictions were relaxed: no API keys required, a more lenient faucet.
- Performance differed from production.
### Endpoints (Offline)
| Surface | URL |
| --- | --- |
| Site | `https://testnet.bitbadges.io` |
| BitBadges API | `https://api.bitbadges.io/testnet` (append the normal routes) |
| CometBFT RPC | `https://rpc-testnet.bitbadges.io` |
| WebSocket | `wss://rpc-testnet.bitbadges.io/websocket` |
| LCD (REST) | `https://lcd-testnet.bitbadges.io` |
| EVM JSON-RPC | `https://evm-rpc-testnet.bitbadges.io` |
| Cosmos chain ID | `bitbadges-2` |
| EVM chain ID | `50025` |
## Faucet API (Offline)
The faucet gave free BADGE for testing and bot development. No API key and relaxed CORS. Kept here as the contract for when testnet returns; do not call it as a live step today.
```text
POST https://api.bitbadges.io/testnet/api/v0/faucet
```
Request:
```json
{
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
}
```
The address must be a `bb1` bech32 address. Use the SDK's `convertToCosmosAddress()` to convert from Ethereum or other formats.
Response: `200` with an empty body on success (tokens are queued and arrive shortly). `500` with a JSON error otherwise:
| Error | Cause |
| --- | --- |
| `Already_airdropped` | The address already received faucet tokens |
| `Invalid_request._Origin_not_found.` | The request origin could not be determined |
Behavior:
- Amount: 1000 `ubadge` per request.
- Limit: one airdrop per address, lifetime. For more, use another address or ask on Discord.
- Processing: asynchronous queue; tokens can take a few seconds.
- Authentication: none on testnet.
```bash
curl -X POST https://api.bitbadges.io/testnet/api/v0/faucet \
-H "Content-Type: application/json" \
-d '{"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"}'
```
```ts
// Using fetch directly
const response = await fetch('https://api.bitbadges.io/testnet/api/v0/faucet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: 'bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d' })
});
if (!response.ok) {
const error = await response.json();
console.error('Faucet error:', error);
}
```
Bot bootstrap pattern:
```ts
import { GenericEvmAdapter, NETWORK_CONFIGS } from 'bitbadges';
// Generate a new bot wallet
const adapter = await GenericEvmAdapter.fromMnemonic(
process.env.BOT_MNEMONIC!,
NETWORK_CONFIGS['testnet'].evmRpcUrl
);
// Fund it (one-time)
await fetch('https://api.bitbadges.io/testnet/api/v0/faucet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: adapter.address })
});
// Wait briefly for tokens to arrive
await new Promise(r => setTimeout(r, 5000));
// Now ready to sign and broadcast transactions
```
On mainnet there is no faucet; acquire BADGE through normal channels.
## Related
- [Network](README.md)
- [Supported Denoms](supported-denoms.md)
- [Bot Examples](../agents/bot-examples.md)
## File: chain/websocket-events.md
---
description: "Subscribe to chain events over the CometBFT WebSocket: endpoint, subscribe and unsubscribe, queries by message type and sender, a Node.js listener."
---
# WebSocket Events
The chain exposes the standard CometBFT JSON-RPC WebSocket for real-time events. Bots use it to react to transfers, mints, and collection updates as they land.
```json
{
"jsonrpc": "2.0",
"method": "subscribe",
"id": 1,
"params": {
"query": "tm.event='Tx'"
}
}
```
## Connection
| Network | WebSocket URL |
| --- | --- |
| mainnet | `wss://rpc.bitbadges.io/websocket` |
| testnet | `wss://rpc-testnet.bitbadges.io/websocket` (offline, see [Testnet](testnet.md)) |
## Queries
Use the `subscribe` method with a CometBFT event query.
All transactions:
```text
tm.event='Tx'
```
New blocks:
```text
tm.event='NewBlock'
```
Transactions by message type:
```text
tm.event='Tx' AND message.action='/tokenization.MsgTransferTokens'
```
Transactions by sender:
```text
tm.event='Tx' AND message.sender='bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d'
```
Combined:
```text
tm.event='Tx' AND message.action='/tokenization.MsgCreateCollection' AND message.sender='bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d'
```
## Message Type URLs
The `message.action` attribute is the message type URL, `/.`. The tokenization package is `tokenization`.
| Message | `message.action` |
| --- | --- |
| Transfer tokens | `/tokenization.MsgTransferTokens` |
| Create collection | `/tokenization.MsgCreateCollection` |
| Update collection | `/tokenization.MsgUpdateCollection` |
| Delete collection | `/tokenization.MsgDeleteCollection` |
| Update user approvals | `/tokenization.MsgUpdateUserApprovals` |
| Create address lists | `/tokenization.MsgCreateAddressLists` |
| Swap (gamm) | `/gamm.v1beta1.MsgSwapExactAmountIn` |
| Send with alias routing | `/sendmanager.MsgSendWithAliasRouting` |
The full message list is in [Messages](../token-standard/messages/README.md).
## Node.js Listener
```ts
import WebSocket from 'ws';
const ws = new WebSocket('wss://rpc.bitbadges.io/websocket');
ws.on('open', () => {
console.log('Connected to BitBadges mainnet WebSocket');
// Subscribe to all transactions
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'subscribe',
id: 1,
params: {
query: "tm.event='Tx'"
}
}));
});
ws.on('message', (data: WebSocket.Data) => {
const msg = JSON.parse(data.toString());
if (msg.result?.data?.value?.TxResult) {
const txResult = msg.result.data.value.TxResult;
const txHash = msg.result.events?.['tx.hash']?.[0];
console.log('New transaction:', txHash);
console.log('Height:', txResult.height);
// Parse events for specific actions
const events = txResult.result?.events || [];
for (const event of events) {
if (event.type === 'message') {
const action = event.attributes?.find(
(a: any) => atob(a.key) === 'action'
);
if (action) {
console.log('Action:', atob(action.value));
}
}
}
}
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
ws.on('close', () => {
console.log('Disconnected. Reconnecting...');
// Implement reconnection logic here
});
```
Install the `ws` package:
```bash
bun add ws
bun add -d @types/ws # if using TypeScript
```
## Unsubscribe
One query:
```json
{
"jsonrpc": "2.0",
"method": "unsubscribe",
"id": 2,
"params": {
"query": "tm.event='Tx'"
}
}
```
All queries:
```json
{
"jsonrpc": "2.0",
"method": "unsubscribe_all",
"id": 2,
"params": {}
}
```
## Behavior
- Event attributes can be base64-encoded in CometBFT responses. Decode with `atob()` or `Buffer.from(str, 'base64').toString()`. Newer CometBFT versions return plain strings; handle both.
- Connections drop on network issues. Reconnect with exponential backoff in production bots.
- The BitBadges API's internal WebSocket (activity feeds, candlestick data) is not public. Use the REST API for indexed data.
- For historical data, use the [BitBadges API](../api/README.md) rather than replaying events.
## Related
- [Network](README.md)
- [Bot Examples](../agents/bot-examples.md)
- [Messages](../token-standard/messages/README.md)
## File: cli/README.md
---
description: "Install the bb CLI, learn its command groups, configure settings and API keys, and run doctor and completion."
---
# CLI
The `bb` CLI builds, inspects, signs, and broadcasts BitBadges transactions and calls the BitBadges API from the terminal. This page covers install, the command groups, settings, and health checks.
Agents should start with [CLI Automation](agent-workflow.md), choose a [standard](../standards/README.md), and use the [main-wallet payment flow](../agents/main-wallet-payments.md) when a human signs each request. The installed `--help-json` tree is the syntax authority; the standard pages explain the state and accounting rules behind those commands.
## Install
```bash
curl -fsSL https://install.bitbadges.io | sh
bb version
bb doctor
```
The one-liner installs two binaries:
| Binary | What it is | Where it comes from |
| --- | --- | --- |
| `bitbadgeschaind` | The chain node binary (Cosmos SDK). The installer symlinks it as `bb`. | GitHub release for your OS and architecture, into `/usr/local/bin` |
| `bitbadges-cli` | The SDK CLI (Node.js). `bb` forwards SDK verbs to it. | `bun install -g bitbadges` if bun is present, else `npm install -g bitbadges` |
`bb` is the only name used in these docs. `bitbadgeschaind` is the canonical binary name and is accepted everywhere `bb` is.
Installer options:
```bash
curl -fsSL https://install.bitbadges.io | sh -s -- --version v35 # pin a release
curl -fsSL https://install.bitbadges.io | sh -s -- --install-dir ~/.local/bin
curl -fsSL https://install.bitbadges.io | sh -s -- --no-sudo
curl -fsSL https://install.bitbadges.io | sh -s -- --testnet # testnet binary (testnet is offline)
```
| Platform | Architecture |
| --- | --- |
| Linux | x86_64, ARM64 |
| macOS | Intel, Apple Silicon |
| Windows | x86_64 via Git Bash, MSYS2, or WSL (no `bb` symlink; call `bitbadgeschaind.exe`) |
### SDK CLI Only
If you do not need the chain node binary:
```bash
bun install -g bitbadges # or: npm install -g bitbadges
bitbadges-cli --help
```
The npm package `bitbadges` ships three bins: `bitbadges` and `bitbadges-cli` (the CLI, same file) and `bitbadges-builder` (the MCP builder tools server, see [Set Up Your AI](../agents/setup.md)). SDK-owned commands such as `bb build` and `bb api` run as `bitbadges-cli build` and `bitbadges-cli api`. Native chain commands such as `query`, `keys`, and `sign-arbitrary` still require `bitbadgeschaind`; the SDK-only install does not provide them.
### Chain Binary from Source
```bash
git clone https://github.com/BitBadges/bitbadgeschain.git
cd bitbadgeschain
make build-mainnet-darwin/arm64 # or build-mainnet-linux/amd64, build-mainnet-linux/arm64, build-mainnet-darwin/amd64, build-mainnet-windows/amd64
```
Requires the Go version in `go.mod` (1.26 at time of writing). Pre-built binaries are on the [releases page](https://github.com/BitBadges/bitbadgeschain/releases). Use the binary version that matches the current chain height; the chain upgrades periodically.
### Claude Code Plugin
Claude Code users can add the [Claude Code Plugin](../agents/claude-code-plugin.md) on top of this install. It wires the MCP server and adds workflow skills. It does not replace the CLI.
## Command Groups
`bb --help` prints the commands in groups so you can tell which binary owns a verb.
```bash
bb --help
bb --help-json | jq '.commands[] | .name' # full SDK command tree as JSON
```
| Group | Commands | Reference |
| --- | --- | --- |
| Chain node (Cosmos SDK) | `start`, `init`, `status`, `version`, `tx`, `query` (`q`), `keys`, `sign-arbitrary`, `genesis`, `config`, `debug`, `prune`, `snapshots`, `comet`, `export`, `rollback`, `index-eth-tx`, `pre-upgrade` | [Chain Commands](chain.md) |
| Build and ship a transaction | `build`, `check`, `explain`, `simulate`, `preview`, `deploy`, `tx status`, `tx wait` | [Build](build.md), [Analyze](analyze.md), [Deploy](deploy.md) |
| Standards (end-user actions) | `pay-requests`, `bounties`, `subscriptions`, `intents`, `credit-tokens`, `products`, `crowdfunds`, `auctions`, `prediction-markets`, `smart-tokens`, `nfts`, `custom-2fa`, `dynamic-stores` | [Standards](standards.md) |
| Indexer access | `api`, `auth` | [API](api.md), [Auth](auth.md) |
| Account and lookup | `account`, `amount`, `url` | [Account](account.md), [Swap](swap.md) |
| Swap and DEX | `swap`, `pools`, `pairs`, `balances`, `price`, `assets` | [Swap](swap.md) |
| Dev / agent surface | `dev` (`tools`, `resources`, `docs`, `skills`, `gen-pub-key`, `feedback`) | [Dev](dev.md) |
| Local state | `settings`, `burner`, `session` | this page, [Deploy](deploy.md), [Dev](dev.md) |
| Discovery | `doctor` | this page |
| Misc | `completion` | this page |
{% hint style="warning" %}
The chain binary forwards SDK verbs by name. Chain releases before the fix in bitbadgeschain PR `fix/bb-forward-missing-sdk-verbs` do not forward `tx status`, `tx wait`, `amount`, `balances`, `assets`, `url`, or `custom-2fa`. On those releases run them as `bitbadges-cli `; `bb --help` shows which verbs your binary forwards.
{% endhint %}
### Deprecated Forms
Old forms still resolve for one release and print a one-line `[bb] DEPRECATED:` banner on stderr that names the new form. The release after the window removes them.
| Old form | New form |
| --- | --- |
| `bb cli ` | `bb ` |
| `bb portfolio`, `bb address`, `bb lookup`, `bb alias`, `bb gen-list-id` | `bb account ` |
| `bb tools`, `bb tool`, `bb resources`, `bb docs`, `bb skills`, `bb gen-pub-key` | `bb dev ` |
| `bb config` | `bb settings` (the chain binary owns `bb config`, which manages `client.toml`) |
| `bb sign-with-browser` | `bb deploy --browser --message` |
| `bb gen-tx-payload` | `bb deploy --gen-payload` |
| `bb swap pools`, `bb swap asset-pairs` | `bb pools`, `bb pairs` |
| `bb build smart-account` | `bb build smart-token` |
| `--expiry`, `--valid-until` | `--expiration` |
| `--json-only` | `--quiet` |
`BB_QUIET=1` or `--quiet` suppresses the banner. Do not write new scripts or agent prompts against the old forms.
## Output Envelope
Every data-emitting SDK verb prints one JSON envelope on stdout and human commentary on stderr.
```bash
bb api tokens get-collection 1 | jq .data
bb explain tx.json --quiet | jq -r .data.fullText
```
```json
{ "ok": true, "data": {}, "warnings": [], "hint": "optional", "meta": {}, "error": null }
```
| Flag | Applies to | Description |
| --- | --- | --- |
| `--condensed` | every envelope verb | Single-line JSON |
| `--output-file ` | every envelope verb | Write the envelope to a file instead of stdout |
| `-q`, `--quiet` | global | Silence stderr commentary (review banners, "Written to" notices, deprecation banners). Errors still print. Same as `BB_QUIET=1`. |
| `--help-json` | global | Print the whole SDK command tree as JSON |
`bb build` adds a `meta` sidecar with validation, review, simulate, and resolved-metadata reports next to the message in `data`. `hint` is set on common failures (auth rejected, 401/403, insufficient funds on deploy, `tx wait` timeout).
Inputs are uniform: a file path, `@file.json`, inline JSON, or `-` for stdin.
## Settings
```bash
bb settings set apiKey "$BITBADGES_API_KEY"
bb settings show
bb settings unset apiKeyTestnet
```
Config lives at `~/.bitbadges/config.json` (or `$BITBADGES_CONFIG_DIR/config.json`). Get an API key at [bitbadges.io/developer](https://bitbadges.io/developer).
| Subcommand | Description |
| --- | --- |
| `settings show` | Print the current config as an envelope |
| `settings set ` | Set one key |
| `settings unset ` | Remove one key |
| Key | Values |
| --- | --- |
| `apiKey` | Default API key |
| `apiKeyTestnet` | Testnet API key |
| `apiKeyLocal` | Local API key |
| `network` | `mainnet`, `testnet`, `local` |
| `url` | Custom API base URL |
### Environment Variables
| Variable | Description |
| --- | --- |
| `BITBADGES_API_KEY` | Default API key (all networks) |
| `BITBADGES_API_KEY_TESTNET` | Testnet API key |
| `BITBADGES_API_KEY_LOCAL` | Local API key |
| `BITBADGES_API_URL` | Custom API base URL (overrides config) |
| `BITBADGES_CONFIG_DIR` | Override `~/.bitbadges` |
| `BB_QUIET` | `1` silences stderr commentary |
| `BITBADGES_TESTNET_OFFLINE` | `false` bypasses the testnet-offline guard for a private chain that uses the testnet chain ID |
### Network Flags
Every SDK verb that reaches the network accepts the same flags.
| Flag | Description |
| --- | --- |
| `--network ` | `mainnet`, `testnet`, or `local` |
| `--mainnet` | Shortcut for `--network mainnet` (the default) |
| `--testnet` | Shortcut for `--network testnet`. Testnet is offline; see [Testnet](../chain/testnet.md). |
| `--local` | Shortcut for `--network local` (`http://localhost:3001`, LCD `http://localhost:1317`) |
| `--url ` | Custom API base URL (overrides everything else) |
| `--api-key ` | Override the API key for this call |
Resolution order for the API key: `--api-key` > network-specific env var > `BITBADGES_API_KEY` > network-specific config key > `apiKey` in config. For the base URL: `--url` > `--local` > `--testnet` > `BITBADGES_API_URL` > config `url` > `https://api.bitbadges.io`.
| Network | API | LCD | Cosmos chain ID | EVM chain ID | EVM RPC |
| --- | --- | --- | --- | --- | --- |
| mainnet | `https://api.bitbadges.io` | `https://lcd.bitbadges.io` | `bitbadges-1` | 50024 | `https://evm-rpc.bitbadges.io` |
| testnet (offline) | `https://api.bitbadges.io/testnet` | `https://lcd-testnet.bitbadges.io` | `bitbadges-2` | 50025 | `https://evm-rpc-testnet.bitbadges.io` |
| local | `http://localhost:3001` | `http://localhost:1317` | `bitbadges-1` | 90123 | `http://localhost:8545` |
## Doctor
```bash
bb doctor
bb doctor --with-preview
bb doctor --condensed | jq '.data.checks[] | select(.status != "pass")'
```
Probes, in order: Node version (18 or newer), SDK package and version, config file, API key for the resolved network (pings the simulate endpoint), MCP stdio bin, persisted sessions parse, and with `--with-preview` a preview upload and fetch round trip that asserts byte equality. Each probe reports `pass`, `fail`, `warn`, or `skip`. The exit code is non-zero only on hard failures.
| Flag | Description |
| --- | --- |
| `--with-preview` | Add the preview round-trip probe |
| `--condensed`, `--output-file` | Output flags |
| network flags | Which API the key probe hits |
## Session
Builder sessions from `bb dev tools call --session ` persist under `~/.bitbadges/sessions/.json`.
```bash
bb session list
bb session show demo
bb session reset demo
```
## Completion
```bash
eval "$(bb completion)" # bash or zsh
bb completion zsh >> ~/.zshrc
```
The emitted script supports both shells through `bashcompinit`. Pass `bash` or `zsh` as a hint; any other value exits 2.
## Quick Examples
```bash
bb query bank balances bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --output json # chain native
bb api tokens get-collection 1 # BitBadges API
bb build vault --backing-coin USDC --name "Demo Vault" \
--image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/vault.png \
--description "USDC vault" \
| bb preview - --open # build, then review and sign in the browser
bb auth login --browser --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d # session for Full Access routes
bb check tx.json && bb simulate tx.json # audit and dry-run
```
The first command, run against mainnet for an address that holds nothing, prints:
```json
{
"balances": [],
"pagination": {}
}
```
{% hint style="info" %}
Ask your agent. With the [MCP builder tools](../agents/setup.md) wired, the build-and-preview line above is one prompt:
```text
Build a USDC vault called Demo Vault and give me a link to review and sign.
```
{% endhint %}
`bb --help` ends with the Chaosnet warning and a link to the policies at bitbadges.io/policies. Transactions use real tokens.
## Related
- [Build](build.md)
- [Deploy](deploy.md)
- [Agents](../agents/README.md)
- [Quickstart](../start/quickstart.md)
## File: cli/account.md
---
description: "Read an address profile, tokens, balances, activity, and approvals; convert and validate addresses; derive alias addresses and public keys; resolve URLs."
---
# bb account, url, gen-pub-key
`bb account` is the read-only user view (the same sections as `bitbadges.io/account/`) plus address utilities. `bb url` resolves canonical links and `bb dev gen-pub-key` derives a Cosmos public key.
## Example
```bash
bb account all --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d
bb account me # same, for the active auth session
bb account tokens --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --view collected
bb account approvals --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --has-coin-transfers
bb account convert 0x0bc63cfe31d5218eb414b142c799e20964a54a1a --to bb1
bb account lookup USDC
bb url collection 42 --raw | xargs open
```
`bb account tokens` for an address that holds nothing returns an empty page (mainnet output):
```json
{
"ok": true,
"data": {
"tokens": [],
"pagination": {
"bookmark": "",
"hasMore": false
}
},
"warnings": [],
"error": null
}
```
Every read verb accepts the [network flags](README.md#network-flags), `--condensed`, and `--output-file`. `0x` inputs are normalized to `bb1` client-side.
{% hint style="info" %}
Ask your agent. The MCP tools `convert_address`, `validate_address`, `query_balance`, and `lookup_token_info` cover the same ground:
```text
Convert 0x0bc63cfe31d5218eb414b142c799e20964a54a1a to a bb1 address and tell me what it holds in collection 1.
```
{% endhint %}
## Read Verbs
| Verb | Wraps | Purpose |
| --- | --- | --- |
| `profile` | `GET /user` | Profile document plus LCD bank balances |
| `tokens` | `GET /account/:addr/tokens` | BitBadges-standard holdings |
| `balances` | `GET /account/:addr/balances` | Lean balance documents |
| `assets` | swap-consolidated balances | Skip:Go plus verified BitBadges assets, numeric amounts |
| `activity` | activity feeds | Transfers, claims, points |
| `approvals` | `POST /collection/:id/filterApprovals` | Approvals owned by the user: subscriptions, listings, bids, payments |
| `all` | all of the above in parallel | One JSON with a section per verb; a failing section degrades to `{ error }` |
| `me` | `all` | For the active `bb auth` address |
| Flag | Verbs | Description |
| --- | --- | --- |
| `--address ` | all but `me` | Required |
| `--view ` | `tokens` | `collected` (default), `created`, `managing`, `all` |
| `--bookmark `, `--oldest-first` | `tokens`, `balances`, `activity` | Pagination and sort |
| `--chain `, `--all-chains` | `assets`, `all`, `me` | Chain for the assets section (default `bitbadges-1`) or a broad Skip:Go set |
| `--type ` | `activity` | `tokens` (default), `claims`, `points`, `all` |
| `--collection ` | `approvals` | Collection ID or `any` (default) |
| `--token-id `, `--time ` | `approvals` | Filter to approvals covering a token or valid at a time |
| `--has-coin-transfers` | `approvals` | Only approvals with a coin transfer leg |
| `--price-min `, `--price-max `, `--denom `, `--sort ` | `approvals` | Price filter and sort |
| `--include `, `--exclude ` | `all`, `me` | Subset of `account`, `tokens`, `balances`, `assets`, `activity`, `approvals` |
## Address Utilities
```bash
bb account convert 0x0bc63cfe31d5218eb414b142c799e20964a54a1a --to bb1
bb account convert bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --to 0x
bb account validate bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d # exit 0 valid, 2 invalid
bb account lookup # every known token
bb account lookup BADGE # denom, decimals, networks, backing address
bb account gen-list-id bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue
```
`convert`, `validate`, `lookup BADGE`, and `gen-list-id` print, in order:
```json
{ "ok": true, "data": { "result": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d", "source": "0x0bc63cfe31d5218eb414b142c799e20964a54a1a", "target": "bb1" }, "warnings": [], "error": null }
```
```json
{ "ok": true, "data": { "valid": true, "chain": "BitBadges", "address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d" }, "warnings": [], "error": null }
```
```json
{ "ok": true, "data": { "symbol": "BADGE", "ibcDenom": "ubadge", "decimals": 9, "networks": ["mainnet", "testnet"] }, "warnings": [], "error": null }
```
```json
{
"ok": true,
"data": {
"listId": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d:bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
"mode": "whitelist",
"addresses": ["bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d", "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue"]
},
"warnings": [],
"error": null
}
```
| Verb | Description |
| --- | --- |
| `convert [--to bb1\|0x]` | Convert between encodings. Default target is the opposite of the input. `bb1` and `0x` are the same key in two encodings. |
| `validate ` | Check validity and detect the chain |
| `lookup [symbol]` | Token info from the coins registry. Omit the symbol to list all. |
| `gen-list-id [--blacklist]` | Deterministic reserved address list ID for an ad-hoc allowlist or blocklist, without registering it on chain. See [Address Lists](../token-standard/concepts/address-lists.md). |
### alias
```bash
bb account alias for-ibc-backing ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8
bb account alias for-wrapper ubadge
bb account alias for-mint-escrow 42
```
```json
{ "ok": true, "data": { "address": "bb1xx5h3l85tnxgj07vef2cjtqzpg2qc9jt52z2q0lptjasajez3cgs5hklra", "kind": "ibc-backing", "source": "ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8" }, "warnings": [], "error": null }
```
```json
{ "ok": true, "data": { "address": "bb1gycvn0nc50lh753dgk4qys5p2sdws8aw7ec9v9gg65pkhm6hqq3qjd3t3n", "kind": "wrapper", "source": "ubadge" }, "warnings": [], "error": null }
```
```json
{ "ok": true, "data": { "address": "bb1slvx3q432arp0ekkt9t2uednd643t9zycmyz8c2x9ppq5def7yssuuwl36", "kind": "mint-escrow", "source": "42" }, "warnings": [], "error": null }
```
| Verb | Address |
| --- | --- |
| `for-ibc-backing ` | Backing address of an IBC-backed Smart Token (deposits land here) |
| `for-wrapper ` | Wrapper path address for a Cosmos coin wrapper |
| `for-mint-escrow ` | Mint escrow address of a collection (where quest reward funds go) |
These are protocol-controlled addresses with auto-set approvals. Do not write to them directly; use the matching flow. See [Alias Denoms](../token-standard/ibc/alias-denoms.md) and [Backed Minting](../token-standard/ibc/backed-minting.md).
## url
```bash
bb url tx 0x9f1c2b3a4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8 # EVM explorer (Blockscout)
bb url tx-cosmos E5B4C3A6E5B1F3B9F0F4C1F2B7A6D5C4E3F2A1B0C9D8E7F6A5B4C3D2E1F0A9B8 # Cosmos explorer (ping.pub)
bb url collection 42
bb url badge 42 7 # token 7 in collection 42
bb url address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d
bb url collection 42 --raw # only the URL string
```
`bb url tx-cosmos`, `bb url address`, and `bb url collection 42 --raw` print:
```json
{
"ok": true,
"data": {
"kind": "tx",
"explorer": "ping.pub",
"hash": "E5B4C3A6E5B1F3B9F0F4C1F2B7A6D5C4E3F2A1B0C9D8E7F6A5B4C3D2E1F0A9B8",
"url": "https://explorer.bitbadges.io/BitBadges%20Mainnet/tx/E5B4C3A6E5B1F3B9F0F4C1F2B7A6D5C4E3F2A1B0C9D8E7F6A5B4C3D2E1F0A9B8"
},
"warnings": [],
"error": null
}
```
```json
{ "ok": true, "data": { "kind": "address", "address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d", "url": "https://bitbadges.io/account/bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d" }, "warnings": [], "error": null }
```
```text
https://bitbadges.io/collections/42
```
| Flag | Description |
| --- | --- |
| `--testnet` | Resolve testnet URLs |
| `--raw` | Print the URL without the envelope, for piping into `open` or `xdg-open` |
| `--condensed`, `--output-file ` | Output flags |
{% hint style="warning" %}
Chain releases before the forwarder fix do not forward `url`. If `bb url` prints unknown command, run `bitbadges-cli url` with the same arguments.
{% endhint %}
## gen-pub-key
```bash
bb dev gen-pub-key --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d # look up on the BitBadges API, then the chain LCD
bb dev gen-pub-key --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --print-message # what the wallet must sign for recovery
bb dev gen-pub-key --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --signature "$SIG" --no-lookup
```
`--print-message` returns the canonical text:
```json
{
"ok": true,
"data": {
"message": "BitBadges public key derivation. This signature is used only to derive your public key for offline signing. No transaction is being created or authorized, no session is being granted, and this signature has no validity outside this CLI flow."
},
"warnings": [],
"error": null
}
```
For an address that has never broadcast or signed in, the lookup path fails with `"code": "pubkey_not_found"` and tells you to use the signature path.
Derives the base64 compressed secp256k1 public key for a `bb1` address (or a `0x` address, converted). Fresh accounts have no pubkey on chain, so the recovery path takes a signature over the canonical message and recovers the key. EVM-only signing does not need this; the EVM transaction recovers the pubkey itself.
| Flag | Description |
| --- | --- |
| `--address ` | Required |
| `--signature ` | Skip lookups and recover from this signature |
| `--message ` | Message that was signed, if not the canonical one |
| `--print-message` | Print the canonical message and exit |
| `--no-lookup` | Force the recovery path |
## Related
- [Accounts](../token-standard/concepts/accounts.md)
- [Address Conversions](../sdk/snippets/address-conversions.md)
- [Auth](auth.md)
- [Swap](swap.md)
## File: cli/agent-workflow.md
---
description: "CLI-first agent playbook: machine-readable discovery, parameter examples, exact units, JSON envelopes, human signing, confirmation, retries, and version compatibility."
---
# CLI Automation for Agents
Use the CLI to discover, build, inspect, and hand transactions to a signer. A language model should not reconstruct approval JSON from memory when a standard builder already expresses the requested terms.
## Discover the Installed Surface
```bash
bb --help-json > commands.json
bb build payment-request-v2 --help
bb pay-requests pay --help
bb dev tools list --names
bb dev skills payment-obligations
```
The npm SDK CLI is also available as `bitbadges-cli`. If an older chain binary does not forward a verb, invoke `bitbadges-cli` directly. Record the installed package version alongside automation runs; new example/schema commands and the versioned browser handoff require matching SDK and frontend releases.
Use [Choose a Standard](../standards/README.md) for intent selection. Use command help for flags, tool schemas for input structure, and standard documentation for lifecycle semantics. The MCP structural schema does not encode every runtime refinement; successful schema inspection is not successful validation.
## Generate Editable Payment Terms
```bash
bb build payment-request-v2 --list-examples
bb build payment-request-v2 --schema
bb build payment-request-v2 --example threshold | jq '.data' > terms.json
```
Available examples are `specific`, `anyone`, `one`, `all`, `threshold`, `installments`, `partial`, `target`, and `link`. Finite examples use dates relative to generation time. They contain sample addresses and amounts; replace them with the user's actual terms and verify the resulting dates before publishing.
```bash
bb build payment-request-v2 --json terms.json --output-file proposal.json
bb pay-requests build-v2 --json terms.json --output-file proposal-alias.json
```
Both commands use the same builder. Arbitrary valid custom obligations remain supported through `--json`. Discovery commands emit parameters or schemas and cannot be combined with signing, simulation, or build inputs. They do not publish collections.
## Metadata Precedence
JSON metadata is used by default. Explicit `--uri` selects URI metadata and replaces inline JSON metadata. Any explicit `--name`, `--image`, or `--description` selects inline metadata, clears a JSON URI, and keeps compatible unspecified inline JSON fields. The completed inline triple is required. Combining a URI flag with inline metadata flags is rejected.
This V2 behavior is explicit; do not assume every legacy builder uses the same flag precedence. Read the installed help and validate the generated proposal.
## Separate Input and Output Shapes
| File or response | Meaning |
| --- | --- |
| Example envelope `.data` | Editable builder parameters |
| `terms.json` | Parameters supplied to `--json` |
| Build envelope `.data` | Generated message or message wrapper |
| Build envelope `.meta` | Validation/review/simulation reports when present |
| Browser result | A signing/submission report; not independent chain confirmation |
| Chain transaction result | Execution success/failure on the selected network |
| Indexed standard status | A projection that may lag confirmed execution |
Check both the process exit status and envelope `ok`. Preserve stderr diagnostics. Do not feed an error envelope into the next command, parse human prose as JSON, or strip the message wrapper from a multi-message transaction. `bb deploy` accepts the documented build envelope; `jq '.data'` is specifically useful when turning an example envelope into parameter input.
Use `--output-file` for artifacts you need to review. A quiet flag changes commentary, not economic behavior. Read flags such as `--browser`, `--burner`, and keyring `--exec` as side-effect switches.
## Exact Amounts, Addresses, and Times
V2 terms use positive base-unit integer strings. CLI amount flags on other commands may resolve symbols into display units; raw denominations can use different rules. Never infer the conversion from a ticker alone. Record the network and canonical denomination, then review both the base amount and human display amount.
Partial-payment `--units` counts quanta and scales all payout legs. It is not the number of people or the amount of a coin. Keep different denominations separate. Convert EVM addresses to the required canonical form through the existing address helpers; never create a new address mapping in a shell script.
V2 payment windows use inclusive Unix millisecond endpoints. Subscription intervals are fixed durations with separate renewal consent. Local time labels are display only; chain execution uses chain time.
## Human Review and Retry
Use [Main-Wallet Payment Requests](../agents/main-wallet-payments.md) to ask for exact human approval. After a hash is returned, query `bb tx status` or wait with `bb tx wait` on the same network. Treat a timeout, closed browser, or lost callback as uncertain until reconciled.
Retry read-only queries with bounded backoff. Retry a payment only after establishing what happened to the prior attempt and obtaining authorization for any changed terms. A reusable link can accept duplicate valid payments. Subscription renewal consent can authorize future charges; an invoice payment does not.
## Fetch Only the Documentation You Need
The docs provide [llms.txt and Markdown pages](../agents/reading-the-docs.md). Start with the standards index and this CLI page, then load the specific standard, action reference, and underlying approval criterion involved. Use the complete corpus when an offline snapshot is needed; avoid repeatedly downloading generated SDK references for ordinary CLI actions.
## File: cli/analyze.md
---
description: "Inspect a built transaction before signing with bb check, bb explain, bb simulate, and bb preview."
---
# bb check, explain, simulate, preview
Four verbs inspect a transaction JSON (from `bb build`, the MCP builder tools, or by hand) before anyone signs it. All accept a file path, `@file.json`, inline JSON, `-` for stdin, and where noted a numeric collection ID.
## Example
```bash
bb check tx.json # validate + review + metadata coverage
bb explain tx.json --quiet | jq -r .data.fullText
bb simulate tx.json # gas + per-address balance changes, no broadcast
bb preview tx.json --open # upload, then open the review-and-sign page
bb build vault --backing-coin USDC --uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json | bb check -
```
| Command | Purpose |
| --- | --- |
| `check ` | Structural validation, design review, and metadata coverage in one pass |
| `explain ` | Plain-English summary; detects transaction vs collection |
| `simulate ` | Dry run against the simulate endpoint |
| `preview ` | Upload and print a read-only link and a review-and-sign link |
All four print the envelope on stdout and human-readable scorecards on stderr (`--quiet` or `BB_QUIET=1` silences stderr). All accept `--condensed`, `--output-file `, and the [network flags](README.md#network-flags).
{% hint style="info" %}
Ask your agent. `review_collection`, `explain_collection`, `simulate_transaction`, and `get_review_url` are the MCP equivalents:
```text
Review and explain the transaction in tx.json, simulate it, then give me a link to sign.
```
{% endhint %}
## check
```bash
bb check tx.json # full (default)
bb check tx.json --depth structural # validateTransaction() only, offline
bb check tx.json --depth review # reviewCollection() only
bb check 42 --depth review # live mainnet collection by ID
bb check tx.json --strict # exit 1 on warnings
```
A `bb build vault --backing-coin USDC` transaction at the default depth prints (review findings trimmed to the first one):
```json fold=9-19
{
"ok": true,
"data": {
"validate": {
"valid": true,
"issues": []
},
"review": {
"findings": [
{
"code": "review.ux.forceful_transfers_not_locked",
"severity": "critical",
"source": "ux",
"category": "approvals",
"title": { "en": "Forceful transfers are not permanently blocked" },
"detail": { "en": "This collection has noForcefulPostMintTransfers set to false, which permits forceful post-mint transfers. No approvals currently enable them, but since the invariant is locked at creation time, any approval the manager adds later could introduce forceful transfers." },
"recommendation": { "en": "If forceful transfers should be permanently impossible, set invariants.noForcefulPostMintTransfers = true at creation. It cannot be toggled later. If forceful transfers are intentional for this collection (auction settlement, subscription revoke, prediction market resolution, etc.), you can safely ignore this." }
}
],
"summary": {
"critical": 1,
"warning": 0,
"info": 1,
"verdict": "fail"
}
}
},
"warnings": [],
"error": null
}
```
`--depth structural` on a valid file is the short form: `{ "ok": true, "data": { "valid": true, "issues": [] }, "warnings": [], "error": null }`.
| `--depth` | Runs | Use when |
| --- | --- | --- |
| `structural` | `validateTransaction()`: uint ranges, approval criteria, shape | Fast offline check before writing JSON to disk |
| `review` | `reviewCollection()`: design audit, standards conformance, UX checks | Reviewing someone else's work or a live collection by ID |
| `full` (default) | validate + review + design + metadata coverage | Pre-broadcast diligence |
| Flag | Description |
| --- | --- |
| `--depth ` | `structural`, `review`, `full` |
| `--strict` | Exit 1 on warnings. Criticals always exit 2. |
| `--no-validate`, `--no-review`, `--no-metadata` | Skip a section at `full` depth |
| `--design` | Include the informational design-decisions section in the envelope |
`review` and `full` accept a numeric collection ID and fetch it from the BitBadges API first. `structural` is offline and refuses numeric IDs.
## explain
```bash
bb explain tx.json
bb explain '{"messages":[{"typeUrl":"/tokenization.MsgDeleteOutgoingApproval","value":{"creator":"bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d","collectionId":"2","approvalId":"agent-daily-budget"}}]}'
bb explain 42 # fetch the collection, then interpret
bb build vault --backing-coin USDC --quiet | jq '{messages:[.data]}' | bb explain -
```
Input detection:
| Input | Path |
| --- | --- |
| Single message `{ typeUrl, value }` | `interpretTransaction()` over `value` |
| Wrapper `{ messages: [] }` with one or more entries | Finds the first collection message and explains it |
| Raw collection (no `typeUrl`, no `messages`) | `interpretCollection()` |
| Numeric `` | Fetches `/api/v0/collection/`, then `interpretCollection()` |
The last command above prints (summary and `fullText` trimmed to their first paragraphs):
```json
{
"ok": true,
"data": {
"kind": "tx",
"messages": [
{
"typeUrl": "/tokenization.MsgCreateCollection",
"summary": "## Transaction Summary\n\nCreate a new Smart Token (IBC-backed) called \"Unnamed Collection\" on BitBadges.\n\n## Collection Overview\n\n**\"Unnamed Collection\"** (creator-provided name) is a Smart Token (IBC-backed) on BitBadges. It contains 1 unique token ID (#1). There is no hard cap on supply per token ID, meaning tokens can be minted without an on-chain maximum.\n\nThis is a smart token backed 1:1 by USDC. Users deposit the IBC backing asset into a vault and receive collection tokens in return. They can redeem their collection tokens at any time to withdraw the backing asset from the vault.\n\nDeclared standards: Smart Token, Vault."
}
],
"fullText": "## Transaction Summary\n\nCreate a new Smart Token (IBC-backed) called \"Unnamed Collection\" on BitBadges."
},
"warnings": [],
"error": null
}
```
`kind` is `tx`, `collection`, or `msg`. `messages[]` is empty for raw collections. `fullText` is the whole prose, so `jq -r .data.fullText` works without branching on shape.
## simulate
```bash
bb simulate tx.json
bb simulate tx.json --creator bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d
bb simulate tx.json --events
```
| Flag | Description |
| --- | --- |
| `--creator ` | Simulation context address (default `bb1simulation`) |
| `--events` | Dump the full events array instead of the count |
Calls `/api/v0/simulate` and returns parsed events, per-address balance changes, and any error the chain would raise. Needs an [API key](../api/README.md#api-keys) on mainnet; a local BitBadges API usually accepts any key or none.
The `--creator` address must exist on chain. For an address that has never held BADGE the envelope is `ok` but the simulation reports the chain error (mainnet output):
```json
{
"ok": true,
"data": {
"success": false,
"error": "API Error 500: {\"errorMessage\":\"Error simulating transaction - fee payer address does not exist: unknown address\"}"
},
"warnings": [],
"error": null
}
```
User approval messages (`MsgUpdateUserApprovals`, `MsgSetIncomingApproval`, and so on) are refused: they change state on an existing collection with set, append, and delete variants, so a dry run has no stable meaning. Use `check` for those.
## preview
```bash
bb preview tx.json
bb preview tx.json --open
bb build vault --backing-coin USDC --uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json --quiet | bb preview - --open
bb preview tx.json --frontend-url http://localhost:3000
```
A successful upload prints (the code is an example; every upload mints a fresh one):
```json
{
"ok": true,
"data": {
"code": "prv_ab12cd34",
"url": "https://bitbadges.io/mint/local-builder?code=prv_ab12cd34",
"reviewUrl": "https://bitbadges.io/mint/local-builder?code=prv_ab12cd34",
"expiresAt": 1788742800000,
"expiresIn": "1h"
},
"warnings": [],
"error": null
}
```
| Field | Description |
| --- | --- |
| `reviewUrl` | Review and sign. Opens Preview, Review Items, Transferability, Permissions, then the wallet signature. Update transactions route to `/update/local-builder/` so the site diffs against chain state. |
| `url` | The same destination. `url` is the older field name, kept so existing scripts keep working. |
| `code` | The `prv_` code behind the link. Valid for one hour. |
| Flag | Description |
| --- | --- |
| `--open` | Open the link in the default browser |
| `--frontend-url ` | Base for the printed link (default `https://bitbadges.io`) |
| network flags | Which BitBadges API stores the preview |
Uploading the preview needs an [API key](../api/README.md#api-keys), like every other BitBadges API route. Whoever you send the link to needs none; the unguessable code is the secret. The site also accepts the code pasted into `/mint/local-builder`. Prefer `bb preview --open` when you want the full review sidebar before signing; `bb deploy --browser` is the tighter loop for a transaction you have already reviewed.
## Related
- [Build](build.md)
- [Deploy](deploy.md)
- [Dev](dev.md)
- [Agents](../agents/README.md)
## File: cli/api.md
---
description: "Call any BitBadges API route from the terminal with bb api, search routes, print schemas, and attach a session."
---
# bb api
`bb api [path-params] [flags]` calls a BitBadges API route. The route list is generated from the OpenAPI spec, so `bb api --help` and `bb api --search` are the current inventory.
## Example
```bash
bb api tokens get-collection 1
bb api accounts get-account --body '{"address":"bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"}'
bb api accounts get-accounts --body '{"accountsToFetch":[{"address":"bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"}]}'
bb api tx broadcast-tx --body @tx.json
bb api accounts get-tokens-view-for-user bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --body '{"viewType":"collected"}' # GET: body becomes query params
bb api tokens get-collection 1 --dry-run # print the request, do not send
bb api --search swap | jq -r '.data.matches[] | "\(.name)\t\(.method)\t\(.path)"'
bb api tokens get-collection --schema
```
`bb api accounts get-account` for the fixture address returns (mainnet output, `views` and empty arrays trimmed):
```json
{
"ok": true,
"data": {
"account": {
"bitbadgesAddress": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"ethAddress": "0x0bC63Cfe31D5218eB414b142c799e20964a54A1A",
"accountNumber": "-1",
"sequence": "-1",
"balances": [{ "amount": "0", "denom": "ubadge" }],
"pubKeyType": "secp256k1",
"publicKey": "",
"chain": "Cosmos",
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d"
}
},
"warnings": [],
"error": null
}
```
{% hint style="info" %}
Ask your agent. `query_collection`, `query_balance`, and `search` wrap the most-used routes:
```text
Fetch collection 1 and tell me whether bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d holds any of its tokens.
```
{% endhint %}
## Route Groups
```bash
bb api --help # groups with route counts
bb api tokens --help # routes in one group
bb api all --help # every route, flat
```
| Group | Routes for |
| --- | --- |
| `accounts` | Accounts and users |
| `tokens` | Collections and tokens |
| `claims` | Claims |
| `auth` | Sign In with BitBadges and OAuth |
| `tx` | Broadcast and simulate |
| `apps` | Developer apps |
| `plugins` | Plugins |
| `stores` | Off-chain dynamic data stores |
| `onchain-stores` | On-chain dynamic stores |
| `pages` | Utility pages |
| `maps` | On-chain maps and protocols |
| `assets` | DEX, pools, asset pairs |
| `misc` | Everything else |
| `all` | Every route, ungrouped |
Path parameters are positional. Each route's `--help` shows the HTTP method, path template, SDK type names, key body and query fields, and an SDK example where one exists.
## Flags
| Flag | Description |
| --- | --- |
| `--body ` | Request body: inline JSON, `@file.json`, or `-` for stdin. On GET routes it is converted to query params. |
| `--query ` | Query params as a JSON object |
| `--dry-run` | Print method, URL, headers, and body without sending |
| `--schema` | Print the route's request body fields, query params, and SDK type names. No API call. |
| `--with-session` | Attach the cookie of the active address for the resolved network (see [Auth](auth.md)) |
| `--as-address ` | Attach the cookie of a specific stored address (overrides `--with-session`) |
| `--condensed`, `--output-file ` | Output flags |
| `--api-key`, `--network`, `--mainnet`, `--testnet`, `--local`, `--url` | [Network flags](README.md#network-flags) |
## Discovery
`bb api --search ` scans route name, path, tag, and description (case-insensitive substring) and returns matches in the envelope. `bb api --search balance` prints (trimmed to the first three matches):
```json
{
"ok": true,
"data": {
"search": "balance",
"matches": [
{ "name": "get-user-balances", "method": "GET", "path": "/account/{address}/balances", "tag": "accounts", "description": "Get User Balances" },
{ "name": "get-balance-by-address-specific-token", "method": "GET", "path": "/collection/{collectionId}/balance/{address}/{tokenId}", "tag": "tokens", "description": "Get Balance By Address - Specific Token" },
{ "name": "get-balance-by-address", "method": "GET", "path": "/collection/{collectionId}/balance/{address}", "tag": "tokens", "description": "Get Balances By Address" }
]
},
"warnings": [],
"error": null
}
```
`bb api --schema` prints the route shape so an agent can build a valid body offline. For `bb api tokens get-collection --schema`:
```json
{
"ok": true,
"data": {
"name": "get-collection",
"method": "GET",
"path": "/collection/{collectionId}",
"description": "Get Collection",
"pathParams": ["collectionId"],
"hasBody": false,
"sdkLinks": {
"response": "iGetCollectionSuccessResponse",
"function": "BitBadgesAPI.getCollection"
},
"queryParams": [],
"bodyFields": [],
"requestSchema": null,
"responseSchema": null,
"example": null
},
"warnings": [],
"error": null
}
```
## Behavior
- An API key is required on every call (`bb settings set apiKey "$BITBADGES_API_KEY"` or the `BITBADGES_API_KEY` variable). Get one at [bitbadges.io/developer](https://bitbadges.io/developer).
- Routes gated by Full Access also need a user session. On HTTP 401 or 403 the envelope carries a `hint` that says to run `bb auth login` and retry with `--with-session`; if a cookie was already attached, the hint suggests a re-login.
- `--with-session` is opt-in; the CLI never attaches a cookie silently.
- Credits, limits, and error shapes: [API](../api/README.md).
## Related
- [Auth](auth.md)
- [Account](account.md)
- [API](../api/README.md)
## File: cli/auth.md
---
description: "Create and manage BitBadges API user sessions from the CLI with bb auth, using a browser wallet or any external signer."
---
# bb auth
`bb auth` signs the CLI in to the BitBadges API with the Sign In with BitBadges challenge flow and stores the session cookie for `bb api --with-session`. The CLI never holds a private key; a browser wallet, `bb sign-arbitrary`, or any ADR-36 / EIP-191 signer produces the signature.
## Example
```bash
# browser wallet (Keplr, MetaMask)
bb auth login --browser --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d
# headless, with a key in the chain binary keyring
MSG=$(bb auth challenge --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d | jq -r .data.message)
SIG_JSON=$(bb sign-arbitrary alice "$MSG")
bb auth login \
--address "$(echo "$SIG_JSON" | jq -r .address)" \
--signature "$(echo "$SIG_JSON" | jq -r .signature)" \
--public-key "$(echo "$SIG_JSON" | jq -r .pubKey)" \
--message "$MSG"
# use the session
bb api misc fetch-api-keys --body '{}' --with-session
bb auth whoami
```
The API key is the app scope and is required on every call. Create one at [bitbadges.io/developer](https://bitbadges.io/developer) and store it with `bb settings set apiKey "$BITBADGES_API_KEY"`. The session cookie is the user scope and is required on Full Access routes (anything that mutates an account, manages keys, or publishes signed data).
## Subcommands
| Command | Purpose |
| --- | --- |
| `auth login` | Fetch a challenge (or reuse a pending one), post the signature, store the cookie, mark the address active |
| `auth challenge` | Print a challenge for two-step or external-signer flows |
| `auth verify` | Two-step counterpart to `login` (same flags, requires `--signature`) |
| `auth status` | List stored sessions; `--check` revalidates server-side |
| `auth use ` | Set the active address for a network |
| `auth whoami` | Print the active address for the resolved network (non-zero exit if none) |
| `auth logout` | Sign out server-side and remove the local record |
| `auth path` | Print the path of the auth store |
Every subcommand accepts the [network flags](README.md#network-flags). Sessions are stored per network; a mainnet and a local session for the same address coexist.
### login and verify
| Flag | Description |
| --- | --- |
| `--address ` | Required. A `bb1` address for Cosmos, a `0x` address for ETH. |
| `--signature ` | Hex or base64 signature over the challenge. Required unless `--browser`. |
| `--public-key ` | Compressed pubkey. Required for Cosmos signatures; ignored for ETH; captured from the wallet with `--browser`. |
| `--message `, `--message-file ` | The exact challenge text (`-` for stdin). Defaults to the saved pending entry. |
| `--browser` | Sign in the browser through the [sign bridge](deploy.md#sign-bridge). Mutually exclusive with `--signature`. |
| `--frontend-url `, `--no-open`, `--port `, `--timeout ` | Bridge options (default timeout 300, max 1800) |
Exit `0` when the cookie is stored, `1` on a rejected signature or network failure.
### challenge
```bash
bb auth challenge --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --no-save-pending
```
The message is the Sign In with BitBadges text with a fresh nonce (mainnet output):
```json
{
"ok": true,
"data": {
"message": "bitbadges.io wants you to sign in with your Cosmos account:\nbb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d\n\nSign this message only if prompted by a trusted party. The signature of this message can be used to authenticate you on BitBadges. By signing, you agree to the BitBadges privacy policy and terms of service.\n\nURI: https://bitbadges.io\nVersion: 1\nChain ID: 1\nNonce: eesRtnq0lSLSuthUx\nIssued At: 2026-09-07T02:56:49.365Z\nResources:\n- bitbadges://scope/full-access",
"nonce": "eesRtnq0lSLSuthUx"
},
"warnings": [],
"error": null
}
```
The BitBadges API binds each challenge nonce to the cookie it sets on `getChallenge`. `auth challenge` saves that cookie as a pending entry (5-minute TTL) so `auth login` can replay it. Without it, verify fails with `No sign-in request found`. `--no-save-pending` skips the save; use it only if you will re-fetch the challenge yourself.
### status, use, whoami, logout, path
```bash
bb auth status --all --check
bb auth use bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue --local
bb auth whoami
bb auth logout --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d
bb auth logout --all
bb auth path # /home/you/.bitbadges/auth.json
```
`status` prints one line per session, for example `mainnet bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d Cosmos expires=2026-09-13T02:56:49.365Z (valid) [server: signed-in]`, and the envelope `{ "ok": true, "data": { "sessions": [] }, "warnings": [], "error": null }` when nothing is stored. `logout` removes the local record even if the server call fails.
## Manual Paste-In
```bash
bb auth challenge --address 0x0bc63cfe31d5218eb414b142c799e20964a54a1a
# sign the printed message in the wallet's "Sign Message" dialog, then paste the hex signature
bb auth login --address 0x0bc63cfe31d5218eb414b142c799e20964a54a1a --signature "$SIG"
```
ETH addresses are detected by the `0x` prefix and need no `--public-key`.
## Storage
`~/.bitbadges/auth.json`, mode `0600`:
```json
{
"version": 1,
"networks": {
"mainnet": {
"active": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"sessions": {
"bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d": {
"address": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"nativeAddress": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"chain": "Cosmos",
"cookieName": "bitbadges",
"cookieValue": "s%3A4f1c9e2a7b3d.QmZ8rT2vLx0pW5nK9yH3cJ6uB1eD4sF7gA",
"scopes": [{ "scopeName": "Full Access" }],
"createdAt": 1788739200000,
"expiresAt": 1789344000000,
"indexerUrl": "https://api.bitbadges.io/api/v0"
}
},
"pending": {}
},
"testnet": {},
"local": {}
}
}
```
## Behavior
- Scope is always `Full Access`; the API hard-codes the scope set today.
- Sessions roll on use. The API resets the expiry to now plus 7 days on every authenticated request, and the CLI writes the new `expiresAt` back. A long-lived agent that makes any request inside that window never needs to log in again.
- The pending challenge expires after 5 minutes. After that, `login` fetches a fresh challenge that will not match a signature made over the old one.
- The store is portable. Copy `auth.json` to another machine and `bb api --with-session` works there; keep it `0600`.
## Related
- [API](api.md)
- [Deploy](deploy.md#sign-bridge)
- [Chain Commands](chain.md#sign-arbitrary)
- [Sign In with BitBadges](../api/sign-in/README.md)
## File: cli/build.md
---
description: "Reference for every bb build subcommand, the shared flags, metadata modes, JSON input, and the walkthrough transfer builder."
---
# bb build
`bb build ` turns a few flags into a complete, ready-to-sign transaction message. Use it when a template fits; pipe the result into `bb check`, `bb preview`, or `bb deploy`.
For invoice substandards, use `bb build payment-request-v2 --list-examples`, `--example `, and `--schema` to discover editable terms. The [agent workflow](agent-workflow.md) covers exact JSON roundtrips and metadata precedence; [Invoices and Payment Links](../standards/payments.md) documents the resulting approvals and tracking. `bb pay-requests build-v2` is an alias; the older `bb pay-requests build` continues to build legacy requests.
## Example
```bash
bb build vault --backing-coin USDC \
--name "Demo Vault" --symbol vUSDC \
--image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/vault.png --description "USDC-backed vault" \
--daily-withdraw-limit 1000 --explain
# build, then review and sign in the browser
bb build vault --backing-coin USDC --uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json | bb preview - --open
# build and broadcast with a connected wallet
bb build vault --backing-coin USDC --uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json \
--manager bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --browser
```
The first command prints the message inside the envelope (485 lines for a vault; the head, with `--creator bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d`, is):
```json fold=13-35
{
"ok": true,
"data": {
"typeUrl": "/tokenization.MsgCreateCollection",
"value": {
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"validTokenIds": [
{
"start": "1",
"end": "1"
}
],
"collectionPermissions": {
"canDeleteCollection": [
{
"permanentlyPermittedTimes": [],
"permanentlyForbiddenTimes": [
{
"start": "1",
"end": "18446744073709551615"
}
]
}
],
"canArchiveCollection": [
{
"permanentlyPermittedTimes": [],
"permanentlyForbiddenTimes": [
{
"start": "1",
"end": "18446744073709551615"
}
]
}
]
}
}
}
}
```
The walkthrough that explains the output is in [Create a Collection](../guides/create-a-collection.md). This page is the flag reference.
{% hint style="info" %}
Ask your agent. Every template here has a skill (`get_skill_instructions`) and the session tools behind it: "Build a USDC vault called Demo Vault with symbol vUSDC and a 1000 daily withdraw limit, then give me the review link."
{% endhint %}
## Subcommands
The subcommand list comes from `bitbadgesjs-sdk/src/cli/commands/build.ts`. Collection builders emit `MsgCreateCollection` for a new collection (they assemble it as `MsgUniversalUpdateCollection` and narrow it at the emit boundary). Approval builders emit `MsgSetOutgoingApproval` or `MsgSetIncomingApproval`.
| Subcommand | Emits | Purpose |
| --- | --- | --- |
| `vault` | collection | IBC-backed vault token with withdraw limits, 2FA gating, emergency recovery |
| `smart-token` | collection | IBC-backed Smart Token: deposit backing coin, withdraw by burning |
| `subscription` | collection | Recurring subscription with tiers and payouts |
| `bounty` | collection | Escrowed bounty with a verifier and recipient |
| `payment-request` | collection | No-escrow request that a payer approves and pays in one action |
| `crowdfund` | collection | Funding goal with a deadline |
| `auction` | collection | Bidding window plus accept window |
| `product-catalog` | collection | One token ID per product with price and supply |
| `prediction-market` | collection | Binary YES/NO market with a resolver |
| `credit-token` | collection | Prepaid credits bought with a payment coin |
| `custom-2fa` | collection | Collection of short-lived 2FA tokens |
| `address-list` | `MsgCreateAddressLists` | On-chain address list (not a collection) |
| `intent` | outgoing approval | OTC swap intent on the Intent Exchange |
| `listing` | outgoing approval | Orderbook listing for one token |
| `bid` | incoming approval | Orderbook bid for one token or a whole collection |
| `pm-sell-intent` | outgoing approval | Sell outcome tokens on a prediction market |
| `pm-buy-intent` | incoming approval | Buy outcome tokens on a prediction market |
| `send` | `cosmos.bank.v1beta1.MsgSend` | Plain coin transfer that bypasses tokenization |
| `transfer` | `MsgTransferTokens` | Guided transfer of existing tokens |
Ten standards groups expose the same builder as `bb build` (for example `bb auctions build` is `bb build auction`). Same flags, same output. `nfts` and `dynamic-stores` have no build alias.
## Shared Flags
Every subcommand accepts these. `--help` renders them under `Metadata`, `Output`, `Network`, `Builder`, and `Deploy` headings after the per-command flags.
| Flag | Group | Description |
| --- | --- | --- |
| `--uri ` | Metadata | Mode 1: pre-hosted metadata URI. Skips the field flags. |
| `--name `, `--image `, `--description ` | Metadata | Mode 2: inline metadata serialized into the on-chain `customData` field |
| `--condensed` | Output | Compact JSON |
| `--output-file ` | Output | Write to a file instead of stdout |
| `--json ` | Output | Pass all params as one JSON object (file, inline, or `-` for stdin). Overrides individual flags. |
| `--explain` | Output | Print a plain-English explanation to stderr in addition to the auto-review |
| `--creator ` | Builder | Creator/sender address (`bb1` or `0x` form) |
| `--manager ` | Builder | Collection manager (`bb1` form) |
| `--simulate` | Builder | Also call the simulate endpoint and render gas and net balance changes (needs an [API key](../api/README.md#api-keys)). Different from `bb deploy --dry-run`, which simulates and exits. |
| `--events` | Builder | With `--simulate`, dump the full events array instead of the count |
| `--network`, `--mainnet`, `--testnet`, `--local`, `--url`, `--api-key` | Network | See [CLI](README.md#network-flags) |
| `--burner`, `--browser`, `--sign-only`, `--frontend-url`, `--no-open`, `--timeout`, `--expected-address`, `--fund`, `--fee`, `--fee-denom`, `--gas`, `--new`, `--reuse`, `--non-interactive`, `--poll-timeout`, `--port ` | Deploy | Broadcast inline instead of emitting JSON. See [Deploy](deploy.md). |
`--json-only` still works as a deprecated alias for `--quiet`.
### Metadata Modes
Every metadata-bearing builder accepts exactly one of two modes per entity:
1. `--uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json` when you already host the JSON.
2. `--name` + `--image` + `--description`. The CLI serializes them into the on-chain `customData` field. The BitBadges API, SDK, and site parse `customData` on read and show it as the resolved metadata, so no IPFS account is needed.
Approvals are text-only: `--name` + `--description`, no image. The CLI errors if neither mode is complete; there are no placeholder defaults. On-chain shape: [Collections](../token-standard/concepts/collections.md).
For `payment-request-v2 --json`, `--uri` replaces inline metadata from the JSON. Supplying any inline metadata flag removes the JSON's `uri` and merges the supplied inline fields with any remaining inline fields in the JSON; the resulting name, image, and description must be complete. Combining `--uri` with inline metadata flags is rejected. Other JSON builder handlers take metadata from the JSON rather than merging these flags into it.
### Denoms and Amounts
`--denom` style flags accept a symbol (`BADGE`, `USDC`, `ATOM`, `OSMO`) or a canonical denom (`ubadge`, `ibc/E1116484B327AEE59CDC3DA73D319834781A13DB2A7DFC1F38A30CD45ABF58B8`). Amounts are display units when the denom is a symbol and base units when it is a raw denom. `--base-units` forces base units. `USDC` resolves to the canonical Injective-routed denom; `USDC.n` is the legacy denom. See [Supported Denoms](../chain/supported-denoms.md).
Durations accept `daily`, `monthly`, `annually`, shorthand such as `30d`, `24h`, `5m`, or ms-since-epoch.
### JSON Input
```bash
bb build vault --backing-coin USDC --json '{"backingCoin":"USDC","uri":"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json"}'
bb build vault --backing-coin USDC --json ./params.json
echo '{"backingCoin":"USDC","uri":"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json"}' | bb build vault --backing-coin USDC --json -
```
JSON uses the matching SDK builder's parameter names. Required CLI flags still apply, even when the JSON includes the corresponding field. In the vault examples, `--backing-coin` satisfies CLI parsing while `backingCoin` in the JSON supplies the builder value. Keep builder parameters and metadata in the JSON; do not assume command-line flags override them. The V2 payment metadata exception is described above.
## Collection Builders
### vault
```bash
bb build vault --backing-coin USDC --symbol vUSDC --uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json \
--daily-withdraw-limit 1000 --require-2fa 84 --emergency-recovery bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf
```
| Flag | Required | Description |
| --- | --- | --- |
| `--backing-coin ` | yes | `USDC`, `BADGE`, `ATOM`, `OSMO` |
| `--symbol ` | no | Display symbol, for example `vUSDC` |
| `--daily-withdraw-limit ` | no | Max daily withdrawal in display units |
| `--require-2fa ` | no | Custom-2FA collection that gates withdrawals |
| `--emergency-recovery ` | no | Recovery address for emergency migration |
### smart-token
```bash
bb build smart-token --backing-coin USDC --symbol sUSDC --uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json \
--tradable --ai-agent-vault
```
| Flag | Required | Description |
| --- | --- | --- |
| `--backing-coin ` | yes | `USDC`, `BADGE`, `ATOM`, `OSMO` |
| `--symbol ` | no | Display symbol (default `v`) |
| `--tradable` | no | Add the "Liquidity Pools" standard tag |
| `--ai-agent-vault` | no | Add the "AI Agent Vault" standard tag (display hint) |
| `--allow-forceful-transfers` | no | Allow forceful post-mint transfers (off by default) |
Vault collections are Smart Tokens with the `cosmosCoinBackedPath` invariant. `bb build smart-account` was the old name.
### subscription
```bash
bb build subscription --interval monthly --price 10 --denom USDC \
--recipient bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --tiers 3 --transferable \
--uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json
```
| Flag | Required | Description |
| --- | --- | --- |
| `--interval ` | yes | `daily`, `monthly`, `annually`, or shorthand such as `30d` |
| `--price ` | no | Price per interval in display units; use with `--denom` and `--recipient` |
| `--denom ` | no | Payment coin |
| `--recipient ` | no | Payout address |
| `--payouts ` | no | Several payouts: `[{"recipient":"bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d","amount":"7","denom":"USDC"},{"recipient":"bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf","amount":"3","denom":"USDC"}]` |
| `--tiers ` | no | Number of tiers (default `1`) |
| `--transferable` | no | Allow post-mint transfers between users |
| `--updatable-mint` | no | Keep the mint (faucet) approval editable so the price can change later. Off by default: the faucet is locked forever, which is what `bb check` requires to pass. Opting in makes `bb check` fail with one critical finding, on purpose |
There is no `bb build recurring-payment`. A subscriber's recurring approval derives from the live collection; use `bb subscriptions subscribe` or `bb subscriptions enable-renewal`.
### bounty
```bash
bb build bounty --amount 500 --denom USDC --verifier bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf \
--recipient bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue --submitter bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d \
--expiration 30d --uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json
```
| Flag | Required | Description |
| --- | --- | --- |
| `--amount ` | yes | Bounty amount in display units |
| `--denom ` | yes | Coin |
| `--verifier ` | yes | Address that accepts or denies |
| `--recipient ` | yes | Address paid on accept |
| `--submitter ` | yes | Address refunded on deny or expiry (usually the creator) |
| `--expiration ` | no | Default `30d` |
### payment-request
```bash
bb build payment-request --amount 10 --denom USDC \
--payer bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --recipient bb18cad7xxsk3drvwdxeasc3wqn2plftpzq2tsrsr --expiration 30d \
--name "Service charge" --image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/invoice.png \
--context "Agent requests payment for the September report under the approved budget of 100 USDC per month."
```
| Flag | Required | Description |
| --- | --- | --- |
| `--amount ` | yes | Payment amount in display units |
| `--denom ` | yes | Coin |
| `--payer ` | yes | The human approver |
| `--recipient ` | yes | Agent or merchant |
| `--expiration ` | no | Default `30d` |
| `--context ` | no | Rationale shown to the payer at approval time (100+ characters recommended). Used as the description when `--description` is not set. |
The inverse of `bounty`: no escrow up front. The payer approves and pays from their own wallet in one action.
### crowdfund
```bash
bb build crowdfund --goal 10000 --denom USDC --crowdfunder bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --deadline 30d \
--uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json
```
| Flag | Required | Description |
| --- | --- | --- |
| `--goal ` | yes | Funding goal in display units |
| `--denom ` | yes | Coin |
| `--crowdfunder ` | no | Receives funds on success and holds the progress balance that gates success or refund. Falls back to `--creator`; errors if neither is set. |
| `--deadline ` | no | Default `30d` |
### auction
```bash
bb build auction --bid-deadline 7d --accept-window 7d --seller bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d \
--name "Rare Item" --description "Limited edition" --image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/1.png
```
| Flag | Required | Description |
| --- | --- | --- |
| `--bid-deadline ` | no | Bidding window (default `7d`) |
| `--accept-window ` | no | Accept window after the bid deadline (default `7d`) |
| `--seller ` | no | Only this address can accept the winning bid (default `--creator`) |
### product-catalog
```bash
bb build product-catalog --store-address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d \
--uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json \
--products '[{"name":"Widget","price":25,"denom":"USDC","maxSupply":100},{"name":"Pass","price":5,"denom":"USDC","burn":true}]'
```
| Flag | Required | Description |
| --- | --- | --- |
| `--products ` | yes | Array, one object per SKU |
| `--store-address ` | yes | Default payment recipient; a product's `storeAddress` overrides it |
Product fields: `name` (required), `price` (display units, required), `denom` (required), `storeAddress` (optional recipient override; BitBadges or EVM account), `maxSupply` (omit or `0` for unlimited), `burn` (`true` burns on purchase), `uri`, `image`, `description`. One token ID per product; each product becomes a "Purchase" approval. The builder freezes products, prices, recipients, stock limits and metadata at creation; publish a new catalog for different terms.
### prediction-market
```bash
bb build prediction-market --verifier bb1zc268nctj8xwslgw7q22cahs6k4y048agr6fvf --denom USDC \
--name "Will testnet return by 2027?" --description "Resolves YES if the BitBadges testnet is back online before 2027-01-01." \
--image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/market.png
```
| Flag | Required | Description |
| --- | --- | --- |
| `--verifier ` | no | Market resolver. `--resolver` is an alias. |
| `--denom ` | no | Payment coin (default `USDC`) |
### credit-token
```bash
bb build credit-token --payment-denom USDC --recipient bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d \
--symbol CREDIT --tokens-per-unit 100 --uri ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/collection.json
```
| Flag | Required | Description |
| --- | --- | --- |
| `--payment-denom ` | no | Payment coin. `--denom` is an alias. |
| `--recipient ` | yes | Payment recipient |
| `--symbol ` | no | Default `CREDIT` |
| `--tokens-per-unit ` | no | Token base units per multiplier; the current builder charges 1 payment-denomination base unit per multiplier (default `100`). See [Credit Token accounting](../standards/commerce.md). |
### custom-2fa
```bash
bb build custom-2fa --creator bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --name "Demo 2FA Token" \
--image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/2fa.png --description "Short-lived 2FA token" --burnable
```
| Flag | Required | Description |
| --- | --- | --- |
| `--creator ` | yes | Manager. Only this address may mint; the builder errors without it. |
| `--burnable` | no | Allow burning |
| `--transferable` | no | Allow post-mint transfers |
Issue tokens afterwards with `bb custom-2fa mint`. The lifetime is encoded at mint time, so a raw mint without that command produces tokens that never expire. See [Standards](standards.md#custom-2fa).
### address-list
```bash
bb build address-list --name "Allowlist" --description "Approved addresses" \
--image ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/list.png
```
Emits `MsgCreateAddressLists`. Takes the shared metadata flags only.
## Approval Builders
### intent
```bash
bb build intent --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --collection-id 81 \
--pay-denom USDC --pay-amount 100 --receive-denom BADGE --receive-amount 500 --expiration 30d
```
| Flag | Required | Description |
| --- | --- | --- |
| `--address ` | yes | Creator |
| `--collection-id ` | yes | Intent Exchange collection (mainnet `81`) |
| `--pay-denom`, `--pay-amount` | yes | What you send |
| `--receive-denom`, `--receive-amount` | yes | What you receive |
| `--expiration ` | no | Default `30d` |
Identical output to `bb intents create`.
### listing
```bash
bb build listing --address bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d --collection-id 1 --token-ids 4 \
--price 50 --denom USDC --max-sales 1 --expiration 30d
```
| Flag | Required | Description |
| --- | --- | --- |
| `--address ` | yes | Seller |
| `--collection-id ` | yes | Collection |
| `--token-ids ` | yes | One token ID (`4` or `4-4`). Orderbook listings are single-token; a real range errors. |
| `--price `, `--denom ` | yes | Asking price |
| `--max-sales ` | no | Default `1` |
| `--expiration ` | no | Default `30d` |
Identical output to `bb nfts list`.
### bid
```bash
bb build bid --address bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue --collection-id 1 --token-ids 4 --price 40 --denom USDC --expiration 7d
bb build bid --address bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue --collection-id 1 --price 40 --denom USDC # collection-wide
```
| Flag | Required | Description |
| --- | --- | --- |
| `--address