payment-request
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.
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")
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:
overrideFromWithApproverAddressmust be false. The chain default routes "from" to the initiator (the payer). Setting true would attempt to debit a non-existent escrow → tx fails.
- Critical:
- 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
- Agent (or merchant) creates the collection, no escrow funded.
- Payer sees the request in their dashboard / wallet (rationale, line items, amount).
- 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.
- 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 totransferTimes[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)
- Use per-field tools to initialize the collection
set_valid_token_ids: set [{ start: "1", end: "1" }]set_standards: set ["PaymentRequest"]set_invariants: set { noCustomOwnershipTimes: true, disablePoolCreation: true, noForcefulPostMintTransfers: true }- Do not call set_mint_escrow_coins, there's no escrow
add_preset_approval: pay only for All; pay + deny for a specific payer (oradd_approvalfor raw)set_permissions: freeze all permissionsset_collection_metadata: name + the rationale (≥100 chars recommended; mirror Stripe Link's bar)set_token_metadata: token 1 metadatavalidate_transaction: verify structure (verifyPaymentRequest enforces the no-escrow invariants)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.