Reference · Pay Token
Pay Token spec
The HS256-signed JWT a buyer (or their agent) attaches as Authorization: Bearer when calling a LemonCake gateway URL. Server-side enforcement of budget, call count, expiry, and rate limit happens on every paid request — see Gateway HTTP spec.
Why a Pay Token?
An AI agent that calls a paid API has three bad options: share the operator's API key, hold a long-lived wallet, or tunnel every call through the operator's server. All three are either insecure or operationally expensive.
A Pay Token is the fourth option: the seller mints a narrow, time-bound, spend-capped, rate-limited permission for a specific endpoint. The agent attaches the token. The gateway verifies the signature, looks up server-side state, decrements the budget on success. The agent never sees the seller's upstream credential.
The shape
A Pay Token is a standard JWT. Three encoded parts joined by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJvd24iOiJvX2FiYzEyMyIsImp0aSI6InB0X2RlZjQ1NiIs InN1YiI6IjQwNjY0YjA2LWFmYjctNGFlMC1hZjFkLWFjZGUxNiIs ImlhdCI6MTcxNjgwMDAwMCwiZXhwIjoxNzE2ODg2NDAwfQ .GJoo3p6ZVGWEvMCvtMXBUa1Ov9YdXTQuZykIlHbNWJk
Decoded:
// header
{ "alg": "HS256", "typ": "JWT" }
// payload
{
"jti": "pt_27788b295d72499fabf5f5d9", // Pay Token ID (DB primary key)
"sub": "40664b06-afb7-4ae0-af1d-...", // endpoint UUID this token can call
"own": "o_4e48c8bfc7934957", // owner ID that issued the token
"iat": 1716800000, // issued at (epoch seconds)
"exp": 1716886400 // expires at (epoch seconds)
}
// signature
HMAC-SHA256(base64url(header) + "." + base64url(payload), LC_JWT_SECRET)Claims
| Claim | Type | Required | Meaning |
|---|---|---|---|
| jti | string | yes | Pay Token ID. Doubles as the primary key in lc_pay_tokens — the gateway uses it to look up live state. |
| sub | string (UUID) | yes | Endpoint the token is scoped to. JWT.sub must match the endpoint resolved from the URL's shortId, otherwise 403 token_endpoint_mismatch. |
| own | string | yes | Owner ID (lc_owner cookie) that minted the token. Used for traceability; the gateway doesn't compare it to anything on the request path. |
| iat | number (epoch s) | yes | Issued-at. Set by /api/lc/tokens on POST. |
| exp | number (epoch s) | yes | Expiry. After this, the gateway returns 401 token_expired and the row's status flips to expired. |
jti, sub, and own are the only application-meaningful claims. iat / exp follow RFC 7519.
Server-side state
Spend cap, call cap, rate limit — the things that make a Pay Token useful — are not in the JWT. They live in Postgres next to the token ID so the seller can revoke, top up, or adjust without re-issuing the JWT to the buyer.
-- lc_pay_tokens id text primary key, -- = JWT.jti endpoint_id uuid references lc_endpoints, -- = JWT.sub owner_id text references lc_owners, -- = JWT.own budget numeric(12, 6), -- USD spend cap spent numeric(12, 6) default 0, -- decremented on each successful call max_calls int, -- per-token call cap calls_used int default 0, -- incremented on each successful call expires_at timestamptz, -- mirrors JWT.exp status text -- 'active' | 'expired' | 'exhausted' | 'revoked' issued_at timestamptz default now()
| Rule | Source of truth | When checked |
|---|---|---|
| JWT signature valid | LC_JWT_SECRET | every gateway call |
| JWT not expired | JWT.exp | every gateway call |
| Token status = active | lc_pay_tokens.status | every gateway call |
| Budget covers this call | lc_pay_tokens.spent + endpoint.price_per_call ≤ budget | every gateway call |
| Calls remaining | lc_pay_tokens.calls_used < max_calls | every gateway call |
| Rate limit not breached | count(lc_test_runs WHERE endpoint_id=? AND at >= now-60s) < endpoint.rate_limit | every gateway call |
Lifecycle
active │ │ POST /g/<shortId> with Bearer <jwt> │ ──── decrement spent / increment calls_used ────► │ ├── exp passed ──► status = "expired" (401 token_expired) ├── calls_used >= max_calls ──► status = "exhausted" (402 token_exhausted) ├── DELETE /api/lc/tokens/<jti> ──► status = "revoked" (403 token_revoked) └── budget exceeded on next call ──► 402 spend_cap_exceeded (status stays active until exhausted by call cap)
Status is a one-way ratchet — once flipped to expired / exhausted / revoked, the token never comes back. Top-ups happen by issuing a fresh token, not by reactivating an old one.
Issue a Pay Token
Sellers mint tokens via the REST API or via the Pay Token pane on /app.
POST https://www.lemoncake.xyz/api/lc/tokens
Content-Type: application/json
Cookie: lc_owner=<your owner cookie> // sent automatically from /app
{
"endpointId": "40664b06-afb7-4ae0-af1d-...",
"budget": 5.00,
"expiresInHours": 24,
"maxCalls": 100
}// 201 Created
{
"token": {
"id": "pt_27788b295d72499fabf5f5d9",
"endpoint_id": "40664b06-afb7-4ae0-af1d-...",
"budget": "5.000000",
"spent": "0.000000",
"max_calls": 100,
"calls_used": 0,
"expires_at": "2026-05-29T05:18:42Z",
"status": "active",
...
},
"jwt": "eyJhbGciOiJIUzI1NiI…" // give this to your buyer
}The jwt is only returned on issuance — the server never echoes it back later. Hand it to your buyer (Slack DM, email, in-band over your own API), they attach it to gateway calls.
Budget guardrail: budget may not exceed 5× the endpoint's token_budget setting, returning 400 budget_exceeds_endpoint_cap if it does. This keeps a stolen seller cookie from minting an unlimited-spend token.
Revoke a Pay Token
DELETE https://www.lemoncake.xyz/api/lc/tokens/pt_27788b295d72499fabf5f5d9 Cookie: lc_owner=<your owner cookie>
Takes effect on the very next gateway call (no cache). The JWT itself remains cryptographically valid — the gateway rejects it because the DB row says status = revoked. This is intentional: we wanted instant revocation, not waiting for short expiries to roll over.
Why HS256 instead of EIP-712 / Ed25519?
Pay Tokens are issued andverified by the same LemonCake gateway. There's no third party that needs to verify the signature without trusting us. HS256 with a shared secret (LC_JWT_SECRET) is the simplest thing that meets that bar.
When the day comes that a third-party verifier wants to check a Pay Token without calling LemonCake (a Coinbase x402 facilitator wrapping ours, for example), we'll add a second alg (RS256 or Ed25519) under the same jti identity — the JWT format already supports per-token alg negotiation via the header.
Buyer-side usage
The whole interaction from the buyer's side is two HTTP requests their server / agent already knows how to make:
// Node.js
const res = await fetch(`https://www.lemoncake.xyz/g/${shortId}`, {
method: "POST",
headers: {
"Authorization": "Bearer " + PAY_TOKEN_JWT,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "…" }),
});
console.log(res.headers.get("x-lemoncake-charge")); // "0.01"
console.log(res.headers.get("x-lemoncake-upstream-ms")); // "17"CORS is permissive — works from server, edge function, or browser. See Gateway HTTP spec for the full error table.
What's intentionally NOT in the token
The original v0.1 draft of this spec carried allowed_paths, allowed_methods, price_per_call, rate_limit, metadata, and nonce inside the JWT. We dropped them all. Reasons:
- Server-side state is more flexible than a signed payload. The seller can adjust price or rate without re-issuing JWTs to every buyer.
- Replay defence comes from the DB, not a nonce.
spentonly goes up, so a replayed request that pushed pastbudgetsimply fails. - Smaller tokens compress better in HTTP headers and survive a wider range of HTTP clients (some have header-size limits).
The current token is intentionally minimal — just enough for a verifier to look up the right row and confirm the signature. Everything operational is in Postgres.
Future
Fields likely to grow into the spec as integrations demand:
scope— path / method allowlist enforced at the gateway, for sellers whose origin exposes multiple sensitive operations.aud— facilitator identifier so the same JWT can be honored by a non-LemonCake verifier (x402-compatible mode).- Alternative
alg— RS256 / Ed25519 for cross-verifier scenarios where shared-secret HS256 isn't acceptable.
Anything we add will follow a single rule: never remove a field. Existing JWTs keep verifying as long as the secret is rotated forward with overlap.
Next
- Gateway HTTP spec → What the buyer sends, what the gateway returns
- Open /app → Issue your first Pay Token in 30 seconds
- Spec feedback → contact@aievid.com

