x402 upto vs MPP Sessions

There’s more than one way to pay through x402. We break down the three payment schemes — a one-time fixed payment, pay-as-you-go usage billing, and accumulating payment channels — with technical details and code examples in TypeScript, Go, and Python.

In x402, payment is handled by the payment scheme (scheme): it defines the semantics of a payment — that is, exactly how the amount is calculated and charged. A scheme isn’t tied to any particular blockchain — it works on top of any supported network: EVM (Ethereum, Base, Optimism…), Solana, TON, Stellar, and others. Today the protocol supports three schemes:

SchemeSemanticsTypical scenario
exactA one-time payment of a specific amountA fixed rate — downloading a file, one-time access to a resource.
uptoPay for actual usage, but no more than a stated maximumLLM text generation, traffic, compute — “metered” billing. For cases where the server can’t determine the exact amount in advance.
batch-settlementMany small payments through a payment channel, later closed out with a single final transaction.Many micropayments from a single client

Scheme 1: exact — fixed price

exact is the simplest and most basic scheme. The seller declares one specific amount, the buyer signs a payment for exactly that amount, and the facilitator settles it. No recalculation: whatever was declared is what gets charged.

This is a good starting point for most projects. If you’re not sure which scheme you need — you almost certainly need exact.

How it works

On EVM, the exact scheme supports two token transfer mechanisms:

MethodDescription
eip3009Uses the token’s built-in transferWithAuthorization function (EIP-3009). This is how USDC works. It’s the default method when the token supports it.
permit2Uses Uniswap Permit2 plus an x402 proxy. Lets you accept any ERC-20 token, even one without EIP-3009. May require a one-time approve.

EIP-3009 is a standard that lets a token holder sign a transfer authorization off-chain (gas-free), which anyone (the facilitator) can then execute on-chain. The buyer signs a structure following the EIP-712 standard (typed data):

// go/mechanisms/evm/types.go
type ExactEIP3009Authorization struct {
    From        string // payer address
    To          string // recipient address
    Value       string // amount in the token's smallest units
    ValidAfter  string // not valid before this time (Unix)
    ValidBefore string // not valid after this time (Unix)
    Nonce       string // 32-byte unique nonce (replay protection)
}

The signed authorization, along with the signature, is sent back to the server in a header. The facilitator verifies the signature and submits the on-chain transaction — the facilitator pays the gas, not the buyer. This is an important detail: the buyer doesn’t need the network’s native “gas” token; a stablecoin is enough.

For tokens without EIP-3009, Permit2 is used — a single “authorizer” contract from Uniswap. The buyer signs a PermitWitnessTransferFrom structure where, besides the token and amount, there’s a witness field — additional data (the recipient address) that the x402 on-chain proxy verifies:

type Permit2Authorization struct {
    From      string                  // owner/signer
    Permitted Permit2TokenPermissions // token + amount
    Spender   string                  // x402Permit2Proxy address
    Nonce     string
    Deadline  string                  // signature expiry
    Witness   Permit2Witness          // { To, ValidAfter }
}

Example: server (TypeScript)

The server registers a scheme implementation for the desired networks and protects a route:

import { HTTPFacilitatorClient } from "@x402/core/server";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { ExactSvmScheme } from "@x402/svm/exact/server";

const facilitatorClient = new HTTPFacilitatorClient({
  url: "https://x402.org/facilitator",
});

const resourceServer = new x402ResourceServer(facilitatorClient)
  .register("eip155:84532", new ExactEvmScheme())
  .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme());

app.use(
  paymentMiddleware(
    {
      "GET /weather": {
        accepts: [
          { scheme: "exact", price: "$0.001", network: "eip155:84532", payTo: "0xYourAddress" },
          { scheme: "exact", price: "$0.001", network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", payTo: "YourSolanaAddress" },
        ],
        description: "Weather data",
        mimeType: "application/json",
      },
    },
    resourceServer,
  ),
);

Example: client (Go)

The client registers the scheme with its own signer (a private key). The eip155:* pattern means “for any EVM network”:

import (
    x402 "github.com/x402-foundation/x402/go/v2"
    exactevm "github.com/x402-foundation/x402/go/v2/mechanisms/evm/exact/client"
    exactsvm "github.com/x402-foundation/x402/go/v2/mechanisms/svm/exact/client"
    evmsigners "github.com/x402-foundation/x402/go/v2/signers/evm"
    svmsigners "github.com/x402-foundation/x402/go/v2/signers/svm"
)

evmSigner, err := evmsigners.NewClientSignerFromPrivateKey(os.Getenv("EVM_PRIVATE_KEY"))
if err != nil {
    log.Fatal(err)
}
svmSigner, err := svmsigners.NewClientSignerFromPrivateKey(os.Getenv("SVM_PRIVATE_KEY"))
if err != nil {
    log.Fatal(err)
}

x402Client := x402.Newx402Client().
    Register("eip155:*", exactevm.NewExactEvmScheme(evmSigner, nil)).
    Register("solana:*", exactsvm.NewExactSvmScheme(svmSigner))

Scheme 2: upto — pay-as-you-go usage billing

With exact, the price is known in advance. But what do you do when the final cost depends on how many resources are actually consumed? You can’t determine that ahead of time.

The upto scheme solves exactly this. The seller declares a maximum price per request. The buyer signs a payment for that maximum. Then, when building the response, the server picks the actual amount ≤ the maximum and charges only that.

This scheme is useful for:

  • LLM text generation (paying for the tokens actually generated).
  • Paying by traffic volume, output size, or processing time.

This is “metered billing” within a single request: you “reserve” a limit up front, and you pay for what you actually use.

How it works

Unlike exact, the upto scheme always uses Permit2, because the final amount is unknown at the moment the buyer signs. The buyer authorizes a maximum, and the right to charge the actual amount is granted to a specific facilitator.

The key difference is in the witness structure — here a facilitator field is added:

// go/mechanisms/evm/types.go
type UptoPermit2Witness struct {
    To          string // fund recipient
    Facilitator string // ONLY this address can call settle() on-chain
    ValidAfter  string // Unix start time
}

This is an important safeguard: the signature is bound to a specific facilitator. Only that facilitator can perform settlement. The facilitator declares its address (facilitatorAddress) in the payment requirements, and the client “bakes” it into the signature.

Settlement overrides: how the server specifies the actual amount

The magic of upto is in the settlement override. After the server has computed the real cost, it calls setSettlementOverrides with the actual amount. The amount can be specified in three ways:

FormatExampleMeaning
Raw atomic units"50000"Charge exactly 50,000 of the token’s smallest units
Percentage"50%"Charge 50% of the route’s maximum
Dollar price"$0.05"Convert $0.05 into token units

Setting "0" means “charge nothing for this request.”

Example: server (TypeScript)

import { paymentMiddleware, setSettlementOverrides, x402ResourceServer } from "@x402/express";
import { UptoEvmScheme } from "@x402/evm/upto/server";

const resourceServer = new x402ResourceServer(facilitatorClient)
  .register("eip155:84532", new UptoEvmScheme());

app.use(paymentMiddleware({
  "GET /api/generate": {
    accepts: {
      scheme: "upto",
      price: "$0.10",          // the maximum the buyer authorizes
      network: "eip155:84532",
      payTo: "0xYourAddress",
    },
    description: "AI text generation billed by usage",
  },
}, resourceServer));

app.get("/api/generate", (req, res) => {
  const actualUsage = computeActualCost();        // e.g., based on the number of LLM tokens
  setSettlementOverrides(res, { amount: String(actualUsage) });  // charge for actual usage
  res.json({ result: "..." });
});

Example: server (Go)

routes := x402http.RoutesConfig{
    "GET /api/generate": {
        Accepts: x402http.PaymentOptions{
            { Scheme: "upto", Price: "$0.10", Network: "eip155:84532", PayTo: "0xYourAddress" },
        },
        Description: "AI text generation billed by usage",
    },
}

mux.HandleFunc("GET /api/generate", func(w http.ResponseWriter, r *http.Request) {
    actualUsage := computeActualCost()
    nethttpmw.SetSettlementOverrides(w, &x402.SettlementOverrides{
        Amount: fmt.Sprintf("%d", actualUsage),
    })
    _ = json.NewEncoder(w).Encode(map[string]string{"result": "..."})
})

Example: client (Go)

The client can register upto alongside exact if it accesses both fixed-price and “metered” resources:

x402Client := x402.Newx402Client().
    Register("eip155:*", exactevm.NewExactEvmScheme(evmSigner, nil)).
    Register("eip155:*", uptoevm.NewUptoEvmScheme(evmSigner, nil))

Scheme 3: batch-settlement — channels for micropayments

Imagine an API that’s called thousands of times a minute for a fraction of a cent per call. If you settle each such micropayment as a separate on-chain transaction, then:

  • the gas fees would dwarf the payment itself many times over;
  • each settlement would wait for block confirmation — which is slow.

batch-settlement solves both problems using unidirectional payment channels (state channels). Funds are deposited into escrow once, and after that each request is confirmed by a cheap off-chain signature. On-chain settlement happens rarely and in batches.

When to choose it:

  • Repeated, high-frequency API calls.
  • Any workload where many small individual payments would be too expensive or too slow.

How it works: vouchers and channels

The channel lifecycle:

  1. Deposit. The client deposits ERC-20 funds into on-chain escrow once. The deposit goes through EIP-3009 or Permit2 and is submitted by the facilitator.
  2. Voucher. Each paid request carries a signed cumulative voucher — the total amount the server is entitled to withdraw from the channel as of that moment. Vouchers are signed off-chain.
  3. Verify. The server verifies the voucher and returns the response immediately, without waiting for an on-chain transfer.
  4. Claim. The server-side channel manager periodically collects the latest vouchers from many channels in a single transaction.
  5. Settle. The claimed funds are transferred to the recipient in a separate transaction.
  6. Refund. Any remaining funds can be returned to the payer after all vouchers have been claimed.

Because vouchers are cumulative, the server only needs to present the last voucher of a channel on-chain — it contains the final total. Thousands of requests collapse into a single settlement signature.

The price still sets a maximum per request, and the server can charge less via the same settlement overrides as in upto.

Deposit

The client doesn’t deposit just enough for a single request — that would be inefficient. It deposits a reserve that’s a multiple of the maximum price:

FieldDescription
depositMultiplierDeposits amount × multiplier of the declared per-request maximum. Defaults to 5, minimum 3.
depositStrategyOptional callback: limits, dynamic deposit sizing, opting out of auto-refill.

Example: server (Go)

The server needs to set up channel storage and start a manager that will claim, settle, and refund funds on a schedule:

cfg := &batchedserver.BatchSettlementEvmSchemeServerConfig{
    ReceiverAuthorizerSigner: receiverAuthorizerSigner,
    WithdrawDelay:            86400,
    Storage: batchedserver.NewFileChannelStorage(batchsettlement.FileChannelStorageOptions{
        Directory: "./channels",
    }),
}

scheme := batchedserver.NewBatchSettlementEvmScheme(receiverAddress, cfg)
manager := scheme.CreateChannelManager(facilitatorClient, x402.Network("eip155:84532"))
manager.Start(batchedserver.AutoSettlementConfig{
    ClaimIntervalSecs:  60,   // collect vouchers every minute
    SettleIntervalSecs: 300,  // transfer to the recipient every 5 minutes
    RefundIntervalSecs: 3600, // refund idle channels once an hour
    MaxClaimsPerBatch:  100,  // up to 100 channels in a single transaction
})

routes := x402http.RoutesConfig{
    "GET /weather": {
        Accepts: x402http.PaymentOptions{
            { Scheme: batchsettlement.SchemeBatched, Price: "$0.01", Network: x402.Network("eip155:84532"), PayTo: receiverAddress },
        },
        Description: "Weather data",
        MimeType:    "application/json",
    },
}

Example: client (TypeScript)

The client SDK handles everything: deposits, signing vouchers, restoring channel state, and resyncing through “corrective” 402 responses. The developer just needs to register the scheme and make ordinary requests:

import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { toClientEvmSigner } from "@x402/evm";
import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/client";
import { createPublicClient, http } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
const signer = toClientEvmSigner(account, publicClient);

const batchScheme = new BatchSettlementEvmScheme(signer, {
  depositPolicy: { depositMultiplier: 5 },
});

const client = new x402Client();
client.register("eip155:*", batchScheme);

const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPayment("https://api.example.com/weather");

The channel manager stores the last voucher and session state for each channel. For a single process, file storage (FileChannelStorage) works fine. For serverless and multi-service setups, you need shared atomic storage — Redis, for example — so that channel updates stay consistent across processes.


Bonus: how x402 verifies that you are who you say you are

x402 supports more than just regular wallets (EOAs). The EVM implementation can verify several types of signatures, as you can see in the code (go/mechanisms/evm/):

  • EOA (verify_eoa.go) — a classic ECDSA signature with a private key. The most common case.
  • EIP-1271 (verify_1271.go) — signatures from smart contract wallets (Safe, Argent, etc.). The contract itself decides whether the signature is valid, via the isValidSignature method.
  • ERC-6492 (erc6492.go) — signatures from smart wallets that haven’t been deployed on-chain yet. The signature is “wrapped” with deployment data (factory address + calldata), so it can be verified even before the wallet is created.
  • EIP-7702 (erc7702.go) — a new standard that lets a regular EOA temporarily “take on” smart-contract code within a transaction.

All payment authorizations are signed using the EIP-712 standard — “typed data” that the wallet shows the user in a readable form (domain, message type, fields) rather than as an unreadable hash. The TypedDataDomain structure binds the signature to a specific contract and network (chainId), which prevents a signature from being reused on a different network:

type TypedDataDomain struct {
    Name              string
    Version           string
    ChainID           *big.Int  // binding to a specific network
    VerifyingContract string    // binding to a specific contract
}

All three schemes can be combined on a single server: different routes, different schemes. And the server can offer several networks at once in accepts (for example, EVM and Solana), with the client choosing the one for which it has funds and a registered implementation.

The schemes cover fundamentally different payment patterns:

  • exact — “pay exactly this much” for fixed prices;
  • upto — “pay for what you use, but no more than the limit” for usage-based billing;
  • batch-settlement — “accumulate and settle in batches” for high-frequency micropayments.