# A Practical Guide to X402 Payment Hooks in TypeScript and Python

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.

![X402 payment hook workflow](https://cdn.acedata2.cloud/gpt-image/aedb62af72a63e26deadd3b07a2358459e0a30de6fe28e85d771a54d4f74e8fa.png)

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:

```text
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:

- `exact` signs a fixed amount and fits fixed-price operations.
- `upto` signs 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:

```bash
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`:

```ts
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:

```bash
pip install acedatacloud acedatacloud-x402
```

Then construct an `EVMAccountSigner` from an environment variable and inject `payment_handler`:

```python
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:

1. Use `preferScheme: 'upto'` or `prefer_scheme="upto"` for metered chat calls.
2. Never put a raw private key into the TypeScript handler; wrap it behind an EIP-1193 provider.
3. Treat the first EVM approval as a distinct setup step with clear operator visibility.
4. 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.
5. 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](https://platform.acedata.cloud/documents/sdk-x402-payment) beside your implementation.

