> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dakota.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# x402 Quickstart

> Enable x402 on an agent, fund its wallet, set a budget, and pay a seller's 402

This walkthrough takes an existing hosted payment agent to a paid x402 request. It assumes an `active` hosted agent (see the [Agentic Payments quickstart](/documentation/agentic-payments/quickstart#1-create-a-hosted-payment-agent)) and an API key.

<Info>
  **Beta.** x402 is in early access. Run this walkthrough against the [sandbox](/documentation/testing) on Base Sepolia.
</Info>

## 1. Enable x402 on the agent

This creates the agent's [x402 wallet](/documentation/agentic-payments/x402#the-x402-wallet), the one wallet it pays x402 from.

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/payment-agents/2vWxAgent0000000000000000000/x402 \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)"
```

```json theme={null}
{
  "payment_agent_id": "2vWxAgent0000000000000000000",
  "status": "active",
  "wallet_id": "2vWxWallet000000000000000000",
  "address": "0x8982a181fC35d5A8AcA08fD41cA4B4A432ea6E58"
}
```

The call is idempotent: calling it again returns the same wallet. If it stopped part-way, `status` is `provisioning` and calling it again completes it. `GET /payment-agents/{id}/x402` returns the same object.

| Response | Meaning                                                                       |
| -------- | ----------------------------------------------------------------------------- |
| `400`    | The agent is not hosted, or not `active`                                      |
| `409`    | Another enable call for this agent is in progress. Retry shortly.             |
| `404`    | Agentic payments are not enabled for your client, or the agent does not exist |

A frozen customer cannot enable x402.

## 2. Fund the wallet

Send USDC to `address` on the network the agent will pay on. For the sandbox, that's USDC on Base Sepolia (`0x036CbD53842c5426634e7929541eC2318f3dCF7e`).

The wallet only ever needs USDC. Payments are signed authorizations that the seller's facilitator submits and pays gas for, so the wallet never needs ETH.

## 3. Set a budget

Create an [x402 mandate](/documentation/agentic-payments/x402/budgets-and-holds). This one lets the agent pay up to \$0.50 per call and \$2.00 per hour in total, only for APIs under `*.marketpulse.example`:

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/payment-agents/2vWxAgent0000000000000000000/x402/mandates \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "asset": "USDC",
    "network": "base-sepolia",
    "max_per_call": "500000",
    "max_per_window": "2000000",
    "window_seconds": 3600,
    "payee_policy": {
      "mode": "domain_allowlist",
      "domains": ["*.marketpulse.example"]
    }
  }'
```

Amounts are **atomic units**. USDC has 6 decimals, so `"500000"` is \$0.50.

```json theme={null}
{
  "id": "2vWxX402Mandate000000000001",
  "agent_id": "2vWxAgent0000000000000000000",
  "wallet_id": "2vWxWallet000000000000000000",
  "asset": "USDC",
  "network": "base-sepolia",
  "max_per_call": "500000",
  "max_per_window": "2000000",
  "window_seconds": 3600,
  "max_calls_per_window": 0,
  "payee_policy": { "mode": "domain_allowlist", "domains": ["*.marketpulse.example"] },
  "valid_from": "2026-09-08T00:00:00Z",
  "valid_until": "2026-10-08T00:00:00Z",
  "revoked_at": null,
  "window_committed": "0",
  "window_calls": 0
}
```

## 4. Pay the 402

Call the seller. An unpaid request comes back as `402 Payment Required` with the seller's terms:

* **x402 v1** sellers put the terms in the JSON body: `{"x402Version": 1, "accepts": [ ... ]}`.
* **x402 v2** sellers put them, base64-encoded, in the `PAYMENT-REQUIRED` response header.

Pick one entry from `accepts` and forward it **verbatim** as `payment_requirements`. Pass the seller's `x402Version` as `x402_version`, and the URL you're paying for as `resource_url`:

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/payment-agents/2vWxAgent0000000000000000000/x402/signatures \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "x402_version": 1,
    "resource_url": "https://api.marketpulse.example/v1/reports/copper-spot",
    "payment_requirements": {
      "scheme": "exact",
      "network": "base-sepolia",
      "maxAmountRequired": "100000",
      "resource": "https://api.marketpulse.example/v1/reports/copper-spot",
      "payTo": "0x94aE0f8B9F3c2A1d5E6b7C8D9e0F1a2B3c4D5E6F",
      "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "maxTimeoutSeconds": 120,
      "extra": { "name": "USDC", "version": "2" }
    }
  }'
```

```json theme={null}
{
  "payment_header_name": "X-PAYMENT",
  "payment_header": "eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiYmFzZS1zZXBvbGlhIn0",
  "signature": "0x9f2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b1c001c",
  "authorization_id": "2vWxX402Hold000000000000001",
  "mandate_id": "2vWxX402Mandate000000000001",
  "payer": "0x8982a181fC35d5A8AcA08fD41cA4B4A432ea6E58",
  "pay_to": "0x94aE0f8B9F3c2A1d5E6b7C8D9e0F1a2B3c4D5E6F",
  "value": "100000",
  "nonce": "0x3f1c8a5b2d7e4906c1a3f85b2e6d04971c8a5b3f2d7e4906c1a3f85b2e6d0497",
  "valid_before": "2026-09-08T00:02:00Z",
  "window_committed": "100000"
}
```

Retry the seller's request with the header named by `payment_header_name`, set to `payment_header`:

```bash theme={null}
curl https://api.marketpulse.example/v1/reports/copper-spot \
  -H "X-PAYMENT: eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiYmFzZS1zZXBvbGlhIn0"
```

The two protocol versions differ only in packaging. The signature underneath is the same:

|                                | x402 v1              | x402 v2                   |
| ------------------------------ | -------------------- | ------------------------- |
| Seller's terms                 | JSON body            | `PAYMENT-REQUIRED` header |
| Price field                    | `maxAmountRequired`  | `amount`                  |
| Network                        | `base-sepolia`       | `eip155:84532`            |
| `x402_version` to send         | `1` (the default)    | `2`                       |
| `payment_header_name` returned | `X-PAYMENT`          | `PAYMENT-SIGNATURE`       |
| Seller's receipt               | `X-PAYMENT-RESPONSE` | `PAYMENT-RESPONSE`        |

Either network spelling is accepted, and a mandate covers the seller in both.

<Warning>
  **Treat `payment_header` like cash.** Anyone holding it can settle the payment until `valid_before`. Send it only to the seller whose 402 you forwarded, and don't log it.
</Warning>

### Retrying safely

Every POST takes `X-Idempotency-Key`, and on the signing endpoint it does real work: **one key mints at most one payment authorization.** Reuse the same key and you get the same authorization back — same `nonce`, same window — signed again, and it is charged against the budget once. So a request that times out is safe to retry under the same key, and only one of the two can ever settle.

Reuse a key for a *different* payment and the call is refused with `409` and nothing is signed:

| `409` when                                                              | What to do                                                            |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------- |
| the key was used for a different payee, amount, token, network or payer | use a new key; this is a different payment                            |
| the authorization it minted has expired or was released                 | use a new key; nothing cashable is left                               |
| the payment it minted has already **settled**                           | **do not retry** — the money moved. The detail names the transaction. |

That last row is the one to handle deliberately: a settled payment is never re-signed, because its nonce is spent and a re-signed header would be refused by the seller's facilitator. Reading that refusal as a failure and retrying with a fresh key would pay the seller twice.

### When the budget says no

A payment the mandate won't cover returns `403` with the reason, and **nothing is signed**. For example:

```json theme={null}
{
  "type": "https://docs.dakota.xyz/api-reference/errors#forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "payment of 700000 exceeds the per-call limit of 500000"
}
```

[Budgets & holds](/documentation/agentic-payments/x402/budgets-and-holds#refusals) lists every refusal.

## 5. Watch the payment settle

Every signature has a **hold**. It starts `outstanding`. Once the seller's facilitator settles it on-chain, Dakota sees the transfer and marks it `settled` with the transaction hash:

```bash theme={null}
curl "https://api.platform.dakota.xyz/payment-agents/2vWxAgent0000000000000000000/x402/holds?state=settled" \
  -H "X-API-Key: $DAKOTA_API_KEY"
```

```json theme={null}
{
  "data": [
    {
      "id": "2vWxX402Hold000000000000001",
      "nonce": "0x3f1c8a5b2d7e4906c1a3f85b2e6d04971c8a5b3f2d7e4906c1a3f85b2e6d0497",
      "mandate_id": "2vWxX402Mandate000000000001",
      "agent_id": "2vWxAgent0000000000000000000",
      "wallet_id": "2vWxWallet000000000000000000",
      "pay_to": "0x94aE0f8B9F3c2A1d5E6b7C8D9e0F1a2B3c4D5E6F",
      "value": "100000",
      "asset": "USDC",
      "network": "base-sepolia",
      "state": "settled",
      "created_at": "2026-09-08T00:00:05Z",
      "valid_before": "2026-09-08T00:02:05Z",
      "tx_hash": "0x05dd3a9587f41b2c6e8d0a4f7b3c95e2d18a6f40b7c29e5d3a81f64b0c7e2d95",
      "settled_at": "2026-09-08T00:00:07Z"
    }
  ],
  "meta": {
    "total_count": 1,
    "has_more_after": false,
    "has_more_before": false
  }
}
```

Both x402 lists page the same way as the rest of the API: `limit`, `starting_after` and `ending_before`, newest first, with `meta` telling you whether there is more in either direction.

A signature the seller never used is `released` a few minutes after its `valid_before`, and its amount returns to the budget.
