payment-protocol
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.
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:
add_preset_approval({
presetId: "payment-protocol.invoice",
params: {
approvalId: "invoice-1",
payerAddress: "bb1...",
payeeAddress: "bb1...",
amount: "<base units>",
denom: "<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:
{
"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" }]
}],
"overridesFromOutgoingApprovals": true,
"maxNumTransfers": {
"overallMaxNumTransfers": "1",
"amountTrackerId": "milestone-1-tracker",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
}],
"standards": ["ListView:Milestones"]
}{
"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
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 coinsRequired 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:
- Backing approvals (deposits): each depositing party gets a separate backing approval with amount caps
{
"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" } }
}
}-
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
-
Timeout approvals (fallbacks): use transferTimes to gate when fallbacks activate
{
"approvalId": "timeout-release",
"transferTimes": [{ "start": "DEADLINE_MS", "end": "18446744073709551615" }],
"approvalCriteria": { "overridesFromOutgoingApprovals": true }
}DEADLINE_MS = creation timestamp + timeout hours * 3600000
-
Verifier fee approvals: flat fee regardless of decision (neutral incentive). Two approvals: one gated by approve vote, one by deny vote.
-
Deposit return/forfeit: worker reclaims on success, poster takes on timeout. Natural mutex via balance depletion.
-
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
poster-deposit: poster backs USDC into escrowworker-deposit: worker deposits a bond (optional)release-on-completion: poster releases funds to workertimeout-refund: poster reclaims if worker ghosts (time-gated)dispute-resolution: arbitrator releases funds (vote-gated)worker-bond-return: worker reclaims bond on successworker-bond-forfeit: poster takes bond on timeoutpenalty-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
- 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.
- Permission locking: For agreements, lock
canUpdateCollectionApprovalsso terms are immutable. - Who pays gas?: The initiator pays gas. For mint-based, the payer initiates.
- Multiple currencies: A single collection can accept different IBC denoms in different approvals.
- Refunds: For coinTransfer-based, refunds require a separate approval. For escrow, use timeout-refund approvals.
- 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.