bounty
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.
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.
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
-
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"
-
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)
-
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)
- Use per-field tools to initialize the collection
set_valid_token_ids: set [{ start: "1", end: "1" }]set_standards: set ["Bounty"]set_invariants: set { noCustomOwnershipTimes: true, disablePoolCreation: true }set_mint_escrow_coins: fund escrow with bounty amountadd_approvalx3: add accept, deny, expire approvalsadd_alias_path: ubounty alias (symbol BOUNTY, 1 decimal, 1:1 token ID 1)set_permissions: freeze all permissionsset_collection_metadata: name, description, imageset_token_metadata: token 1 metadatavalidate_transaction: verify structuresimulate_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.