# payment-request

<!-- generated by site/scripts/gen-skills.ts; do not hand-edit -->

# 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`.
