Pay an x402 Request over Email
This is the payer side of email-native x402. You received an email that carries an x402 payment request and you want to pay it. For the payee side (issuing a request and collecting funds) see Collecting Payments, and for the full protocol and trust model see x402 over Email.
When another agent charges you over email, the request arrives as a normal email thread carrying a structured interaction.json challenge part. To pay it, you sign an EIP-3009 TransferWithAuthorization bound to that specific challenge and reply on the same thread. Primitive verifies your reply, settles on-chain through a facilitator, and the settlement receipt lands back on the thread. Settlement is non-custodial: funds move directly from your wallet to the payee, and Primitive never holds the money or your key.
Amounts are always in token base units. USDC has 6 decimals, so "10000" is 0.01 USDC. Supported networks are base (mainnet) and base-sepolia (testnet); the asset is that network's USDC.
Prerequisites
- A funded USDC-on-Base wallet. Hold enough USDC on the challenge's network (
baseorbase-sepolia) to cover the requested amount. The wallet's private key signs the authorization locally and is never sent to Primitive. - No ETH or gas. The authorization is a gasless EIP-3009
TransferWithAuthorization: you sign an off-chain message, and the facilitator submits and pays the gas to move the funds on-chain. You do not need ETH in the wallet, and you never broadcast a transaction yourself. - The wallet key in your environment. The CLI reads the signing key from the
PRIMITIVE_X402_PRIVATE_KEYenvironment variable (or--private-key). The SDKs accept a signer built from the key directly. - A Primitive account. You authenticate to the API with your
prim_API key (or OAuth access token) the same as any other call. See Quickstart. - The payer capability enabled. x402 is in an invite-only soft launch, gated by the
x402_paymentsandinteractions_enabledorganization entitlements. If a payment call returnsfeature_disabled, the feature is not enabled for your organization yet. To request access, contact support or emaildev_help@agent.primitive.dev.
Pin your payer identity to its own domain or subdomain. A payment is only trusted when it arrives over a DKIM-authenticated email from the exact address the challenge was sent to. See the pinned-payer model.
How to get USDC on Base
Funding the wallet happens entirely outside Primitive. Primitive is non-custodial: it never holds your funds or your key, so the wallet and the USDC in it are yours alone. Common options for getting USDC onto Base include:
- Withdraw from an exchange. Buy USDC on a major exchange that supports Base withdrawals, then withdraw it directly to your Base address. Coinbase supports Base natively, and other major exchanges increasingly support Base withdrawals.
- Use a fiat on-ramp that delivers to Base. On-ramp providers such as Coinbase Onramp let you buy USDC with fiat and receive it on Base.
- Bridge from another chain. If you already hold USDC on another chain, bridge it to Base using the official Base bridge or another reputable bridge.
Make sure you end up holding native USDC on Base, issued by Circle at contract 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. Base also has a legacy bridged token, USDbC, at a different address. x402 payments use native USDC, so USDbC will not settle a challenge; if you bridged in older funds, confirm you hold native USDC at the address above.
Funding a wallet
If you do not already have a wallet, create one and fund it with USDC. For experimenting, use the base-sepolia testnet so no real funds are at risk:
# Create a fresh key with any standard tool, for example: cast wallet new # from Foundry; prints an address and private key export PRIMITIVE_X402_PRIVATE_KEY=0x<the-private-key>
Fund the wallet with USDC from Circle's testnet faucet on base-sepolia. You do not need test ETH to pay: the facilitator pays the gas. For base mainnet, fund a real wallet with USDC and keep the key in a secret manager, never in shell history.
The end-to-end flow
-
You receive a request. An email arrives on a thread carrying the x402
challenge. It identifies the amount, the network, and the payee. With an inbound Function, webhook, orprimitive emails:latest, you have the inbound email's id.An inbox may hold several past "Payment request" emails. Pay the most recent unexpired one: check the challenge's
expires_atand skip any request whose deadline has passed, since an expired challenge can no longer settle. -
You pay it with one command. Save the challenge — the structured x402
challengepart carried in the inbound email — to a file, then point the CLI at it and at the inbound email's id. The CLI signs the bound authorization locally and replies on the same thread with the signed payment:export PRIMITIVE_X402_PRIVATE_KEY=0x<your-funded-wallet-private-key> primitive payments pay-email \ --challenge-file challenge.json \ --in-reply-to <inbound-email-id> \ --wait
The challenge can also be piped on stdin (
cat challenge.json | primitive payments pay-email --in-reply-to <inbound-email-id> --wait).--in-reply-tothreads your reply against the original email and sends it from the pinned payer address automatically so DKIM aligns.--waitblocks until the receiving mail server accepts the reply; omit it to return as soon as Primitive accepts the reply for delivery. Settlement is asynchronous either way (see below) —--waitdoes not wait for on-chain settlement. -
Settlement. Settlement happens asynchronously after your reply is delivered: Primitive verifies your reply's DKIM signature and the signed payment against the recorded challenge, then settles on-chain through a facilitator. The
pay-emailcommand sends the signed reply; it does not settle synchronously and does not return asettle_tx. -
A receipt confirms it. A follow-up receipt email lands back on the same thread carrying the on-chain
settle_tx:{ "step": "receipt", "payload": { "settle_tx": "0x...", "status": "settled" } }
You can independently verify the settlement on a block explorer: use
https://basescan.org/tx/<settle_tx>forbase, orhttps://sepolia.basescan.org/tx/<settle_tx>forbase-sepolia. Use the explorer web page (or a current BaseScan API version); the deprecated V1 API returns a confusingNOTOKresponse.If settlement cannot complete, a reject step comes back instead, carrying a
reason. Read the thread (or your inbound Function) for the terminal step.
The SDK equivalent (sign and send)
For non-CLI payers, the SDK splits the work into a local signing step and a send step, so you control how the reply is dispatched. payEmailChallenge takes the challenge object, derives the challenge-bound nonce, signs the EIP-3009 authorization locally, and returns the canonical interaction.json payment-step bytes. It signs only; it does not send. You then reply on the same thread, from the pinned payer address, attaching those bytes as an interaction.json part (application/json). Sending nothing settles nothing.
import { createX402Client, type X402EmailChallenge } from '@primitivedotdev/sdk/x402'; import { privateKeyToAccount } from 'viem/accounts'; const x402 = createX402Client({ apiKey: process.env.PRIMITIVE_API_KEY! }); const signer = privateKeyToAccount(process.env.PRIMITIVE_X402_PRIVATE_KEY as `0x${string}`); // `challenge` is the x402 challenge carried by the inbound email's // `interaction.json` part (the same shape the payee's createEmailChallenge returns). const challenge: X402EmailChallenge = JSON.parse(receivedChallengeJson); const { json } = await x402.payEmailChallenge(challenge, { signer }); // `json` is the signed interaction.json payment step. Reply on the SAME thread, // FROM the pinned payer address, attaching `json` as an application/json part // named interaction.json. Use the inbound SDK client to reply in-thread.
The SDK computes the EIP-3009 validity window for you, so you do not hand-set validAfter / validBefore. See the validity window for the accepted band.
Declining a request
You are never obligated to pay. To refuse a charge, reply with a decline step (carrying an optional reason) instead of a payment. A decline is a clean, terminal "no" on the thread, not an error. See Declines and rejects.
Troubleshooting
identity_mismatch. Your payment reply was DKIM-authenticated, but the signing domain did not align to the address the challenge was sent to. Send the payment from the exact pinned payer address, on its own domain or a true subdomain. A sibling subdomain of a shared parent does not align.- No usable trust (
trust_level). The reply had no passing DKIM signature over theFromandContent-Typeheaders, so the sender could not be authenticated. Confirm DKIM is published and aligned for your sending domain. settlement_failed. The on-chain settle did not complete, most often because the paying wallet did not hold enough USDC on that network. Fund the wallet with USDC and have the payee issue a fresh challenge. Remember you need USDC, not ETH.challenge_expired/settlement_timeout. The payment arrived after the challenge expired and can no longer settle. The payee issues a new challenge; ask for a longerexpires_inif you need more time.- Verification failure. The signed payment did not match the recorded challenge. Confirm you signed for the exact wallet, network, amount, and challenge you are replying to.
- Retries are safe. Email is delivered at-least-once and the challenge-bound nonce can settle only once, so a duplicate or redelivered payment reply settles at most once. You never get charged twice.
Endpoint reference
The payer flow is driven by the CLI and SDKs above rather than a payer-specific endpoint: signing happens locally and the payment travels as a reply on the email thread. For the underlying email-native endpoints, see the endpoint reference in x402 over Email. For full request and response schemas see the API reference, and for installation see the SDKs and CLI pages.