A Practical Guide to X402 Payment Hooks in TypeScript and Python
Handle HTTP 402 wallet payments at the SDK boundary while keeping ordinary model calls clean.

If you want an agent or CLI to pay per API call without storing a platform token, the tricky part is not the model request—it is handling HTTP 402 safely without leaking payment logic into every call.

This guide shows the smallest useful pattern: inject an X402 payment handler into the Ace Data Cloud SDK, then keep the rest of your application code looking like an ordinary chat-completions call.
How the payment hook works
An X402 request is a short negotiation rather than a special API endpoint. Your SDK first sends the normal request without an Authorization header:
POST /openai/v1/chat/completions
→ 402 Payment Required
→ accepts: [{ scheme, network, maxAmountRequired, ... }]
The SDK passes the request context and the server's accepts array to your local paymentHandler. The handler selects an accepted option, signs an authorization, and returns a base64-encoded envelope in the PAYMENT-SIGNATURE header. The SDK resends the original request with that header; after verification and settlement, the server returns the normal business response.
The envelope uses x402Version: 2. Its accepted object identifies the chosen scheme and CAIP-2 network, while payload contains the chain-specific authorization and signature.
Two schemes matter:
exactsigns a fixed amount and fits fixed-price operations.uptosigns an upper bound while settlement follows actual usage. This is the appropriate preference for metered chat-completion calls.
preferScheme does not invent a scheme. It only chooses among options the server actually offers; if upto is unavailable, the client falls back to a matching advertised option.
TypeScript: connect a browser wallet
Install the SDK and X402 client:
npm install @acedatacloud/sdk @acedatacloud/x402-client
The TypeScript handler accepts network, an EIP-1193 evmProvider, an evmAddress, and an optional preferScheme. For a browser wallet on Base, connect the account, switch to chain ID 0x2105, and inject the handler without passing apiToken:
import { AceDataCloud } from '@acedatacloud/sdk';
import { createX402PaymentHandler } from '@acedatacloud/x402-client';
const accounts: string[] = await (window as any).ethereum.request({
method: 'eth_requestAccounts'
});
await (window as any).ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x2105' }]
});
const client = new AceDataCloud({
paymentHandler: createX402PaymentHandler({
network: 'base',
evmProvider: (window as any).ethereum,
evmAddress: accounts[0],
preferScheme: 'upto'
})
});
const res: any = await client.openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Explain idempotency in one paragraph.' }],
max_tokens: 120
});
console.log(res.choices[0].message.content);
The first EVM use involves two approvals: a one-time on-chain USDC approval through Permit2, followed by the off-chain EIP-712 signature for the X402 envelope. Later calls only need the envelope signature.
For a Node.js worker or CLI, the TypeScript package still expects an EIP-1193 provider—it does not accept a raw private key. A practical pattern is to create a viem WalletClient with privateKeyToAccount, Base, and your RPC transport, then pass walletClient as any as evmProvider. That keeps key handling outside the payment hook and avoids maintaining the envelope protocol yourself.
Python: use a server-side signer
Python takes a more direct route and is convenient for background jobs. Install both packages:
pip install acedatacloud acedatacloud-x402
Then construct an EVMAccountSigner from an environment variable and inject payment_handler:
import os
from acedatacloud import AceDataCloud
from acedatacloud_x402 import create_x402_payment_handler, EVMAccountSigner
signer = EVMAccountSigner.from_private_key(os.environ["EVM_PRIVATE_KEY"])
client = AceDataCloud(
payment_handler=create_x402_payment_handler(
network="base",
evm_signer=signer,
prefer_scheme="upto",
)
)
res = client.openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Give me three retry rules."}],
max_tokens=120,
)
print(res["choices"][0]["message"]["content"])
On Base, the Python client also provides approve_permit2(evm_signer=signer, rpc_url=...) for the one-time approval. Keep that transaction separate from routine request handling so an operator can see when an on-chain authorization is being created.
Practical guardrails
A few details are worth making explicit in production:
- Use
preferScheme: 'upto'orprefer_scheme="upto"for metered chat calls. - Never put a raw private key into the TypeScript handler; wrap it behind an EIP-1193 provider.
- Treat the first EVM approval as a distinct setup step with clear operator visibility.
- Separate payment failures from business failures: the handler deals with the initial 402, while a resent request can still return ordinary 401, 422, or 5xx errors.
- Create separate SDK clients if one process needs both bearer-token and X402 modes. The two paths can coexist cleanly.
Solana is also supported through solanaWallet in TypeScript or SolanaKeypairSigner in Python, but the documented Solana path currently exposes only exact; it has no Permit2 approval step.
The part I like about this design is its boundary: payment negotiation lives in one injected function, while the model call stays boring and testable. For the complete signatures, chain differences, and wire-level examples, keep the SDK + X402 Payment Hook documentation beside your implementation.
Comments
Join the discussionNo comments yet. Be the first to comment.





