Testing x402 Payments Without Real Funds
Building an x402 payment integration means testing a flow that involves Solana transactions and USDC. Testing against mainnet with real funds is possible but creates cost and cleanup overhead. DocketLayer provides a test mode that returns fixture data so you can build and validate your API integration — response parsing, error handling, request structure — without spending real money.
Devnet Wallet Setup
Solana devnet is a persistent test network that mirrors mainnet's structure but uses tokens with no real value. Devnet USDC is minted freely from the Circle developer faucet. You need a funded devnet wallet before testing the x402 payment construction code.
# Generate a new keypair (Solana CLI)
solana-keygen new --outfile ~/.config/solana/devnet-test.json
# Set CLI to devnet
solana config set --url devnet
# Airdrop SOL for transaction fees
solana airdrop 2 --keypair ~/.config/solana/devnet-test.json
# Check balance
solana balance --keypair ~/.config/solana/devnet-test.jsonfrom solders.keypair import Keypair
from solana.rpc.api import Client
# Generate a new keypair
keypair = Keypair()
print("Public key:", keypair.pubkey())
print("Secret key (save this):", keypair.secret().hex())
# Connect to devnet
client = Client("https://api.devnet.solana.com")
# Request SOL airdrop for transaction fees
response = client.request_airdrop(keypair.pubkey(), 2_000_000_000)
print("Airdrop tx:", response.value)import { Keypair, Connection, LAMPORTS_PER_SOL } from "@solana/web3.js";
// Generate a new keypair
const keypair = Keypair.generate();
console.log("Public key:", keypair.publicKey.toBase58());
console.log("Secret key:", Buffer.from(keypair.secretKey).toString("hex"));
// Connect to devnet and request SOL airdrop
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const sig = await connection.requestAirdrop(keypair.publicKey, 2 * LAMPORTS_PER_SOL);
await connection.confirmTransaction(sig);
console.log("SOL balance ready"); After creating the wallet and airdropping SOL for fees, mint devnet USDC using the Circle developer faucet at faucet.circle.com. Select Solana devnet and paste your wallet address. Circle will send 10 USDC to your devnet wallet.
Test Mode
DocketLayer's test mode is activated by adding ?test=1 to any request, or by setting the X-DocketLayer-Test: 1 header. Test mode returns realistic fixture data — not live court lookups — with no payment required. Use it to validate your request structure, inspect the response format, and confirm your parsing code before connecting a funded wallet.
curl "https://api.docketlayer.ai/v2/case?court_code=nysd&case_id=1:23-cv-00001&test=1" To test the x402 payment construction code specifically — building the signed transaction, encoding the header, handling the retry — use a devnet wallet and call the live endpoint. Payment verification uses the Solana RPC and will validate the transaction structure regardless of whether funds are real.
Testing the Full Flow
import httpx, json
from solders.keypair import Keypair
from solana.rpc.api import Client
BASE_URL = "https://api.docketlayer.ai/v2/case"
client = Client("https://api.devnet.solana.com")
keypair = Keypair.from_base58_string("YOUR_DEVNET_PRIVATE_KEY")
# Step 1: Test mode — no payment required
params = {"court_code": "nysd", "case_id": "1:23-cv-00001", "test": "1"}
r = httpx.get(BASE_URL, params=params)
assert r.status_code == 200
print("Fixture data:", r.json()["case"]["case_name"])
# To test the payment flow itself, omit test=1 and handle the 402:
params_paid = {"court_code": "nysd", "case_id": "1:23-cv-00001"}
r = httpx.get(BASE_URL, params=params_paid)
assert r.status_code == 402, f"Expected 402, got {r.status_code}"
requirements = json.loads(r.headers["X-Payment-Requirements"])
receiver_address = requirements["payTo"]
amount_usd = int(requirements["maxAmountRequired"]) / 10 ** 6 # 990000 → 0.99
# Build, sign, and encode (see x402 guide for full implementation)
encoded_payment = build_payment_tx(client, keypair, receiver_address, amount_usd)
# Retry with payment
r2 = httpx.get(BASE_URL, params=params_paid, headers={"x402-payment": encoded_payment})
assert r2.status_code == 200
print("Success:", r2.json()["case"]["case_name"])import { Connection, Keypair } from "@solana/web3.js";
import axios from "axios";
const connection = new Connection("https://api.devnet.solana.com");
const secretKey = Uint8Array.from(JSON.parse(process.env.WALLET_SECRET_KEY));
const keypair = Keypair.fromSecretKey(secretKey);
const BASE_URL = "https://api.docketlayer.ai/v2/case";
// Test mode — no payment required, returns fixture data
const testParams = { court_code: "nysd", case_id: "1:23-cv-00001", test: "1" };
const fixture = await axios.get(BASE_URL, { params: testParams });
console.log("Fixture data:", fixture.data.case.case_name);
// Full payment flow — omit test=1
const params = { court_code: "nysd", case_id: "1:23-cv-00001" };
const probe = await axios.get(BASE_URL, { params }).catch(e => e.response);
console.assert(probe.status === 402, `Expected 402, got ${probe.status}`);
const requirements = JSON.parse(probe.headers["x-payment-requirements"]);
const receiverAddress = requirements.payTo;
const amountUsd = parseInt(requirements.maxAmountRequired) / 1e6; // 990000 → 0.99
// Build, sign, and encode (see x402 guide for full implementation)
const encodedPayment = await buildPaymentTx(connection, keypair, receiverAddress, amountUsd);
// Retry with payment
const result = await axios.get(BASE_URL, {
params,
headers: { "x402-payment": encodedPayment }
});
console.log("Success:", result.data.case.case_name); Verifying Payment Locally
If you want to inspect the payment transaction before sending, you can decode and examine it locally. The x402-payment header value is a base64-encoded JSON payload that wraps the signed Solana transaction. Decoding it lets you confirm the fee payer, instruction count, and structure match what you intended before retrying the request.
import base64, json
from solders.transaction import Transaction
def inspect_payment_header(header_value: str):
# Decode the outer base64 to get the payment payload JSON
payment_payload = json.loads(base64.b64decode(header_value).decode())
# Extract and decode the Solana transaction
tx_base64 = payment_payload["payload"]["transaction"]
tx_bytes = base64.b64decode(tx_base64)
tx = Transaction.from_bytes(tx_bytes)
print("Fee payer:", tx.message.account_keys[0])
print("Instructions:", len(tx.message.instructions))import { Transaction } from "@solana/web3.js";
function inspectPaymentHeader(headerValue) {
// Decode the outer base64 to get the payment payload JSON
const paymentPayload = JSON.parse(
Buffer.from(headerValue, "base64").toString()
);
// Extract and decode the Solana transaction
const txBytes = Buffer.from(paymentPayload.payload.transaction, "base64");
const tx = Transaction.from(txBytes);
console.log("Fee payer:", tx.feePayer?.toBase58());
console.log("Instructions:", tx.instructions.length);
} CI/CD Integration
For automated testing in CI, use test=1 to validate API integration — request structure, response parsing, error handling — without any wallet dependency. Store a devnet wallet secret key as an environment secret only for the subset of tests that exercise the payment construction code specifically.
Keep the devnet wallet topped up with a small SOL balance for transaction fees. Devnet USDC from the Circle faucet is unlimited. A CI job that runs integration tests against test mode costs nothing beyond the standard CI compute.