Skip to content

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.

Edit this page on GitHub

For agents

How agents read these docs