PaymentRequest

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.

Category: Token Types

Summary

Required standards: ["PaymentRequest"]

  • 1 token ID (vehicle for approval engine β€” minted directly to burn)

  • 2 collection-level approvals: pay, deny

  • 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: [{ start: 1, end: expirationTimestamp }]; once that window closes neither can fire (no separate expire approval, since there's no escrow to refund)

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 Linkarrow-up-right'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)

2 Required Approvals

Both approvals share: Mint β†’ burn address, 1x token ID 1, maxNumTransfers = 1, overridesFromOutgoingApprovals=true, overridesToIncomingApprovals=true, transferTimes [{ start: "1", end: expirationTimestamp }]. NO votingChallenges β€” gating is via initiatedByListId, not voting.

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 (NOT "All" β€” gates approval to just the payer)

  • coinTransfers: [{ to: recipientAddress, overrideFromWithApproverAddress: false, overrideToWithInitiator: false, coins: [{ denom, amount }] }]

    • CRITICAL: overrideFromWithApproverAddress MUST be false. The chain default routes the coinTransfer's "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-*)

Payer rejects β†’ mint-to-burn β†’ no coin transfer. Records denial state for indexers/UIs.

Same shape as Pay but:

  • NO coinTransfers (or empty array)

  • Same initiatedByListId (payer)

  • Same transferTimes (concurrent with pay)

Why no expire branch?

Bounty has an expire approval to refund escrowed funds back to the submitter once the deadline passes. PaymentRequest has no escrow β€” if the payer doesn't act before expirationTimestamp, both approvals' time windows close and the request is simply un-actionable. There's nothing to refund and no terminal state to record on-chain. UIs detect "expired" by comparing the current time to transferTimes[0].end. Validators reject collections with more than 2 approvals.

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 and the request is implicitly expired. No on-chain action is needed to "finalize" expiration.

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 β€” expiration is implicit via the shared transferTimes[0].end. Bounty needs an expire branch to refund escrow; we don't have escrow.

  • 2 approvals instead of Bounty's 3

  • Same mint-to-burn vehicle, same frozen permissions

Creation Flow (CLI)

The --context flag populates the collection metadata description with the rationale shown to the payer at approval time. Aim for β‰₯100 chars (Stripe Link's bar) to force agents to justify the spend.

Creation Flow (Builder 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 x2 β€” pay, deny (or add_approval for raw)

  7. set_permissions β€” freeze all permissions

  8. set_collection_metadata β€” name + the rationale (β‰₯100 chars recommended)

  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 set initiatedByListId to "All" on pay/deny β€” that would let anyone approve. Lock to the specific payer's address.

  • DON'T forget the rationale in collection metadata description β€” it's what the human reads to decide. Aim for β‰₯100 chars (Stripe Link's bar) to force agents to justify the spend.

  • 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 so the payer's "no" is captured on-chain rather than being indistinguishable from "hasn't acted yet".

Consumers that want any payer-initiated payment can match Invoices; consumers that want the agent-payments artifact specifically should match PaymentRequest.

Why this exists

Stripe shipped link-cliarrow-up-right in April 2026: an agent CLI for one-shot, human-approved payment credentials backed by Stripe Link's wallet. Agents create a "spend request" with merchant info + β‰₯100-char rationale + amount, the user approves on their phone, the agent gets a one-time virtual card to pay the merchant.

PaymentRequest is the on-chain equivalent. The chain rails offer things Stripe Link cannot:

  1. Programmable allowances: a payer can grant the agent a BitBadges approval ("≀50 USDC/week, ≀10 USDC/tx, expires 30d") so in-envelope requests auto-approve and out-of-envelope requests wake the human. Stripe needs a whole policy engine for this; we get it from the existing approval primitive.

  2. Receipts as badges: the paid request optionally mints a "purchased X from Y" badge into the payer's collection. Audit trail is automatic.

  3. MPP / HTTP 402 interop: speak the same wire format as Machine Payments Protocolarrow-up-right and any MPP-compatible client (including Stripe's link-cli with credential_type: shared_payment_token) can pay BitBadges merchants, and BitBadges-native agents can pay anyone speaking MPP.

Last updated