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
| Aspect | exact | upto |
|---|---|---|
| Settlement amount | Always the full authorized amount | Any amount ≤ authorized maximum |
| Who determines amount | Fixed at authorization time | Server determines at settlement time |
| Zero settlement | Not applicable | Allowed — no on-chain transaction |
| Asset transfer methods | EIP-3009 or Permit2 | Permit2 only |
| Chain support | EVM, Solana, Aptos, … | EVM 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
spenderis thex402UptoPermit2Proxycontract (0x402...0002), not the facilitator. witness.tocryptographically binds the recipient — the facilitator cannot redirect funds.permitted.amountis 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
- Single-use authorization — each Permit2 nonce can be consumed exactly once. After settlement, the authorization cannot be reused.
- Time-bounded —
validAfter(lower bound) anddeadline(upper bound) restrict when the authorization is valid. - Recipient binding —
witness.tois part of the signed message; the proxy enforces it as the transfer destination. - 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.toaddress. - 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
| Component | Status |
|---|---|
| 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 |
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
uptowith 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
- EVM Asset Transfer Methods — how EIP-3009 and Permit2 work in x402
- x402 upto scheme specification (current draft)
- x402 upto EVM specification (current draft)