create-typescript-x402-client
Creating x402 HTTP Clients (Fetch and Axios)
Build HTTP clients that automatically detect 402 Payment Required responses, sign Algorand transactions, and retry requests with payment proof -- all transparently.
Prerequisites
Before using this skill, ensure:
- Node.js or browser environment with TypeScript support
- An Algorand wallet or private key for signing payment transactions
- USDC balance on the target network (testnet or mainnet) in the payer's account
Core Workflow: The 402 Payment Flow
The key insight is that wrapFetchWithPayment and wrapAxiosWithPayment intercept 402 responses, sign a transaction group, and retry the original request with the payment header -- all automatically:
Client Request (GET /api/premium)
|
v
Server Response
|
+-- Status != 402 --> Return response as-is
|
+-- Status == 402 -->
|
+-- Parse PaymentRequired (headers V2 / body V1)
+-- Select scheme via registered x402Client
+-- Build atomic transaction group
+-- Sign with ClientAvmSigner (wallet or private key)
+-- Encode PAYMENT-SIGNATURE header
+-- Retry original request with payment header
|
v
Return retried response
How to Proceed
Step 1: Install Dependencies
For Fetch-based clients:
npm install @x402-avm/fetch @x402-avm/avm algosdk
For Axios-based clients:
npm install @x402-avm/axios @x402-avm/avm algosdk axios
Step 2: Implement a ClientAvmSigner
The ClientAvmSigner interface is what bridges your wallet or private key to the x402 payment system.
Interface:
interface ClientAvmSigner {
address: string;
signTransactions(
txns: Uint8Array[],
indexesToSign?: number[],
): Promise<(Uint8Array | null)[]>;
}
For Node.js (private key):
import algosdk from "algosdk";
const secretKey = Buffer.from(process.env.AVM_PRIVATE_KEY!, "base64");
const address = algosdk.encodeAddress(secretKey.slice(32));
const signer = {
address,
signTransactions: async (txns: Uint8Array[], indexesToSign?: number[]) => {
return txns.map((txn, i) => {
if (indexesToSign && !indexesToSign.includes(i)) return null;
const decoded = algosdk.decodeUnsignedTransaction(txn);
const signed = algosdk.signTransaction(decoded, secretKey);
return signed.blob;
});
},
};
For Browser (@txnlab/use-wallet):
import { useWallet } from "@txnlab/use-wallet-react";
import type { ClientAvmSigner } from "@x402-avm/avm";
function useAvmSigner(): ClientAvmSigner | null {
const { activeAccount, signTransactions } = useWallet();
if (!activeAccount) return null;
return {
address: activeAccount.address,
signTransactions: async (txns: Uint8Array[], indexesToSign?: number[]) => {
return signTransactions(txns, indexesToSign);
},
};
}
Step 3: Create and Configure the x402Client
import { x402Client } from "@x402-avm/fetch"; // or "@x402-avm/axios"
import { registerExactAvmScheme } from "@x402-avm/avm/exact/client";
const client = new x402Client();
registerExactAvmScheme(client, { signer });
Step 4: Wrap Fetch or Axios
Fetch:
import { wrapFetchWithPayment } from "@x402-avm/fetch";
const fetchWithPay = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPay("https://api.example.com/premium-data");
Axios:
import axios from "axios";
import { wrapAxiosWithPayment } from "@x402-avm/axios";
const api = wrapAxiosWithPayment(axios.create(), client);
const response = await api.get("https://api.example.com/premium-data");
Step 5: Add Payment Policies (Optional)
Policies filter payment requirements before selection. They are applied in order:
import type { PaymentPolicy } from "@x402-avm/fetch";
const maxAmount: PaymentPolicy = (version, reqs) => {
return reqs.filter((r) => BigInt(r.amount ?? "0") <= BigInt("5000000"));
};
const preferAlgorand: PaymentPolicy = (version, reqs) => {
const algoReqs = reqs.filter((r) => r.network.startsWith("algorand:"));
return algoReqs.length > 0 ? algoReqs : reqs;
};
client.registerPolicy(preferAlgorand).registerPolicy(maxAmount);
Step 6: Add Lifecycle Hooks (Optional)
Monitor and control the payment lifecycle:
client.onBeforePaymentCreation(async ({ selectedRequirements }) => {
const amountUSDC = Number(selectedRequirements.amount) / 1_000_000;
console.log(`Paying $${amountUSDC.toFixed(6)} USDC`);
if (amountUSDC > 10) {
return { abort: true, reason: "Amount exceeds $10 limit" };
}
});
client.onAfterPaymentCreation(async () => {
console.log("Payment signed successfully");
});
client.onPaymentCreationFailure(async ({ error }) => {
console.error("Payment failed:", error.message);
});
Important Rules / Guidelines
- Always register a scheme before wrapping --
registerExactAvmScheme(client, { signer })must be called beforewrapFetchWithPaymentorwrapAxiosWithPayment - AVM_PRIVATE_KEY format -- Base64-encoded 64-byte key (32-byte seed + 32-byte public key)
- Address derivation -- Always use
algosdk.encodeAddress(secretKey.slice(32)), never the first 32 bytes - Single retry -- The wrapper retries exactly once after 402. If the retry also returns 402, the error is propagated
- Interceptor order for Axios -- Add your own interceptors first, then call
wrapAxiosWithPaymentlast - Config-based alternative -- Use
wrapFetchWithPaymentFromConfig/wrapAxiosWithPaymentFromConfigfor declarative setup without manualx402Clientconstruction - Wildcard networks -- Use
"algorand:*"in config-based setups to match any Algorand network
Config-Based Setup (Alternative)
Instead of creating an x402Client manually, use the config-based approach:
import { wrapFetchWithPaymentFromConfig, type x402ClientConfig } from "@x402-avm/fetch";
import { ExactAvmScheme } from "@x402-avm/avm";
const config: x402ClientConfig = {
schemes: [
{
network: "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
client: new ExactAvmScheme(signer),
},
],
policies: [maxAmount],
};
const fetchWithPay = wrapFetchWithPaymentFromConfig(fetch, config);
Common Errors / Troubleshooting
| Error | Cause | Solution |
|---|---|---|
Failed to parse payment requirements |
Server returned 402 with invalid body | Check server is running x402-compatible middleware |
Failed to create payment payload |
Insufficient balance or wrong network | Ensure USDC balance on the correct network |
Payment already attempted |
Server returned 402 after payment was sent | Payment was rejected; check facilitator logs |
No network/scheme registered |
Server requires an unregistered network | Register the needed scheme with registerExactAvmScheme |
Payment creation aborted |
A beforePaymentCreation hook returned abort |
Review hook logic; check amount limits |
All payment requirements were filtered out |
Policies removed all options | Relax policies or register additional schemes |
No client registered for x402 version: 2 |
No schemes registered at all | Call registerExactAvmScheme(client, { signer }) |
Reading Payment Receipts
After a successful paid request, check the response header:
import { decodePaymentResponseHeader } from "@x402-avm/fetch";
const paymentResponseHeader = response.headers.get("PAYMENT-RESPONSE");
if (paymentResponseHeader) {
const receipt = decodePaymentResponseHeader(paymentResponseHeader);
console.log("Transaction settled:", receipt);
}
References / Further Reading
- REFERENCE.md - Detailed API reference
- EXAMPLES.md - Complete code examples
- x402-avm Fetch Examples
- x402-avm Documentation