Concepts
Payment Pre-Auth Cache
Skip the 402 round-trip on subsequent calls to the same model with a short-lived signed receipt — disabled on Solana
Status: specification only. Pre-auth is not implemented in the gateway today. This page documents the protocol design Solvela will adopt when the optimization is wired in (slot:
crates/gateway/src/middleware/x402.rs). Track follow-up at the bottom of this page.
What it is
After a successful 402 → payment → 200 cycle for a given (model, payer, amount) tuple, the client may sign a pre-authorized payment payload for its next request to the same model. The gateway accepts the pre-signed payload directly, skipping the 402 round-trip and saving roughly 150–250ms of wallclock time per call.
Pre-auth is opt-in client-side, advisory server-side, and MUST be disabled on Solana — see Critical safety constraint.
Why it matters
Agentic loops typically issue 5–50 LLM calls per task: a planner call, a series of tool-use turns, a final consolidation. A 402 round-trip costs one signing operation client-side plus one verification + RPC call gateway-side — empirically 150–250ms end-to-end on Solana, comparable on EVM. Across 5–50 calls that compounds to 1–10 seconds of pure protocol overhead, on top of the LLM latency itself. Pre-auth collapses the protocol overhead to a single payload-signing operation per turn.
Protocol shape
Cycle 1 (cold) — normal 402 dance
The agent sends a request without payment. The gateway returns 402 with a PaymentRequired body. The agent signs a payment payload and resends with payment-signature: <base64>. The gateway verifies, proxies to the LLM provider, returns 200 with the response.
On the success response, the gateway includes a new header:
HTTP/1.1 200 OK
Content-Type: application/json
X-Solvela-PreAuth-Receipt: <base64-encoded-receipt>
X-Solvela-PreAuth-TTL-Ms: 60000
X-Solvela-PreAuth-Scope: model=auto;payer=GH...;amount_atomic=1234X-Solvela-PreAuth-Receipt is a server-signed receipt that authorizes the bearer to make ONE additional call to the same (model, payer, amount) within the TTL window without going through 402.
Cycle 2 (warm) — opt-in pre-auth
The agent caches the receipt locally. On its next request to the same model, the agent sets:
POST /v1/chat/completions
X-Solvela-PreAuth: true
X-Solvela-PreAuth-Receipt: <base64-encoded-receipt>
Content-Type: application/json
{ "model": "auto", "messages": [...] }The agent does NOT send a payment-signature header on this request — the receipt is the proof of payment.
Gateway accepts or rejects
The gateway verifies the receipt's signature, scope (model/payer/amount must match the new request), and freshness (within TTL). On success, the request is proxied normally and a fresh receipt is issued in the response. On any verification failure, the gateway responds with 402 Payment Required as if no pre-auth header were present — clients must handle this fail-open path.
Receipt rotation
Each successful cycle issues a fresh receipt. Receipts are NOT chainable — a single receipt authorizes exactly one subsequent call. This bounds the blast radius of a leaked receipt to one request's worth of inference cost.
Receipt structure
The base64-decoded receipt is a JSON object signed by the gateway's HMAC key:
{
"v": 1,
"iss": "solvela-gateway",
"iat": 1730320000000,
"exp": 1730320060000,
"scope": {
"model": "auto",
"payer": "GHRkn...",
"amount_atomic": "1234",
"currency": "USDC"
},
"nonce": "01HG5...",
"sig": "base64(hmac_sha256(secret, canonical_json(above)))"
}The gateway's HMAC secret rotates per-deploy; receipts older than the most recent rotation become invalid. This is acceptable — clients fall back to the 402 path.
Header reference
| Header | Direction | Required when | Notes |
|---|---|---|---|
X-Solvela-PreAuth | client → server | Opting into pre-auth | Value: true (any other value treated as opt-out) |
X-Solvela-PreAuth-Receipt | both directions | Always when pre-auth in use | Base64-encoded JSON, see structure above |
X-Solvela-PreAuth-TTL-Ms | server → client | Always on receipt issuance | Hint to client; authoritative TTL is in receipt's exp |
X-Solvela-PreAuth-Scope | server → client | Always on receipt issuance | Human-readable scope summary; (model, payer, amount_atomic) |
Critical safety constraint
Pre-auth MUST be disabled on Solana.
Solana transactions are bound to a recent_blockhash that expires roughly 60–90 seconds after issuance — the cluster enforces this at the validator level (see Solana's transaction confirmation handling docs). Once expired, the cluster rejects the transaction outright; the signature can never land. That single chain property rules out pre-auth on Solana for two independent reasons:
1. The validity window is too short to amortize. Pre-auth's whole payoff is reusing a signed payload across many calls. With a 60–90 second ceiling, the receipt expires before a typical agentic loop finishes its planning phase. The client ends up re-signing on nearly every call anyway, paying the protocol overhead the optimization was supposed to eliminate.
2. There is no off-chain authorization escape hatch. EVM's EIP-3009 transferWithAuthorization lets the signer pick validAfter / validBefore themselves, and the contract's nonce mapping prevents on-chain replay — the gateway can hold the authorization and settle later within the signer's chosen window. Solana's SPL transfer_checked has no equivalent: every settlement must be re-signed against a fresh blockhash and is single-use by signature. A receipt that grants credit toward a "future" Solana call has no on-chain authorization the gateway can later submit on the user's behalf, which means honoring it would require the gateway to front funds against an unenforceable promise.
Either argument alone is sufficient. Together they make pre-auth structurally a no-fit for Solana under x402's trustless settlement invariant.
Solvela's implementation:
- The receipt's
scope.currencyindicates the payment chain. - If
scope.currency === "USDC-SPL"(Solana), the gateway rejects the pre-auth header even if the receipt itself is valid, returning 402 instead. - Clients on Solana SHOULD NOT cache or send pre-auth receipts. The gateway omits the
X-Solvela-PreAuth-Receiptheader entirely on Solana payment success responses — there is nothing to cache.
On Solana, pre-auth is a no-op — the gateway behaves exactly as it does today.
EVM applicability
When Solvela adds EVM support (Base / Ethereum / Polygon), pre-auth becomes safe because EIP-3009 TransferWithAuthorization payloads carry their own validAfter / validBefore timestamp window — there is no blockhash binding to slot timing. A pre-signed authorization can only be redeemed once (the contract's nonce mapping prevents replay), and only within the timestamp window the client encodes.
The receipt model maps cleanly:
scope.currency = "USDC-EVM-Base"(or-Ethereum, etc.) gates pre-auth as ENABLED.- The gateway's
validBeforeenforcement aligns with the receiptexp. - The
noncefield in the receipt mirrors EIP-3009's per-authorization nonce.
So the same opt-in client header set works for EVM with no protocol change beyond the chain-aware enablement gate.
Response cache — adjacent failsafe
Even with pre-auth disabled on Solana and constrained on EVM, the gateway carries a related layer that limits the blast radius of accidentally-replayed requests:
The existing response cache (ResponseCache in crates/gateway/src/cache.rs) keys on SHA-256(model ‖ serialised_messages ‖ temperature) and stores the upstream completion for the configured TTL (default 10 minutes). A client that issues the same prompt twice within the TTL window — pre-auth header or not — gets the cached response and the upstream LLM is only charged once.
This is not a payment-replay guard. Payment-replay protection is handled separately: the gateway tracks transaction signatures and rejects duplicate payment-signature headers with 402 Invalid Payment. The response cache is a cost-control / latency optimization, not a correctness safeguard against double-charging.
The two layers are deliberately independent:
- Pre-auth is a latency optimization on the gateway's HTTP boundary (skip the 402 round-trip).
- Response cache is a cost optimization on the upstream LLM hop (don't call the provider twice for the same prompt).
- Payment-signature replay protection is a correctness guard at the verifier (don't accept the same signed transaction twice).
The pre-auth design only interacts with the first two; it cannot weaken the third.
Sequence diagram
sequenceDiagram
participant A as Agent (client)
participant G as Solvela Gateway
participant P as LLM Provider
Note over A,G: Cycle 1 — cold path
A->>G: POST /v1/chat/completions (no payment)
G-->>A: 402 PaymentRequired { amount, pay_to }
A->>A: Sign payment payload
A->>G: POST /v1/chat/completions<br/>payment-signature: <base64>
G->>G: Verify payment
G->>P: Forward request
P-->>G: Response
G-->>A: 200 OK<br/>X-Solvela-PreAuth-Receipt: <r1><br/>X-Solvela-PreAuth-TTL-Ms: 60000
Note over A,G: Cycle 2 — warm path (EVM only)
A->>A: Cache receipt r1
A->>G: POST /v1/chat/completions<br/>X-Solvela-PreAuth: true<br/>X-Solvela-PreAuth-Receipt: r1
G->>G: Verify receipt sig + scope + TTL
G->>P: Forward request
P-->>G: Response
G-->>A: 200 OK<br/>X-Solvela-PreAuth-Receipt: <r2>
Note over A,G: Cycle 3 — fail-open on bad receipt
A->>G: POST /v1/chat/completions<br/>X-Solvela-PreAuth: true<br/>X-Solvela-PreAuth-Receipt: <expired>
G->>G: Verify fails (expired)
G-->>A: 402 PaymentRequired<br/>(falls back to cold path)
A->>A: Discard expired receipt, sign fresh payment
A->>G: POST ... payment-signature: <new>
G-->>A: 200 OK ...
Note over A,G: Cycle 4 — Solana (pre-auth disabled)
A->>G: POST /v1/chat/completions (no payment)
G-->>A: 402 PaymentRequired
A->>G: POST ... payment-signature (Solana USDC-SPL)
G->>P: Forward
P-->>G: Response
G-->>A: 200 OK<br/>(NO PreAuth-Receipt header — Solana)Failure modes and how the protocol handles them
Receipt expired
Gateway returns 402 with PaymentRequired body. Client discards receipt and falls back to cold path. No charge.
Receipt scope mismatch
Client requested a different model, payer, or amount than the receipt covers. Gateway rejects with 402. No charge.
HMAC secret rotated
Receipt was signed by the previous secret. Gateway returns 402. Client discards receipt and falls back. No charge.
Solana request with receipt
Gateway returns 402 unconditionally — Solana is not eligible for pre-auth regardless of receipt validity. No charge.
Both PreAuth-Receipt AND payment-signature sent
Gateway prefers payment-signature (the more authoritative proof) and ignores the receipt. Request-dedup catches accidental retries.
Receipt leaked / stolen
Attacker can redeem at most ONE call's worth of inference within TTL (single-use receipt, narrow scope). Bounded blast radius.
Client-side implementation sketch
A client SDK should treat pre-auth as a transparent optimization: callers do not opt in per-call; the SDK manages the receipt cache.
// pseudocode
class SolvelaClient {
private receiptCache = new Map<string, Receipt>();
async chatCompletion(req: ChatRequest): Promise<ChatResponse> {
const scopeKey = `${req.model}:${this.payer}:${this.estimateAmount(req)}`;
const cached = this.receiptCache.get(scopeKey);
if (cached && this.chain === "evm" && !cached.isExpired()) {
const res = await this.send(req, { preAuthReceipt: cached.encoded });
if (res.status === 200) {
this.receiptCache.set(scopeKey, res.headers.preAuthReceipt);
return res;
}
// 402 — fall through to cold path; receipt was no good
this.receiptCache.delete(scopeKey);
}
// cold path
const cold = await this.send(req);
if (cold.status === 402) {
const signed = await this.signPayment(cold.body);
const res = await this.send(req, { paymentSignature: signed });
if (this.chain === "evm" && res.headers.preAuthReceipt) {
this.receiptCache.set(scopeKey, res.headers.preAuthReceipt);
}
return res;
}
return cold;
}
}The Solana branch never enters the cached path — the gateway will not issue receipts there.
Implementation TODO
When this spec is implemented:
- Slot:
crates/gateway/src/middleware/x402.rs— add aPreAuthLayerthat runs before the existingx402middleware. On hit, setsExtension<VerifiedPayment>so the rest of the chain treats the request as paid. On miss or invalid receipt, removes the pre-auth headers and lets the request fall through to the normal 402 path. - Receipt signing: new module
crates/gateway/src/preauth.rs— HMAC-SHA256 signer with rotating secrets. Reusering::hmac(already a transitive dep). - Per-chain gate: explicit
if matches!(chain, Chain::Solana) { return reject_preauth(); }in thePreAuthLayer. Inline comment citing this page. - Client SDK changes: the standalone
solvela-ts,solvela-python,solvela-gorepos and the in-monoreposdks/mcp— receipt cache + opt-in header set. Solana code paths remain unchanged. - Tests:
- Receipt round-trip (sign / verify / scope match)
- Expired receipt → fall-through to 402
- Solana request with receipt → 402 even when receipt valid
- HMAC secret rotation invalidates old receipts
- Both
payment-signatureand receipt sent → payment-signature wins
- Metrics:
solvela_preauth_hit_total{chain},solvela_preauth_miss_total{chain, reason},solvela_preauth_savings_mshistogram.
Until implemented, the headers above are reserved — clients setting them today get a normal 402 response (the gateway ignores unknown headers).