crowdfund
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.
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 setoverridesFromOutgoingApprovalsoroverridesToIncomingApprovals(both must be false or omitted). It relies ondefaultBalances.autoApproveSelfInitiatedOutgoingTransfers: truefor the outgoing side and on the burn destination for the incoming side. The deposit-refund / deposit-progress / success approvals are Mint-side and keepoverridesFromOutgoingApprovals: trueas the chain requires, withoverridesToIncomingApprovals: 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)
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)
{
"approvalId": "deposit-refund",
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "<DEADLINE_TIMESTAMP>" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"requireToEqualsInitiatedBy": true,
"coinTransfers": [{
"to": "Mint",
"coins": [{ "amount": "1", "denom": "<DEPOSIT_DENOM>" }]
}],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"allowAmountScaling": true,
"maxScalingMultiplier": "18446744073709551615"
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": { "overallMaxNumTransfers": "0" }
}
}{
"approvalId": "deposit-refund",
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "<DEADLINE_TIMESTAMP>" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"requireToEqualsInitiatedBy": true,
"coinTransfers": [{
"to": "Mint",
"overrideFromWithApproverAddress": false,
"overrideToWithInitiator": false,
"coins": [{ "amount": "1", "denom": "<DEPOSIT_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: trueensures the contributor receives their own refund token.allowAmountScaling: truelets contributors choose their deposit size, the coin payment and token amount scale together.
2. Deposit-Progress (paired: mints Token 2 to crowdfunder, no coinTransfer)
{
"approvalId": "deposit-progress",
"fromListId": "Mint",
"toListId": "<CROWDFUNDER_ADDRESS>",
"initiatedByListId": "All",
"tokenIds": [{ "start": "2", "end": "2" }],
"transferTimes": [{ "start": "1", "end": "<DEADLINE_TIMESTAMP>" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "2", "end": "2" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"allowAmountScaling": true,
"maxScalingMultiplier": "18446744073709551615"
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": { "overallMaxNumTransfers": "0" }
}
}{
"approvalId": "deposit-progress",
"fromListId": "Mint",
"toListId": "<CROWDFUNDER_ADDRESS>",
"initiatedByListId": "All",
"tokenIds": [{ "start": "2", "end": "2" }],
"transferTimes": [{ "start": "1", "end": "<DEADLINE_TIMESTAMP>" }],
"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)
{
"approvalId": "success-withdraw",
"fromListId": "Mint",
"toListId": "<BURN_ADDRESS>",
"initiatedByListId": "<CROWDFUNDER_ADDRESS>",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "<DEADLINE + 1>", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [{
"to": "<CROWDFUNDER_ADDRESS>",
"overrideFromWithApproverAddress": true,
"coins": [{ "amount": "1", "denom": "<DEPOSIT_DENOM>" }]
}],
"mustOwnTokens": [{
"tokenIds": [{ "start": "2", "end": "2" }],
"amountRange": { "start": "<GOAL_AMOUNT>", "end": "18446744073709551615" },
"ownershipCheckParty": "<CROWDFUNDER_ADDRESS>"
}],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"allowAmountScaling": true,
"maxScalingMultiplier": "18446744073709551615"
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": { "overallMaxNumTransfers": "1" }
}
}{
"approvalId": "success-withdraw",
"fromListId": "Mint",
"toListId": "<BURN_ADDRESS>",
"initiatedByListId": "<CROWDFUNDER_ADDRESS>",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "<DEADLINE + 1>", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [{
"to": "<CROWDFUNDER_ADDRESS>",
"overrideFromWithApproverAddress": true,
"overrideToWithInitiator": false,
"coins": [{ "amount": "1", "denom": "<DEPOSIT_DENOM>" }]
}],
"mustOwnTokens": [{
"collectionId": "0",
"tokenIds": [{ "start": "2", "end": "2" }],
"amountRange": { "start": "<GOAL_AMOUNT>", "end": "18446744073709551615" },
"ownershipCheckParty": "<CROWDFUNDER_ADDRESS>"
}],
"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)
{
"approvalId": "refund",
"fromListId": "!Mint",
"toListId": "<BURN_ADDRESS>",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "<DEADLINE + 1>", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [{
"overrideFromWithApproverAddress": true,
"overrideToWithInitiator": true,
"coins": [{ "amount": "1", "denom": "<DEPOSIT_DENOM>" }]
}],
"mustOwnTokens": [{
"tokenIds": [{ "start": "2", "end": "2" }],
"amountRange": { "start": "0", "end": "<GOAL_AMOUNT - 1>" },
"ownershipCheckParty": "<CROWDFUNDER_ADDRESS>"
}],
"predeterminedBalances": {
"incrementedBalances": {
"startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
"allowAmountScaling": true,
"maxScalingMultiplier": "18446744073709551615"
},
"orderCalculationMethod": { "useOverallNumTransfers": true }
},
"maxNumTransfers": {
"overallMaxNumTransfers": "18446744073709551615"
}
}
}{
"approvalId": "refund",
"fromListId": "!Mint",
"toListId": "<BURN_ADDRESS>",
"initiatedByListId": "All",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "<DEADLINE + 1>", "end": "18446744073709551615" }],
"approvalCriteria": {
"overridesFromOutgoingApprovals": true,
"overridesToIncomingApprovals": true,
"coinTransfers": [{
"to": "",
"overrideFromWithApproverAddress": true,
"overrideToWithInitiator": true,
"coins": [{ "amount": "1", "denom": "<DEPOSIT_DENOM>" }]
}],
"mustOwnTokens": [{
"collectionId": "0",
"tokenIds": [{ "start": "2", "end": "2" }],
"amountRange": { "start": "0", "end": "<GOAL_AMOUNT - 1>" },
"ownershipCheckParty": "<CROWDFUNDER_ADDRESS>"
}],
"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)
set_valid_token_ids: set [{ start: "1", end: "2" }]set_standards: set ["Crowdfund"]set_invariants: set { noCustomOwnershipTimes: true }add_approvalx4: deposit-refund, deposit-progress, success, refundset_collection_metadata: name, description, imageset_token_metadatax2: Token 1 (Refund), Token 2 (Progress)set_permissions: preset "fully-immutable"validate_transaction: verify structuresimulate_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