Complete payment flow
Configure a merchant backend and payer client from ownership proof through chain-confirmed fulfillment.
This guide builds the default stateless path. Your application owns its order record; the gateway owns references and protocol coordination; the chain and signed receipt provide payment evidence.
Merchant creates PaymentRef
Gateway returns signed 402
Wallet signs EIP-3009
Facilitator broadcasts
Chain and receipt verify
Funds move directly from payer authorization to payTo. The gateway never holds wallet keys or balances.
Prerequisites
- A deployed x402 gateway URL such as
https://pay.example.com. - A merchant-controlled EVM
payTowallet on the configured network. - USDC using EIP-3009 on that network.
- A payer wallet adapter that can sign EIP-712 typed data.
- Backend secret storage for the merchant API key and notify HMAC key.
Install the single package in both applications and only import the entry point needed by that runtime:
Shellnpm install x402-paysdk
1. Prove payTo ownership
Registration is a two-request challenge flow. The wallet signs a challenge bound to the gateway origin, action, payTo, network, nonce, and expiry.
TypeScriptimport { createMerchantChallenge, merchantChallengeTypedData, registerMerchant} from 'x402-paysdk/merchant';const gateway = 'https://pay.example.com';const payTo = '0x1111111111111111111111111111111111111111';const webhookUrl = 'https://merchant.example/x402/notify';const challenge = await createMerchantChallenge(gateway, { payTo, network: 'base', action: 'merchant.register', webhookUrl});const signature = await merchantWallet.signTypedData( merchantChallengeTypedData(challenge.message));const credentials = await registerMerchant(gateway, { challengeId: challenge.challengeId, payTo, network: 'base', label: 'Example Store', webhookUrl, signature});
Store apiKey, notifyHmacKey, and notifyHmacKeyId immediately in a secret manager. The gateway only returns raw credentials at issuance or rotation.
2. Create the merchant client
TypeScriptimport { MerchantClient } from 'x402-paysdk/merchant';const merchant = new MerchantClient({ gateway, apiKey: process.env.X402_API_KEY!, notifyHmacKey: process.env.X402_NOTIFY_KEY!, notifyHmacKeyId: process.env.X402_NOTIFY_KEY_ID!, payTo, network: 'base'});
Create this client only on the backend. Do not serialize it, expose it through frontend state, or log its options.
3. Create and persist a PaymentRef
Amounts are atomic USDC units, so 24000000 means 24 USDC for a six-decimal token.
TypeScriptimport { createPaymentRef } from 'x402-paysdk/merchant';const ref = await createPaymentRef(merchant, { payTo, network: 'base', asset: 'USDC', amount: '24000000', invoiceId: 'order_7K9M2', label: 'Order 7K9M2', description: 'Example Store checkout', expiresInSecs: 900, notifyUrl: webhookUrl});await orders.savePaymentReference({ orderId: 'order_7K9M2', refId: ref.refId, refUrl: ref.refUrl, expectedAmount: ref.snapshot.amount, expectedPayTo: ref.snapshot.payTo, expiresAt: ref.snapshot.expiresAt});
Return only refUrl and display metadata to the payer application. The same URL can be transported as a link, QR code, deep link, or API payload.
4. Connect the payer wallet
The payer integration supplies a narrow signer. Private key material never enters the SDK.
TypeScriptimport type { WalletSigner } from 'x402-paysdk/pay';const wallet: WalletSigner = { address: account.address, signTypedData: (typedData) => walletClient.signTypedData(typedData)};
Before asking for a signature, you can inspect the signed requirements:
TypeScriptimport { getPaymentRequired } from 'x402-paysdk/pay';const required = await getPaymentRequired(ref.refUrl);const offer = required.accepts[0];renderConfirmation({ payTo: offer.payTo, amount: offer.amount, network: offer.network, asset: offer.asset});
5. Authorize and settle
Use one unpredictable or business-unique paymentId for a single payment attempt. Reusing it with a different context must fail closed.
TypeScriptimport { pay } from 'x402-paysdk/pay';const settlement = await pay({ refUrl: ref.refUrl, paymentId: crypto.randomUUID(), wallet});console.log(settlement.txHash, settlement.receiptStatus);
pay() resolves the HTTPS receive URL, obtains 402 Payment Required, verifies the signed offer and gateway DID identity, builds EIP-3009 typed data, asks the wallet to sign, submits PAYMENT-SIGNATURE, and returns the settlement evidence.
6. Send evidence to the backend
The payer client should send the tx hash and complete signed receipt pair to your backend. Treat them as evidence to verify, not as a trusted “paid” flag.
TypeScriptawait fetch('/api/orders/order_7K9M2/payment', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ txHash: settlement.txHash, signedReceipt: settlement.signedPaymentReceipt, receipt: settlement.receipt })});
7. Verify receipt and chain finality
The backend binds verification to the order's stored expectations. Only fulfill after the signature, payment context, transfer, and confirmation policy all pass.
TypeScriptimport { verifyReceipt } from 'x402-paysdk/merchant';const verification = await verifyReceipt(merchant, { signedReceipt, receipt, expectedPayTo: order.expectedPayTo, expectedAmount: order.expectedAmount, minConfirmations: 3});if (!verification.valid || !verification.confirmed) { throw new Error(verification.reason ?? 'payment is not final');}await orders.markPaidOnce(order.id, { txHash: verification.txHash!, payer: verification.payer!, receipt});
The markPaidOnce operation belongs in your database and should be atomic. The default gateway intentionally does not own merchant fulfillment state.
8. Add notify as a fast hint
Read the exact raw request body before JSON parsing and use shared atomic nonce storage in multi-instance production.
TypeScriptimport { verifySettlementNotify } from 'x402-paysdk/merchant';const result = await verifySettlementNotify( request.headers, rawBody, { [process.env.X402_NOTIFY_KEY_ID!]: process.env.X402_NOTIFY_KEY! }, sharedNonceStore);if (!result.ok) return new Response('invalid notify', { status: 401 });await paymentQueue.enqueue(result.notify);return new Response(null, { status: 204 });
Notify can trigger verification sooner, but it is never financial finality. The queued worker must still call receipt or tx verification.
Completion criteria
- The payer saw the exact
payTo, amount, network, and asset before signing. - The signed offer was trusted through pinned signers or same-origin DID discovery.
- The backend matched the receipt to its stored order expectations.
- The chain transfer reached the configured confirmation policy.
- Fulfillment used a single atomic state transition.
- Credentials, payment signatures, and raw notify bodies were not logged.
Continue with production operations for recovery and reconciliation.