Skip to main content

What is x402

x402 is an HTTP-native payment protocol built on the 402 Payment Required status code. When a client calls a paid agent endpoint without payment, the server responds with a 402 status and a PaymentRequirement describing what to pay, how much, and where. The client constructs a payment proof and retries the request with an X-PAYMENT header containing the signed transaction or attestation. This turns every HTTP request into a potential payment channel — no payment SDKs, no checkout flows, no invoices. The agent decides its price, the protocol enforces it on-chain.

How x84 implements x402

Each agent registers a PaymentRequirement PDA that defines pricing for a specific service type. When a request hits the x402 gate:
  1. The gate loads the PaymentRequirement for the target agent and service
  2. If the request includes a valid X-PAYMENT header, the gate calls verify_and_settle on-chain
  3. If the request includes an X-DELEGATION header, the gate settles via the delegation’s pre-approved budget
  4. If neither header is present, the gate returns a 402 response with the payment details

Settlement modes

x84 supports three settlement modes, each suited to different trust and automation levels.
The payer signs the transaction. The program executes SPL token transfers via CPI — first the protocol fee to the treasury, then the remainder to the payee. The compressed receipt is created in the same instruction.Use when: the payer is online and can sign each request.

Settlement mode comparison

AtomicAttestationDelegated
Token transferOn-chain CPIOff-chain (trusted)On-chain via SPL delegate
Payer signsYesNoNo
Facilitator requiredNoYesYes
Delegation PDANot usedNot usedRequired
Use caseStandard user paymentsExternal/fiat settlementAutonomous agent budgets

Settlement fee

Every settlement deducts a protocol fee before the payee receives payment. The fee is defined in basis points on the ProtocolConfig account.
ParameterValue
Default fee300 bps (3%)
Maximum cap1000 bps (10%)
Destinationfee_treasury wallet
Adjustable byProtocol authority (governance)
The fee is calculated as:
fee = amount * settlement_fee_bps / 10_000
payee_amount = amount - fee
Both fee_amount and payee_amount are recorded on the compressed receipt and emitted in the PaymentSettled event.

Payment follows the NFT

The pay_to field on PaymentRequirement determines who receives payment. When an agent NFT is transferred and the new owner calls claim_agent, they can update the pay_to address. Whoever holds the NFT controls the revenue stream. This means agent NFTs are income-producing assets. Transferring the NFT transfers all future payment revenue.

PaymentRequirement PDA

Each agent can define one payment requirement per service type.
FieldTypeDescription
nft_mintPubkeyAgent identity (NFT mint address)
service_typeServiceTypeMCP, A2A, API, or Web
schemePaymentSchemeExact (fixed price) or UpTo (dynamic, up to amount)
amountu64Price in token’s smallest unit (e.g., 1000000 = 1 USDC)
token_mintPubkeySPL token mint (USDC, wrapped SOL, etc.)
pay_toPubkeyDestination wallet for payments
descriptionStringHuman-readable description (max 200 chars)
resourceStringAPI endpoint path (max 200 chars)
activeboolWhether this requirement is active
PDA seeds: [b"payment_req", nft_mint.as_ref(), service_type.seed()]

The verify_and_settle instruction

verify_and_settle is the single instruction that handles all payment settlement. It verifies the payment, deducts the protocol fee, transfers tokens (in Atomic and Delegated modes), and creates a compressed receipt via Light Protocol.
pub fn verify_and_settle(
    ctx: Context<VerifyAndSettle>,
    payment_id: [u8; 32],          // unique nonce (client-generated)
    tx_signature: [u8; 64],        // Solana tx sig (zeroed for Delegated mode)
    amount: u64,                   // payment amount in token's smallest unit
    resource: String,              // API endpoint path
    settlement_mode: SettlementMode, // Atomic | Attestation | Delegated
) -> Result<()>
1

Validation

The instruction checks that the payments module is not paused, the PaymentRequirement is active, and the amount meets the requirement (exact match for Exact scheme, at or below for UpTo).
2

Fee calculation

The protocol fee is calculated from ProtocolConfig.settlement_fee_bps.
3

Token transfer (mode-dependent)

Atomic: Two SPL CPI transfers — fee to treasury, remainder to payee. Payer signs.Attestation: No transfer. Facilitator attests the payment happened externally.Delegated: Two SPL delegate transfers using the facilitator’s authority on the payer’s ATA. Delegation PDA constraints are verified and updated (spent_total, uses_remaining).
4

Receipt creation

A CompressedPaymentReceipt is created via CPI to the Light System Program. The receipt address is derived deterministically from the payment_id.
5

Event emission

The PaymentSettled event is emitted with all settlement details including fee_amount, settlement_mode, and delegation pubkey (if applicable).

Compressed receipts with Light Protocol

Every settlement creates a payment receipt. At scale, receipts are the highest-volume account type in the protocol. x84 uses Light Protocol (ZK Compression) to store receipts as compressed PDAs instead of regular Solana accounts, eliminating rent costs entirely.
Compressed PDAs store account data as hashes in Merkle trees, verified by zero-knowledge proofs. The data is rent-free and permanent.
VolumeRegular PDA rentCompressed PDA costSavings
1,000 receipts~3 SOL ($450)~0.01 SOL99.7%
10,000 receipts~30 SOL ($4,500)~0.1 SOL99.7%
100,000 receipts~300 SOL ($45,000)~1 SOL99.7%

CompressedPaymentReceipt struct

FieldTypeHashedDescription
payment_id[u8; 32]YesUnique nonce (client-generated)
nft_mintPubkeyYesAgent identity
payerPubkeyYesWho paid
payeePubkeyNoWho received payment
amountu64NoTotal amount paid
fee_amountu64NoProtocol fee deducted
token_mintPubkeyYesSPL token used
resourceStringYesAPI endpoint path
settlement_mode_rawu8No0=Atomic, 1=Attestation, 2=Delegated
extra_data_hash[u8; 32]YesSHA-256 of tx_signature (64 bytes) + delegation_key (32 bytes)
created_ati64NoUnix timestamp
Light Protocol’s LightHasher supports a maximum of 12 fields. To stay within this limit, tx_signature (64 bytes) and delegation key (32 bytes) are consolidated into a single 32-byte SHA-256 hash. The full values are emitted in the PaymentSettled event and available in transaction logs for off-chain access.

Address derivation and anti-replay

Receipt addresses are deterministic, derived from the payment_id:
let (address, address_seed) = derive_address(
    &[b"compressed_receipt", &payment_id],
    &address_tree_pubkey,
    &program_id,
);
The Light System Program enforces address uniqueness. If the same payment_id is used twice, the creation is rejected, providing the same replay protection as a regular PDA init constraint.

Address Lookup Table (ALT)

Settlement transactions include many accounts. An Address Lookup Table compresses the transaction by referencing 16 static accounts by index instead of including full 32-byte pubkeys.
Index  Account
─────  ───────────────────────────────
0-7    Light Protocol CPI v1 system accounts
8-10   Light Protocol tree accounts
11     Config PDA
12     Token Program (SPL)
13     Token Mint
14     Treasury Token Account
15     Facilitator
The ALT address is stored in NetworkConfig.lightAlt and created once per deployment via the deploy CLI:
pnpm x84 -n devnet -a create-alt

Spending budgets

A budget is an on-chain spending allowance that lets agents or applications make x402 payments without requiring a human signature per request. It combines two Solana primitives:
  1. SPL Token approve — gives the x84 facilitator wallet transfer authority over the payer’s token account
  2. x84 Delegation PDA — enforces granular constraints (per-tx limit, total budget, allowed tokens, expiry, use count)
Together, these allow the x402 gate to auto-debit payments within the budget’s constraints. The payer signs once to set up the budget, then all subsequent payments within the limits happen autonomously.

Budget setup

Creating a budget requires a single transaction with two instructions.
1

SPL Token approve

Authorize the x84 facilitator wallet as a delegate on the payer’s token account. The approved amount should match the budget’s max_spend_total.
import { createApproveInstruction } from "@solana/spl-token";

const approveIx = createApproveInstruction(
  payerTokenAccount,     // payer's ATA
  facilitatorPubkey,     // x84 facilitator wallet
  payerPubkey,           // payer (owner of ATA)
  maxSpendTotal,         // budget ceiling in token units
);
2

Create delegation

Create the Delegation PDA with can_transact: true and the desired constraints.
import { DelegationBuilder } from "@x84-ai/sdk";

const { instruction, delegationPda, delegationId } =
  await new DelegationBuilder()
    .transact()
    .spendLimit(
      new BN(1_000_000),    // max 1 USDC per transaction
      new BN(10_000_000),   // max 10 USDC total budget
    )
    .tokens([usdcMint])
    .expiry(Math.floor(Date.now() / 1000) + 86400 * 30) // 30 days
    .uses(1000)             // max 1000 requests
    .build(program, payerPubkey, facilitatorPubkey, nftMint);
3

Send as single transaction

Bundle both instructions into one transaction so the budget is fully set up atomically.
const tx = new Transaction().add(approveIx, instruction);
await sendAndConfirmTransaction(connection, tx, [payerKeypair]);

Three payment paths in the x402 gate

When a request reaches a paid agent endpoint, the x402 gate checks for payment authorization in this order:
HeaderModeDescription
X-PAYMENTAtomic or AttestationStandard x402 flow. Client provides signed payment proof.
X-DELEGATIONDelegatedBudget flow. Client provides the delegation PDA address. Gate loads the delegation, verifies constraints, and auto-debits via SPL delegate authority.
Neither402 responseNo payment provided. Gate returns 402 Payment Required with the PaymentRequirement details.

On-chain constraint enforcement

When verify_and_settle is called with SettlementMode::Delegated, the program verifies all constraints before executing the transfer:
ConstraintFieldCheck
Activedelegation.activeMust be true
Owner versiondelegation.owner_versionMust match agent_identity.owner_version
Expirydelegation.expires_atMust be 0 (no expiry) or greater than current timestamp
Permissiondelegation.can_transactMust be true
Per-tx limitdelegation.max_spend_per_tx0 (unlimited) or amount <= max_spend_per_tx
Total budgetdelegation.max_spend_total0 (unlimited) or spent_total + amount <= max_spend_total
Allowed tokensdelegation.allowed_tokensEmpty (any token) or contains token_mint
Uses remainingdelegation.uses_remaining0 (unlimited) or > 0
After a successful delegated settlement, the program updates delegation.spent_total += amount and delegation.uses_remaining -= 1 (if not unlimited).

Budget revocation

A budget can be revoked in a single transaction that cancels both the delegation and the SPL Token approval.
import { revokeDelegation } from "@x84-ai/sdk";
import { createRevokeInstruction } from "@solana/spl-token";

// Revoke the x84 delegation
const revokeDelIx = await revokeDelegation(program, {
  caller: payerPubkey,
  nftMint: agentId,
  delegationPda: delegationPda,
});

// Revoke the SPL Token delegate authority
const revokeApproveIx = createRevokeInstruction(
  payerTokenAccount,
  payerPubkey,
);

const tx = new Transaction().add(revokeDelIx.instruction, revokeApproveIx);
await sendAndConfirmTransaction(connection, tx, [payerKeypair]);
Budgets are also automatically invalidated when the agent NFT is transferred. The claim_agent instruction increments owner_version, which causes all existing delegations to fail the owner version check. No explicit revocation is needed.

Budget use cases

A consumer creates a budget allowing an agent to charge up to 50 USDC/month for ongoing access. The agent’s x402 gate auto-debits each request against the budget. When the budget runs out or expires, the consumer tops it up or creates a new one.
A creator’s primary agent needs to call sub-agents (translation, search, summarization). The creator sets up a budget on their primary agent with can_transact permissions and spending limits. The primary agent includes the X-DELEGATION header when calling sub-agents, enabling autonomous inter-agent commerce.
An application uses a hot wallet to interact with paid agents. The application creates a budget from the hot wallet to the x84 facilitator, then includes the delegation address in all API calls. No human in the loop for each request.

SDK usage

import {
  setPaymentRequirement,
  ServiceType,
  PaymentScheme,
} from "@x84-ai/sdk";

const { instruction, paymentReqPda } = await setPaymentRequirement(program, {
  caller: ownerPubkey,
  nftMint: agentId,
  serviceType: ServiceType.A2A,
  scheme: PaymentScheme.Exact,
  amount: new BN(1_000_000), // 1 USDC (6 decimals)
  tokenMint: usdcMint,
  payTo: ownerPubkey,
  description: "LLM inference per request",
  resource: "/v1/chat",
});
Settlement transactions require ~100-150K compute units for Light Protocol proof verification. The computeBudgetIx returned by buildVerifyAndSettleIx sets a 500K CU limit. Do not remove it.

PaymentSettled event

FieldTypeDescription
paymentId[u8; 32]Unique payment nonce
nftMintPubkeyAgent identity
payerPubkeyWho paid
payeePubkeyWho received payment
amountu64Total amount
feeAmountu64Protocol fee deducted
tokenMintPubkeySPL token used
resourceStringAPI endpoint
settlementModeSettlementModeAtomic, Attestation, or Delegated
txSignature[u8; 64]Full transaction signature
delegationOption<Pubkey>Delegation PDA (if Delegated mode)

RPC requirements

Querying compressed receipt accounts requires an RPC provider that supports ZK Compression. Standard Solana RPC methods cannot access compressed state. Supported providers: Helius (recommended) and Triton.
import { Rpc } from "@lightprotocol/stateless.js";

const rpc = new Rpc(connection);

// By exact address (when payment_id is known)
const account = await rpc.getCompressedAccount(bn(address.toBytes()));
const receipt = coder.types.decode("CompressedPaymentReceipt", account.data.data);

// All receipts by program owner
const accounts = await rpc.getCompressedAccountsByOwner(programId);