Use x402 and MPP
x402 and the Machine Payments Protocol (MPP) allow an agent to discover and pay for a service over HTTP. When the agent calls a paid endpoint, the service answers with HTTP 402 (Payment Required) and a payment challenge. The agent then pays the challenge in USDC and sends the request again. For example, if your agent calls Wren's Weather and the service answers with a 402 challenge, your agent will pay the challenge and repeat the call to get the response.
We record each of these payments as a send from an account that the agent's policy lets it send from. This means the same counterparty list, spending limits, and approvals apply to x402 and MPP payments as to any other send.
We support these payments in the CLI and the SDK. Note that the MCP connector does not support x402 or MPP payments.
Requirements
Before your agent can pay an endpoint, you'll need the following:
- A wallet account on the challenge's network: That is Base where enabled, or Base Sepolia in the sandbox. Arc support is rolling out.
- A send capability: The agent's policy must allow sends from that account.
- A counterparty for the service: The address the service asks to be paid
(
payTo) must be saved as a counterparty wallet rail.
We do not add payTo as a counterparty for you, because a
402 response is untrusted. A service's address can change, so you may want to
consider letting the agent request new counterparties.
Pay with the CLI
To pay an x402 endpoint, pass the URL and the wallet account the payment draws from:
catena x402 --url https://api.example.com/paid \
--account acct_... --max-amount 0.25For MPP, run catena mpp with the same options:
| Option | Use |
|---|---|
--url |
The endpoint to call. |
--account |
The wallet account the payment draws from. |
--max-amount |
A price ceiling in USD. The CLI refuses a larger challenge before it pays anything. Pass it whenever you have one. |
--data |
A request body. Implies POST and JSON. |
--header "Name: Value" |
An extra request header. |
In the example above, --max-amount 0.25 means the CLI will refuse any
challenge over $0.25 before it pays anything. The
CLI reference lists every option.
Read the result
The command prints JSON. When it needs something from you, it will exit non-zero, and the JSON names the problem. Below are the results that a run may return:
| Field | Meaning | Next step |
|---|---|---|
.paid |
The payment went through. .payment has the intent ID, amount, and recipient. .body has the response. |
Use the response. |
.counterpartyNotFound |
The recipient is not a saved counterparty. Nothing was charged. | Read .nameIsPlaceholder, create the counterparty with .createCommand, then run the same command again. |
.approvalPending |
The payment is at or above the approval threshold. | Ask an admin to approve it, then run the same command again. It uses the approval and pays once. |
.networkMismatch |
The account cannot pay on the challenge's network. Nothing was charged. | Run again with --account set to a wallet on one of .networkMismatch.requiredNetworks. |
When .nameIsPlaceholder is false, the suggested name is the one the service
gave for itself, and nobody has verified it, so confirm that you trust it before
you create the counterparty. When it is true, you'll need to replace the
placeholder name yourself.
If .bodyTruncated is true, you can read the full body from the file at
.bodyPath.
Never fetch the URL again to see more
ImportantEach run can pay again
Every run of the command can pay again, even for the same URL and price. Do not run the command a second time to read more of a response, and do not pay again after an unclear result.
To recover a payment that finished after an interruption, run
catena intents get <id>. For x402, .data.paymentCredential.value holds the
PAYMENT-SIGNATURE header that you can retry with while it is still valid.
Pay with the SDK
In the SDK, you wrap fetch so that it pays challenges from a wallet account:
import { createCatenaClient } from "@catena/sdk"
import { wrapFetchWithX402Payment } from "@catena/sdk/x402"
const client = createCatenaClient()
const fetchWithPayment = wrapFetchWithX402Payment(client, {
accountId: walletAccountId,
maxAtomicAmount: 1_000_000n, // 1 USDC
})
const response = await fetchWithPayment("https://api.example.com/paid-thing")maxAtomicAmount is in atomic USDC units, so 1_000_000n is 1 USDC. Responses
without a payable challenge pass through unchanged. For MPP, use
wrapFetchWithMppPayment from @catena/sdk/mpp. See the SDK reference
for details.
Evidence from sellers
Some x402 sellers sign their offer and receipt. When they do, Catena verifies both and keeps them on the intent (as evidence for an audit or a dispute). They do not change the payment.