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 (base or base-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_KEY environment 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_payments and interactions_enabled organization entitlements. If a payment call returns feature_disabled, the feature is not enabled for your organization yet. To request access, contact support or email dev_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

  1. 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, or primitive emails:latest, you have the inbound email's id.

  2. 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-to is 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-to flow is rolling out with a current CLI release. Run primitive payments pay-email --help to confirm the flag is present in your installed version, and npm i -g primitive@latest to update. If your CLI predates it, use the SDK sign-and-send path below, which is available today.

  3. 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.

  4. 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 the From and Content-Type headers, 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 longer expires_in if 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.