SDK + X402 Payment Hook

X402 is an on-chain payment protocol proposed by Coinbase that charges based on HTTP 402: the server returns 402 Payment Required on requests without a token, along with the accepts: [...] field listing acceptable chains / assets / prices; the client locally signs an authorization (Permit2 / EIP-712 on EVM, SPL token transfer authorization on Solana), places the base64-encoded envelope in the X-Payment header, and resends. The server verifies and then settles on-chain, returning the business result.

All consumer-facing APIs of Ace Data Cloud have built-in X402 endpoints, and /.well-known/x402 is the protocol discovery entry.

@acedatacloud/sdk and acedatacloud both expose a paymentHandler hook: when a request made by the SDK receives a 402, it calls your injected handler to get the X-Payment header and then resends the original request. Using @acedatacloud/x402-client / acedatacloud-x402 with the SDK, the entire process is completely transparent to the business code—you only need to use client.openai.chat.completions.create(...), which looks exactly like the token model, but under the hood, it is pay-per-call without needing to pre-charge.

This article:

  • Walks through the TS side of the "no token + X402 handler injection" link (see T12 Verification)
  • Lists the differences between the EVM / Solana signature links
  • Provides three adaptations: viem private key mode, browser wallet mode, Python EVMAccountSigner mode
  • Clarifies the preferScheme / prefer_scheme field, which is easy to trip over

I. Protocol Overview (Must Read)

A successful X402 call involves 3 HTTP RTTs:

1. SDK -> /openai/v1/chat/completions               (no Authorization)
   <- 402 Payment Required
      { accepts: [{ scheme:'upto', network:'base', maxAmountRequired:'10000', resource:'usdc', ... }] }

2. SDK Internal -> paymentHandler({ url, method, body, accepts })   (local signing, 0 RTT)
   <- { headers: { 'X-Payment': '<base64-envelope>' } }

3. SDK -> /openai/v1/chat/completions               (X-Payment header injected)
   <- 200 + business response + 'X-Payment-Response: <settled>'

The X402 envelope is a JSON object that is base64 encoded and placed in the X-Payment header. Structure (excerpt):

{
  "x402Version": 1,
  "scheme": "upto",
  "network": "base",
  "payload": {
    "permit2": {
      "permitted": [{ "token": "0x...USDC", "amount": "10000" }],
      "nonce": "...",
      "deadline": "..."
    },
    "witness": { "...metered-billing-fields..." },
    "signature": "0x..."
  }
}
scheme Meaning
exact Fixed price (pricing scenarios for image/video generation, search, etc.). The signed amount = the amount requested by the server.
upto Metered billing (chat completions / token types). Sign an upper limit amount, with actual settlement based on the used portion (based on Permit2 + witness). Strongly recommended for session-based APIs.

preferScheme / prefer_scheme is used to select a preference when the server provides multiple schemes simultaneously. If the server only exposes exact, this field will be ignored; if upto is set but the server does not expose it, it will fall back to the first matching item.

II. TypeScript: Browser Wallet + Server viem Two Usage Methods

Installation

npm install @acedatacloud/sdk @acedatacloud/x402-client

Tested version numbers:

@acedatacloud/sdk@2026.504.2
@acedatacloud/x402-client@2026.531.3

createX402PaymentHandler Complete Signature

export interface X402PaymentHandlerOptions {
  network: 'solana' | 'base' | 'skale';
  solanaWallet?: SolanaWalletAdapter;       // required when network='solana'
  evmProvider?: EVMProvider;                // required when network='base'/'skale', EIP-1193
  evmAddress?: string;                      // required when network='base'/'skale'
  preferScheme?: 'exact' | 'upto';
}

The return value is a (ctx) => Promise<{ headers: Record<string, string> }> that matches the SDK's paymentHandler hook signature.

Usage 1: Browser (MetaMask / WalletConnect)

import { AceDataCloud } from '@acedatacloud/sdk';
import { createX402PaymentHandler } from '@acedatacloud/x402-client';

// 1. Prompt the user to connect their wallet
const accounts: string[] = await (window as any).ethereum.request({
  method: 'eth_requestAccounts'
});
const userAddress = accounts[0];

// 2. Switch to Base mainnet
await (window as any).ethereum.request({
  method: 'wallet_switchEthereumChain',
  params: [{ chainId: '0x2105' }]   // 8453 = Base
});

// 3. Construct the SDK client, injecting the X402 handler
//    Note: Do not pass apiToken, allowing the SDK to follow the 402 path
const client = new AceDataCloud({
  paymentHandler: createX402PaymentHandler({
    network: 'base',
    evmProvider: (window as any).ethereum,
    evmAddress: userAddress,
    preferScheme: 'upto'   // required for chat types
  })
});

// 4. Call normally
const res: any = await client.openai.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: 'hi' }],
  max_tokens: 20
});
console.log(res.choices[0].message.content);

The first call will prompt for two signature approvals in the browser: the first is a one-time approve for USDC via Permit2 (the amount is MaxUint256, written to the chain); the second is the EIP-712 signature for the X402 envelope (not written to the chain, just for facilitator verification). Subsequent calls only require the second signature, making the experience "click once to sign → get result."

Usage 2: Node Server + viem Private Key (suitable for backend / CLI)

@acedatacloud/x402-client on the TS side only accepts EIP-1193 providers—it does not directly manage private keys. In Node / CLI scenarios, the standard practice is to use viem to wrap the private key in a WalletClient, and then use @ethereumjs/util or viem's internal EIP-1193 adapter.

import { AceDataCloud } from '@acedatacloud/sdk';
import { createX402PaymentHandler } from '@acedatacloud/x402-client';
import { createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(process.env.BASE_RPC_URL)
});

// viem WalletClient comes with EIP-1193 compatible .request(), can be used directly as evmProvider
const client = new AceDataCloud({
  paymentHandler: createX402PaymentHandler({
    network: 'base',
    evmProvider: walletClient as any,   // walletClient.request meets EIP-1193
    evmAddress: account.address,
    preferScheme: 'upto'
  })
});

const res: any = await client.openai.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: 'hi' }],
  max_tokens: 20
});
console.log(res.choices[0].message.content);

If you feel that the EIP-1193 adaptation of viem is not stable enough, you can also use the lower-level signEVMUptoPayment to manually connect accepts → signed envelope → X-Payment header, bypassing the SDK hooks; however, it is still recommended to prioritize createX402PaymentHandler to avoid maintaining protocol upgrades yourself.

Usage 3: Solana

import { AceDataCloud } from '@acedatacloud/sdk';
import { createX402PaymentHandler } from '@acedatacloud/x402-client';
import { Keypair } from '@solana/web3.js';

const kp = Keypair.fromSecretKey(/* Uint8Array */);

const client = new AceDataCloud({
  paymentHandler: createX402PaymentHandler({
    network: 'solana',
    solanaWallet: {
      publicKey: kp.publicKey,
      signTransaction: async (tx) => {
        tx.sign([kp]);
        return tx;
      }
    }
  })
});

The Solana chain currently only exposes the exact scheme, so preferScheme does not work on Solana.

Three, Python: Private Key Mode

The Python acedatacloud-x402 follows the directly signing with private key approach (no EIP-1193 abstraction), which is more suitable for server-side/task executors.

Installation

pip install acedatacloud acedatacloud-x402

Tested version numbers:

acedatacloud==2026.4.26.1
acedatacloud-x402==2026.5.31.3

EVM (Base / Skale)

import os
from acedatacloud import AceDataCloud
from acedatacloud_x402 import (
    create_x402_payment_handler,
    EVMAccountSigner,
)

# 1. Construct signer from private key
signer = EVMAccountSigner.from_private_key(os.environ["EVM_PRIVATE_KEY"])

# 2. Construct SDK: do not pass api_token, let SDK follow the 402 path
client = AceDataCloud(
    payment_handler=create_x402_payment_handler(
        network="base",
        evm_signer=signer,
        prefer_scheme="upto",   # chat types must choose upto
    )
)

# 3. Normal call
res = client.openai.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "hi"}],
    max_tokens=20,
)
print(res["choices"][0]["message"]["content"])

Solana

import os
from acedatacloud import AceDataCloud
from acedatacloud_x402 import (
    create_x402_payment_handler,
    SolanaKeypairSigner,
)

signer = SolanaKeypairSigner.from_secret_key_base58(os.environ["SOLANA_PRIVATE_KEY"])

client = AceDataCloud(
    payment_handler=create_x402_payment_handler(
        network="solana",
        solana_signer=signer,
        rpc_url="https://api.mainnet-beta.solana.com",  # optional
    )
)

One-time approve (only for EVM first time)

On EVM Base, X402 uses Permit2, requiring the wallet to give the Permit2 contract a one-time MaxUint256 approve for USDC. The acedatacloud-x402 has a built-in approve_permit2:

from acedatacloud_x402 import approve_permit2

tx_hash = approve_permit2(
    evm_signer=signer,
    rpc_url=os.environ["BASE_RPC_URL"],
)
print("permit2_approve_tx", tx_hash)

This transaction only needs to be sent once, after which all X402 EVM payments will use this authorization. Solana does not require this.

Four, Real Operation Verification

Test goal: TS SDK does not pass token, inject X402 handler, can normally construct and initiate requests (lightweight verification without consuming real USDC on the chain).

// /tmp/sdk-tests/ts/x402-wire-test.ts
import { AceDataCloud } from '@acedatacloud/sdk';
import { createX402PaymentHandler } from '@acedatacloud/x402-client';

const handler = createX402PaymentHandler({
  network: 'base',
  evmProvider: { request: async () => '0x0' } as any,   // placeholder provider
  evmAddress: '0x0000000000000000000000000000000000000000',
  preferScheme: 'upto'
});

console.log('handler_type', typeof handler);   // function

const client = new AceDataCloud({
  paymentHandler: handler
});

console.log('client_ctor_ok', client.constructor.name);   // AceDataCloud

Output:

handler_type function
client_ctor_ok AceDataCloud

Results explanation:

  • No apiToken was passed, and the SDK construction did not throw an error, proving that X402 mode is indeed a legitimate alternative to the token.
  • The createX402PaymentHandler returns a function (hook), which the SDK will only call when it receives a 402.
  • Actual end-to-end testing of payment on the chain was not included in this tutorial due to the involvement of real USDC deductions; you can refer to the e2e examples in the X402 Integration Guide.

The Python side create_x402_payment_handler also performed the same verification — the function return value is callable, and injecting payment_handler=... does not throw an error when constructing AceDataCloud(...). The semantics are aligned on both sides.

Five, Comparison with "Bearer Token Mode"

Dimension API Token X402
Applicable Scenarios Own backend, long-term projects Third-party developers, pay-per-use, Agentic calls
Registration Requires application at console Not required; just need an on-chain wallet
Billing Precision Pre-recharge, billed according to token table Real-time billing on-chain
Balance Can be viewed in the console Check on-chain wallet USDC
Initial Cost Free quota given upon email registration Needs to bridge USDC to Base, first Permit2 approve
Suitable for chat types ✅ (must use preferScheme=upto)
Suitable for one-time payments / cross-account payments
Code Changes apiToken: '...' paymentHandler: createX402PaymentHandler(...)
Two modes can coexist—different authentication methods can be configured for different `client` instances within the same process.

## VI. Common Pitfalls

1. **The chat class must use `preferScheme=upto`**: Using `exact` will cause the facilitator to deduct USDC based on `maxAmountRequired` (not the actual usage).
2. **Do not pass the raw private key to `createX402PaymentHandler` on the Node side**: The TS package does not accept `{ privateKey }`, it must be wrapped as an EIP-1193 provider (recommended viem `WalletClient`).
3. **The first call is a double signature**: The first time signs the Permit2 approve (on-chain, incurs gas), the second time signs the X402 envelope (off-chain). Subsequent calls only require the second signature.
4. **Solana does not have the concept of Permit2**: Directly sign SPL token transfer authorization, no need for approval; however, currently, only `exact` is supported on the Solana chain.
5. **Distinguishing between business errors and payment errors**: 402 → handler failure throws `X402SignError` (specific type varies by chain); subsequent resends will still classify business interface errors (401 / 422 / 5xx) as ordinary SDK exceptions.
6. **The most stable way to adapt `viem`**: `evmProvider: walletClient as any` will lose type checking but has the best compatibility; if you want to retain types, use viem's `.transport.request` to wrap a layer of `{ request }` object to pass in.

## Learn More

- 📦 [`@acedatacloud/x402-client` on npm](https://www.npmjs.com/package/@acedatacloud/x402-client)
- 🐍 [`acedatacloud-x402` on PyPI](https://pypi.org/project/acedatacloud-x402/)
- 🗂 [X402 client source code](https://github.com/AceDataCloud/SDK/tree/main/x402-client)
- 🔗 [X402 integration guide](https://platform.acedata.cloud/documents/x402-integration)
- 📘 [TypeScript SDK integration tutorial](https://platform.acedata.cloud/documents/sdk-typescript)
- 🐍 [Python SDK integration tutorial](https://platform.acedata.cloud/documents/sdk-python)
- 🌐 [x402.org](https://www.x402.org/)