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.
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. -
You pay it with one command. Point the CLI at the inbound email and it does the rest: derives the challenge from that email, 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 --in-reply-to <inbound-email-id>
--in-reply-tois the only thing you need: the CLI reads the challenge straight off the inbound email, so you do not paste challenge JSON or hand-attach a part. The reply is sent from the pinned payer address automatically so DKIM aligns.The one-command
pay-email --in-reply-toflow is rolling out with a current CLI release. Runprimitive payments pay-email --helpto confirm the flag is present in your installed version, andnpm i -g primitive@latestto update. If your CLI predates it, use the SDK sign-and-send path below, which is available today. -
Settlement is async. Paying does not settle synchronously. Primitive verifies your reply's DKIM signature and the signed payment against the recorded challenge, then settles on-chain through a facilitator.
-
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" } }
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.