Core Concepts 8 min read

Upto Scheme

Usage-based payments with maximum amount authorization

The upto scheme lets a client authorize a maximum amount, while the actual charge is determined at settlement time based on resource consumption. This is designed for scenarios where the final cost isn’t known until after the request is fulfilled.

Use Cases

  • LLM token generation — client authorizes up to $5, actual charge based on tokens produced
  • Bandwidth metering — pay per byte transferred, up to a cap
  • Dynamic compute — authorize max compute cost, pay for actual resources consumed

How It Differs from exact

Aspectexactupto
Settlement amountAlways the full authorized amountAny amount ≤ authorized maximum
Who determines amountFixed at authorization timeServer determines at settlement time
Zero settlementNot applicableAllowed — no on-chain transaction
Asset transfer methodsEIP-3009 or Permit2Permit2 only
Chain supportEVM, Solana, Aptos, …EVM only
Why Permit2 only?

EIP-3009’s transferWithAuthorization requires the exact amount at signature time — the signed value cannot differ from the settled value. Permit2’s permitWitnessTransferFrom allows the actual transfer amount to be ≤ the signed maximum, which is what makes the upto scheme possible.

Payment Flow

sequenceDiagram
    participant Client
    participant Seller as Resource Server (Seller)
    participant Facilitator
    participant Proxy as x402UptoPermit2Proxy
    participant P2 as Permit2 Contract
    participant Token as ERC-20 Contract

    Note over Client: Prerequisite:<br/>token.approve(Permit2, MAX) — one time
    Client->>Seller: 1. Request resource
    Seller-->>Client: 2. 402 Payment Required<br/>(scheme: "upto", amount: max)
    Note over Client: 3. Sign permitWitnessTransferFrom<br/>(amount = max authorized)
    Client->>Seller: 4. Retry with<br/>signed PaymentPayload
    Seller->>Facilitator: 5. Verify payment
    Facilitator-->>Seller: 6. Valid
    Note over Seller: 7. Fulfill request,<br/>measure actual consumption
    Seller->>Facilitator: 8. Settle<br/>(actualAmount ≤ max)
    alt actualAmount > 0
        Facilitator->>Proxy: 9. settle(...)
        Proxy->>P2: 10. permitWitnessTransferFrom(...)
        P2->>Token: 11. transferFrom(...)
        Token-->>P2: 12. Transfer executed
        P2-->>Proxy: 13. Success
        Proxy-->>Facilitator: 14. Settled
    else actualAmount = 0
        Note over Facilitator: No on-chain transaction
    end
    Facilitator-->>Seller: 15. SettlementResponse<br/>(with actual amount)
    Seller-->>Client: 16. Response

Zero Settlement

When the actual consumption is zero (e.g., the request failed or produced no output), the facilitator returns success with amount: "0" and an empty transaction hash. No on-chain transaction is submitted — the Permit2 nonce is not consumed, but the authorization expires naturally via its deadline.

PaymentRequirements

The server advertises the upto scheme with the maximum amount the client should authorize:

{
  "scheme": "upto",
  "network": "eip155:84532",
  "amount": "5000000",
  "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
  "maxTimeoutSeconds": 300
}

The amount field represents the maximum the client may be charged. The extra fields can be empty or omitted entirely.

PaymentPayload

The client signs a Permit2 permitWitnessTransferFrom authorization with permitted.amount set to the maximum:

{
  "x402Version": 2,
  "accepted": {
    "scheme": "upto",
    "network": "eip155:84532",
    "amount": "5000000",
    "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
    "maxTimeoutSeconds": 300
  },
  "payload": {
    "signature": "0x...",
    "permit2Authorization": {
      "permitted": {
        "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        "amount": "5000000"
      },
      "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66",
      "spender": "0x4020633461b2895a48930ff97ee8fcde8e520002",
      "nonce": "0xf374...3480",
      "deadline": "1740672154",
      "witness": {
        "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
        "validAfter": "1740672089",
        "extra": {}
      }
    }
  }
}

Key observations:

  • The spender is the x402UptoPermit2Proxy contract (0x402...0002), not the facilitator.
  • witness.to cryptographically binds the recipient — the facilitator cannot redirect funds.
  • permitted.amount is the maximum — the facilitator can settle for any amount up to this value at the server’s discretion.

SettlementResponse

The upto settlement response includes the actual amount charged:

{
  "success": true,
  "transaction": "0x1234...cdef",
  "network": "eip155:84532",
  "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66",
  "amount": "2350000"
}

The amount field is specific to the upto scheme — it tells the server (and the client) exactly how much was charged.

Proxy Contract: x402UptoPermit2Proxy

The upto scheme uses a dedicated proxy contract (x402UptoPermit2Proxy) that differs from the exact variant in one critical way: the settle() function accepts an amount parameter.

function settle(
    PermitTransferFrom calldata permit,
    uint256 amount,           // ← actual amount to transfer
    address owner,
    Witness calldata witness,
    bytes calldata signature
) external {
    if (amount > permit.permitted.amount) revert AmountExceedsPermitted();
    _settle(permit, amount, owner, witness, signature);
}

The contract enforces that amount ≤ permit.permitted.amount. If the facilitator tries to settle for more than the client authorized, the transaction reverts with AmountExceedsPermitted().

Security Model

Core Guarantees

  1. Single-use authorization — each Permit2 nonce can be consumed exactly once. After settlement, the authorization cannot be reused.
  2. Time-boundedvalidAfter (lower bound) and deadline (upper bound) restrict when the authorization is valid.
  3. Recipient bindingwitness.to is part of the signed message; the proxy enforces it as the transfer destination.
  4. Maximum amount enforcement — the proxy contract guarantees the transferred amount is never greater than permitted amount, amount ≤ permit.permitted.amount.

Trust Considerations

With upto, the client trusts the server to charge a fair amount based on actual usage. The protocol guarantees:

  • The charge will never exceed the authorized maximum.
  • Funds will only go to the signed witness.to address.
  • The authorization will be used at most once.

However, within these bounds, the server determines the final amount. A misbehaving server could charge up to amount regardless of actual consumption. Clients should authorize only what they’re willing to spend per request.

Current Support

ComponentStatus
x402 Specification⏳ Mostly complete, but in draft stage still
x402UptoPermit2Proxy (on-chain)⏳ Deployed on Base Sepolia, waiting for audit and ops finalization
FareSide Facilitator (verify + settle)✅ Available
x402-rs Rust client✅ Available
x402-rs Axum middleware (server)🚧 In progress
TypeScript SDK (@x402)🚧 Not yet available
Seller integration

The upto scheme requires the resource server to report the actual consumed amount at settlement time. Server-side middleware support for this is being actively developed. Check the x402-rs repository and reference SDK repository for the latest status.

Example Implementation

For developers eager to explore the code and experiment with the upto scheme, reference TypeScript implementations are available in the x402-rs repository:

  • upto-evm-scheme.ts — client and server scheme implementation for upto with Permit2 authorization signing
  • payment-required.ts — HTTP middleware for handling payment verification and settlement with dynamic amount support

These implementations demonstrate the full payment flow including signature generation, verification, and settlement with actual usage-based amounts.

Further Reading