# Agent payment

An agent pays with the partner's prepaid wallet. It never receives card data, never calls Stripe
directly, and never creates a payment method. The principal funds the wallet; the agent spends
within a mandate.

## Live payment path

1. Read `GET /v1/capabilities` and `GET /v1/agent-manifest.json` for flags, limits and URLs.
2. Read `GET /v1/catalog` and choose an available `offerId`.
3. Check the quote or expected total against the mandate.
4. Call `POST /v1/orders` with a stable `reference`, a unique `Idempotency-Key` and either a
   `quoteId` or the one-call `currency`, `items` and `expectedTotalMinor`.
5. Poll `GET /v1/orders/{purchaseId}` until `completed`, `failed` or the target
   `partially_fulfilled`.
6. Redeem with `POST /v1/orders/{purchaseId}/redeem` into an entitled channel.

The wallet is debited inside the purchase transaction. A failed order creates no purchase and no
debit. A processing order is already paid and will be fulfilled asynchronously.

## Current and target states

| Behaviour | Current | Target |
| --- | --- | --- |
| Insufficient wallet credit | `402 insufficient_funds` | `402 payment_required` with `funding_url` |
| Order above approval threshold | `409 approval_required`; no order and no debit | Add a human approval exchange |
| Agent per-order / daily / rolling limits | Enforced before debit | Same |
| Bulk line and unit caps | Principal 200 lines / 5,000 units enforced | Agent 50 lines / 500 units unless raised |
| Order statuses | `processing`, `completed`, `failed` | Adds `partially_fulfilled` |
| Funding an agent | Principal funds the wallet | A capped off-session mandate may be added |

Agent caps and threshold enforcement are **Current**. Handle `agent_budget`, `agent_inactive` and
`approval_required`; do not retry an approval failure as a transient error.

## Budget check before ordering

The agent must not rely on the server to enforce its budget yet. Apply all three limits and the
approval threshold before calling the order endpoint:

```ts
type AgentMandate = {
  perOrderLimitMinor: number;
  dailyLimitMinor: number;
  rollingLimitMinor: number;
  approvalThresholdMinor: number;
};

export function decidePurchase(input: {
  mandate: AgentMandate;
  totalMinor: number;
  spentTodayMinor: number;
  spentRollingMinor: number;
}) {
  const { mandate, totalMinor } = input;
  if (totalMinor > mandate.perOrderLimitMinor) return "deny_per_order";
  if (input.spentTodayMinor + totalMinor > mandate.dailyLimitMinor) return "deny_daily";
  if (input.spentRollingMinor + totalMinor > mandate.rollingLimitMinor) return "deny_rolling";
  if (totalMinor >= mandate.approvalThresholdMinor) return "request_approval";
  return "allow";
}
```

Keep the counters in durable storage keyed by agent and UTC day. Do not keep them only in process
memory across restarts.

## One-call payment

```http
POST /v1/orders
Idempotency-Key: agent-2026-11-04-0001
```

```json
{
  "reference": "field-kit-2026-11-04-0001",
  "currency": "USD",
  "expectedTotalMinor": 1618,
  "items": [
    { "offerId": "11111111-1111-4111-8111-111111111111", "quantity": 2 }
  ]
}
```

`expectedTotalMinor` is the agent's ceiling check. If the fresh total differs, the API returns
`409 price_changed` with the new `totalMinor` and a fresh quote, and does not charge. The agent must
not retry with a raised total unless the mandate covers it.

## Quote-based payment

Use this when a human reviews the total or the agent needs a stable, inspectable object before
purchase:

```http
POST /v1/quotes
```

```json
{ "currency": "USD", "items": [{ "offerId": "11111111-1111-4111-8111-111111111111", "quantity": 2 }] }
```

The quote lasts 15 minutes. Review `expiresAt`, `currency`, `totalMinor` and `capped`. If the
decision takes longer, request a new quote rather than extending the old one.

## Recovery rules

### 402 insufficient_funds or payment_required

- Do not retry the order.
- `insufficient_funds`: report the shortfall to the principal and stop.
- `payment_required`: surface `funding_url` to the principal and stop.
- Never ask for card details, never call Stripe, and never create a second funding session.
- Resume only after the wallet shows available credit.

### 409 price_changed

- Do not retry with the same expected total.
- Read the returned `totalMinor` and quote.
- If the new total is within the mandate, make a new decision and a new order with a new
  idempotency key. If outside the mandate, stop and escalate.

### 409 quote_expired

- Create a new quote.
- Do not reuse the expired quote id.

### 429 rate_limited or quota_exceeded

- Read `Retry-After`. Wait, add jitter, and retry with the same idempotency key and body.
- If the daily quota is exhausted, stop and report the reset time.
- Do not open a second key to evade a quota.

### 403 ip_not_allowed

- Stop. The egress address is not on the key allowlist.
- Do not rotate the key or the allowlist yourself.

### 500 internal_error

- Check `GET /v1/orders/{purchaseId}` or `GET /v1/orders` before retrying.
- Retry only with the same idempotency key after confirming no order or charge exists.

## Funding authority

- A catalogue or order scope does not grant funding authority.
- Do not give an agent `wallet:fund` unless the principal explicitly delegates top-ups.
- Do not raise the agent budget, approval threshold or expiry.
- Do not create another agent or key.
- Do not choose an arbitrary payment source. The wallet currency must match the quote currency.

## Redemption and secrets

An agent may receive LPA text or QR SVG only in a server-side process that writes to the
principal's entitled delivery channel. It must not print, log, cache, forward or summarise the
credential in model output. If the delivery channel is not known, stop before redeeming.

## Related

- [Agent identities](/docs/agent-identities) for scopes and keys.
- [Approval flow](/docs/approval-flow) for threshold decisions.
- [Orders](/docs/orders) for statuses and idempotency.
- [Wallet](/docs/wallet) for funding and balances.
