Status: This page is a compile-tested reference, not a drop-in CLI. The integrator must provide a secure wallet signer, a durable transactional store, and a cross-process per-order lock.
This reference shows the payment-critical parts of an Ambient headless subscription client. Use the Headless x402 subscriptions guide for endpoint examples, supported plans, account usage, key management, and current limitations. See Ambient pricing before choosing a client-side spending limit.
Registration creates a pending order and stores only the bootstrap key's prefix and hash. It does not create a team, usable API-key row, or subscription policy. Ambient creates those together when a valid settled grant completes. A renewal order uses the same payment state but has no bootstrap key.
Ambient's 12-month limit counts nonexpired current and future policies, unexpired pending-order months, and the requested months. Do not create speculative renewal intents: each live intent reserves capacity.
Use Node.js 20.18 or newer and pin the payment stack. viem and @solana/kit are signer-adapter dependencies; omit a rail's unused wallet package from a single-rail client.
npm install --save-exact \
@x402/[email protected] \
@x402/[email protected] \
@x402/[email protected] \
[email protected] \
@solana/[email protected]
Do not use an automatic paid-fetch wrapper for a subscription payment URL. The client must durably save one exact authorization before transmitting it and must not let generic HTTP retry middleware repeat the paid request.
The types below work for both registration and renewal. Construct PaymentOrder.bootstrap only from a registration response. Validate a registration response's completion_url is /billing/x402/subscription-complete, but always submit grants to the fixed HTTPS endpoint used below. Renewal requests accept the canonical planId field and the compatibility alias plan_id.
import { x402Client } from "@x402/core/client";
import {
decodePaymentRequiredHeader,
decodePaymentResponseHeader,
encodePaymentSignatureHeader,
} from "@x402/core/http";
import type {
PaymentPayload,
PaymentRequired,
PaymentRequirements,
SettleResponse,
} from "@x402/core/types";
import { ExactEvmScheme, type ClientEvmSigner } from "@x402/evm";
import { ExactSvmScheme, type ClientSvmSigner } from "@x402/svm";
const API_ORIGIN = "<https://api.ambient.xyz>";
const COMPLETION_URL = `${API_ORIGIN}/billing/x402/subscription-complete`;
const SOLANA = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
const SOLANA_USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const SOLANA_RECIPIENT = "AYYo37bgztqAgP6wp2S9SiYWcijr8gz2o14c8RGWojJZ";
const BASE = "eip155:8453";
const BASE_USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const BASE_RECIPIENT = "0x6992c688c56be442efe1e1ce7d5c95212ed7d59b";
type Network = typeof SOLANA | typeof BASE;
type TrustedOffer = PaymentRequirements & { network: Network };
type PlanId = "starter" | "basic" | "power" | "pro";
type Stage =
| "registered"
| "approved"
| "authorized"
| "attempted"
| "granted"
| "completed"
| "ambiguous";
type PaymentOrder = {
kind: "registration" | "renewal";
orderId: string;
planId: PlanId;
months: number;
paymentUrl: string;
bootstrap?: {
apiKey: string;
orderExpiresAt: string;
};
};
type Approval = {
orderId: string;
planId: PlanId;
months: number;
resourceUrl: string;
scheme: "exact";
network: Network;
asset: string;
recipient: string;
amountMicroUsdc: string;
expectedPayer: string;
approvedAt: string;
};
type PaidResponse = {
status: number;
headers: Record<string, string>;
paymentResponse?: string;
grantToken?: string;
grantSignature?: string;
body?: string;
};
type Completion = {
team_id: string;
plan_id: PlanId;
months: number;
current_period_end: string;
already_completed: boolean;
};
type OrderState = {
stage: Stage;
order: PaymentOrder;
paymentRequiredHeader?: string;
paymentRequired?: PaymentRequired;
selectedOffer?: PaymentRequirements;
approval?: Approval;
paymentPayload?: PaymentPayload;
paymentSignature?: string;
transmissionAttemptedAt?: string;
paidResponse?: PaidResponse;
settlementReceipt?: SettleResponse;
completion?: Completion;
ambiguityReason?: string;
};
type StoredState = { revision: number; value: OrderState };
interface DurableOrderStore {
// This lock must exclude writers in every process, not only this Node process.
withExclusiveLock<T>(orderId: string, fn: () => Promise<T>): Promise<T>;
read(orderId: string): Promise<StoredState>;
// Atomically commits and returns the new revision, or throws on a mismatch.
compareAndSwap(
orderId: string,
expectedRevision: number,
next: OrderState,
): Promise<StoredState>;
}
type WalletChoice =
| { network: typeof BASE; signer: ClientEvmSigner }
| { network: typeof SOLANA; signer: ClientSvmSigner };
function assertOrderIdentity(orderId: string, state: OrderState): void {
if (state.order.orderId !== orderId) {
throw new Error("Loaded state belongs to a different order");
}
}
compareAndSwap must be durable before it returns. A filesystem implementation needs an atomic same-directory replacement, file and directory flushes, owner-only permissions, and a lock identity that survives replacement. A database implementation should use a row lock or equivalent transaction. Never log the API key, payment signature, grant, wallet material, or complete stored response headers.
The live 402 is authoritative for availability and amount, but it is not a trusted recipient list. The selected offer must match the allowlist below. Base addresses compare lowercase; Solana identifiers remain case-sensitive. The Base checks require EIP-3009, canonical USDC's EIP-712 domain, and a positive authorization lifetime no longer than the currently trusted 300-second maximum.
type Preview = {
paymentRequiredHeader: string;
paymentRequired: PaymentRequired;
selectedOffer: TrustedOffer;
};
function positiveAmount(value: unknown): bigint {
if (typeof value !== "string" || !/^[1-9][0-9]*$/.test(value)) {
throw new Error("Amount must be a canonical positive decimal integer");
}
return BigInt(value);
}
function formatMicroUsdc(amount: bigint): string {
const whole = amount / 1_000_000n;
const fraction = (amount % 1_000_000n)
.toString()
.padStart(6, "0")
.replace(/0+$/, "");
return fraction ? `${whole}.${fraction}` : whole.toString();
}
function canonicalIdentifier(network: Network, value: string): string {
const trimmed = value.trim();
if (!trimmed) throw new Error("Identifier is empty");
return network === BASE ? trimmed.toLowerCase() : trimmed;
}
function isTrustedOffer(offer: PaymentRequirements): offer is TrustedOffer {
if (offer.scheme !== "exact") return false;
if (offer.network === SOLANA) {
return offer.asset === SOLANA_USDC && offer.payTo === SOLANA_RECIPIENT;
}
if (offer.network === BASE) {
return (
offer.asset.toLowerCase() === BASE_USDC &&
offer.payTo.toLowerCase() === BASE_RECIPIENT &&
offer.extra.assetTransferMethod === "eip3009" &&
offer.extra.name === "USD Coin" &&
offer.extra.version === "2" &&
Number.isInteger(offer.maxTimeoutSeconds) &&
offer.maxTimeoutSeconds > 0 &&
offer.maxTimeoutSeconds <= 300
);
}
return false;
}
function selectTrustedOffer(
required: PaymentRequired,
paymentUrl: string,
preferredNetwork: Network,
): TrustedOffer {
if (required.x402Version !== 2) throw new Error("Expected x402 v2");
if (required.resource?.url !== paymentUrl) {
throw new Error("Challenge resource differs from the saved payment URL");
}
const amounts = new Set(
required.accepts.map((offer) => positiveAmount(offer.amount).toString()),
);
if (amounts.size !== 1) throw new Error("Offered rails disagree on amount");
const offer = required.accepts
.filter(isTrustedOffer)
.find((candidate) => candidate.network === preferredNetwork);
if (!offer) throw new Error("No trusted offer for the selected network");
return offer;
}
async function previewPayment(
paymentUrl: string,
preferredNetwork: Network,
): Promise<Preview> {
const response = await fetch(paymentUrl, {
method: "POST",
redirect: "manual",
});
if (response.status !== 402) {
throw new Error(`Expected unsigned 402, received ${response.status}`);
}
const header = response.headers.get("Payment-Required");
if (!header) throw new Error("Payment-Required header is missing");
const paymentRequired = decodePaymentRequiredHeader(header);
const selectedOffer = selectTrustedOffer(
paymentRequired,
paymentUrl,
preferredNetwork,
);
return {
paymentRequiredHeader: header,
paymentRequired,
selectedOffer,
};
}
async function saveApproval(
store: DurableOrderStore,
orderId: string,
preview: Preview,
wallet: WalletChoice,
maximumMicroUsdc: bigint,
): Promise<OrderState> {
return store.withExclusiveLock(orderId, async () => {
const stored = await store.read(orderId);
const state = stored.value;
assertOrderIdentity(orderId, state);
if (state.paymentSignature || state.transmissionAttemptedAt) {
throw new Error("An authorization already exists for this order");
}
if (state.stage !== "registered" && state.stage !== "approved") {
throw new Error(`Cannot approve an order in stage ${state.stage}`);
}
const offer = selectTrustedOffer(
preview.paymentRequired,
state.order.paymentUrl,
preview.selectedOffer.network,
);
const amount = positiveAmount(offer.amount);
if (amount > maximumMicroUsdc) {
throw new Error(`Charge ${formatMicroUsdc(amount)} USDC exceeds approval`);
}
if (wallet.network !== offer.network) {
throw new Error("Wallet network differs from the selected offer");
}
const approval: Approval = {
orderId: state.order.orderId,
planId: state.order.planId,
months: state.order.months,
resourceUrl: state.order.paymentUrl,
scheme: "exact",
network: offer.network,
asset: canonicalIdentifier(offer.network, offer.asset),
recipient: canonicalIdentifier(offer.network, offer.payTo),
amountMicroUsdc: amount.toString(),
expectedPayer: canonicalIdentifier(
offer.network,
wallet.signer.address,
),
approvedAt: new Date().toISOString(),
};
const committed = await store.compareAndSwap(orderId, stored.revision, {
...state,
stage: "approved",
paymentRequiredHeader: preview.paymentRequiredHeader,
paymentRequired: preview.paymentRequired,
selectedOffer: offer,
approval,
});
return committed.value;
});
}
Show the user or calling policy the exact order, plan, months, wallet.signer.address, rail, asset, recipient, and formatMicroUsdc(amount) before calling saveApproval. The maximum is an independent client policy, not a hardcoded plan-price table.
A payment URL may remain usable for roughly 24 hours, but an authorization does not. Base authorizations currently use a short expiry window (typically 300 seconds), while Solana validity also depends on a recent blockhash. Create the authorization immediately before transmission.
The lock is acquired and current state is reread before the signer runs. If an authorization already exists, the function returns it without signing again. The official 2.11.0 client awaits onAfterPaymentCreation, so the payload and exact encoded header are durable before createPaymentPayload returns.
function offerMatchesApproval(
offer: PaymentRequirements,
approval: Approval,
): boolean {
if (!isTrustedOffer(offer) || offer.network !== approval.network) return false;
return (
offer.scheme === approval.scheme &&
canonicalIdentifier(approval.network, offer.asset) === approval.asset &&
canonicalIdentifier(approval.network, offer.payTo) === approval.recipient &&
positiveAmount(offer.amount).toString() === approval.amountMicroUsdc
);
}
async function createSavedAuthorization(
store: DurableOrderStore,
orderId: string,
wallet: WalletChoice,
): Promise<OrderState> {
return store.withExclusiveLock(orderId, async () => {
const stored = await store.read(orderId);
const state = stored.value;
assertOrderIdentity(orderId, state);
if (state.paymentSignature) return state;
if (
state.stage !== "approved" ||
!state.paymentRequired ||
!state.selectedOffer ||
!state.approval
) {
throw new Error("Order does not have a durable approved offer");
}
const approval = state.approval;
const paymentRequired = state.paymentRequired;
if (
approval.orderId !== state.order.orderId ||
approval.planId !== state.order.planId ||
approval.months !== state.order.months ||
approval.resourceUrl !== state.order.paymentUrl
) {
throw new Error("Approved terms differ from the saved order");
}
if (wallet.network !== approval.network) {
throw new Error("Wallet network differs from the approved network");
}
if (
canonicalIdentifier(wallet.network, wallet.signer.address) !==
approval.expectedPayer
) {
throw new Error("Wallet payer differs from the approved payer");
}
const client = new x402Client((_version, offers) => {
const selected = offers.find((offer) =>
offerMatchesApproval(offer, approval),
);
if (!selected) throw new Error("Approved offer is no longer selectable");
return selected;
});
if (wallet.network === BASE) {
client.register(BASE, new ExactEvmScheme(wallet.signer));
} else {
client.register(SOLANA, new ExactSvmScheme(wallet.signer));
}
let saved: OrderState | undefined;
client.onAfterPaymentCreation(async ({ paymentPayload }) => {
const committed = await store.compareAndSwap(orderId, stored.revision, {
...state,
stage: "authorized",
paymentPayload,
paymentSignature: encodePaymentSignatureHeader(paymentPayload),
});
saved = committed.value;
});
await client.createPaymentPayload(paymentRequired);
if (!saved?.paymentSignature) {
throw new Error("Authorization was not durably saved");
}
return saved;
});
}