# tokenpony / Token Pony Express (TPX) v0.3 tokenpony is the home of TPX and its reference provider. TPX is an OAuth 2.0 profile (OAuth 2.1 baseline) for metered LLM inference grants: an application ships with NO LLM credentials of its own, and the user grants it a metered credit budget from a provider the user chooses and pays. Grants are budget-capped, revocable, and pseudonymous (they carry no user identity). A conforming OAuth client stack works without modification; everything TPX-specific is one authorization details type (llm-inference), one introspection member (budget_used), one usage member (cost), and one account endpoint (GET /credits). All money on the wire is USD. - Resource (inference API): https://api.tokenpony.dev - Discovery entry point: GET https://api.tokenpony.dev/.well-known/oauth-protected-resource - Full spec: https://tokenpony.dev/spec - User dashboard (passkey login): https://api.tokenpony.dev/dashboard - Demo TPX client app: https://ponychat.tokenpony.dev ## Units All money on the wire is USD: budgets and budget_used are JSON numbers (a budget of 0.10 caps spend at ten cents), per-request cost is a USD number, and per-token prices are USD decimal strings. Users top off their balance at the dashboard via Stripe at face value; checkout adds card fees at cost, and after a user's first top-off each purchase also adds postage: the price of one US Forever stamp (currently $0.82). There are no free credits. Per-model USD rates are on GET {resource}/models. ## Integrating an app (standard OAuth 2.1) Step 0. Discover (RFC 9728 then RFC 8414): GET {origin}/.well-known/oauth-protected-resource -> { "resource": "https://api.tokenpony.dev", "authorization_servers": ["https://api.tokenpony.dev"] } GET {authorization_server}/.well-known/oauth-authorization-server -> issuer, authorization_endpoint, token_endpoint, pushed_authorization_request_endpoint, registration_endpoint, introspection_endpoint, revocation_endpoint, authorization_details_types_supported: ["llm-inference"], ... A provider speaks TPX iff authorization_details_types_supported includes "llm-inference". Step 1. Register once per provider (RFC 7591 dynamic registration): POST {registration_endpoint} Content-Type: application/json { "client_name": "My App", "redirect_uris": ["https://myapp.example/callback"], "token_endpoint_auth_method": "none" } <- public client; PKCE, no secret -> 201 { "client_id": "app_...", ... } Server-side (confidential) clients use "client_secret_basic" and receive a client_secret (shown once). redirect_uris are compared by exact string match; https required (localhost allowed for development). Step 2. Authorize. PKCE S256 is REQUIRED. Push the request via PAR, then redirect: POST {pushed_authorization_request_endpoint} (form-encoded; Basic auth if confidential) response_type=code&client_id=app_... &redirect_uri=https://myapp.example/callback &code_challenge={S256(verifier)}&code_challenge_method=S256 &resource=https://api.tokenpony.dev &authorization_details=[{"type":"llm-inference","budget":0.10}] &state={optional} -> 201 { "request_uri": "urn:ietf:params:oauth:request_uri:...", "expires_in": 90 } Redirect the user to: {authorization_endpoint}?client_id=app_...&request_uri=... The budget is USD, at most 6 decimal places (0.10 = ten-cent cap). Optional "models": ["kimi-k2.7-code", ...] restricts the grant to listed models. Unknown fields are rejected (fail closed). The user authenticates with a passkey and approves. Callback: 302 {redirect_uri}?code=tpxc_...&state=...&iss=https://api.tokenpony.dev VALIDATE iss (RFC 9207) against the issuer the flow started with. Codes are single-use, 5-minute expiry; replaying a used code revokes the grant it created. Step 3. Exchange the code (form-encoded, RFC 6749): POST {token_endpoint} grant_type=authorization_code&code=tpxc_... &redirect_uri=https://myapp.example/callback &client_id=app_...&code_verifier={verifier} -> { "access_token": "tpx_at_...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "tpx_rt_...", "authorization_details": [{"type":"llm-inference","budget":0.1}] } Read the granted budget from authorization_details (it may be lower than requested). Access tokens live 1 hour. Refresh with grant_type=refresh_token; refresh tokens ROTATE on every use, and reusing a rotated one revokes the grant. DPoP (ES256) is supported at /token and the API; send a DPoP proof to sender-constrain your tokens (recommended). OAuth endpoints return flat RFC 6749 errors: {"error": "...", "error_description": "..."}. Step 4. Chat on the user's balance. The API is OpenAI-compatible, served both at {resource}/chat/completions and {resource}/v1/chat/completions (for OpenAI SDKs, set baseURL={resource}/v1 and apiKey={access_token}). GET {resource}/models (no auth; OpenRouter-shaped pricing: USD per token as strings: prompt, completion, request, input_cache_read; "0" means free) GET {resource}/credits (Bearer; spend summary for this credential, in USD: { "data": { "total_purchased", "total_used" } }; grant tokens see only their grant) POST {resource}/chat/completions Authorization: Bearer tpx_at_... { "model": "llama-3.3-70b", "messages": [{"role":"user","content":"hi"}], "stream": true|false, "max_tokens": 2048 } Every response carries the exact charge; streaming reports it in the final SSE chunk: "usage": { "prompt_tokens": 812, "cached_tokens": 256, "completion_tokens": 214, "total_tokens": 1026, "cost": 0.00193 } ## Grant state POST {introspection_endpoint} token=tpx_at_... -> { "active": true, "client_id": "app_...", "exp": ..., "authorization_details": [{"type":"llm-inference","budget":0.1}], "budget_used": 0.04125 } Remaining budget = budget - budget_used. No identity claims are ever returned. POST {revocation_endpoint} token=tpx_rt_... -> 200. Revokes the grant and all its tokens. Revoke grants you no longer need. ## Errors at the inference API 401 + WWW-Authenticate: Bearer error="invalid_token" token expired/unknown/revoked -> refresh; if refresh says invalid_grant, re-authorize 402 {"error":{"code":"budget_exhausted"}} grant budget spent -> new authorization request 402 {"error":{"code":"balance_exhausted"}} user's provider balance empty -> user tops off 403 {"error":{"code":"model_not_permitted"}} grant is model-restricted 404 {"error":{"code":"model_not_found"}} list {resource}/models Never ask users for provider API keys; that is what grants replace. ## Personal API keys (non-TPX path) Users can mint personal keys (sk_...) in the dashboard and use them directly against {resource}/v1 with any OpenAI SDK. Same metering, same error shapes. ## App conformance checklist (spec section 12) MUST: PKCE S256; validate iss; exact redirect_uris; request and read budgets via authorization_details; on 401 refresh, on invalid_grant re-authorize. SHOULD: use PAR and DPoP; introspect before large jobs; revoke unneeded grants. ## Agents: AAuth-Budget / TPX-A (experimental, identity-free budgeted access) EXPERIMENTAL: TPX-A is an experimental extension. The TPX OAuth flow above is the stable path; this profile will change as the AAuth drafts evolve. Beyond the OAuth flow above, tokenpony is an AAuth resource + access server (the TPX-A profile of AAuth-Budget). An autonomous agent with its own Ed25519 identity gets a metered, identity-free budget that a person approves at their own Person Server (PS). tokenpony is both the resource and the access server; the PS relays the approved budget. Discovery: GET {resource}/.well-known/aauth-resource.json -> { resource, jwks, budget_endpoint, token_endpoint, signature_algorithms_supported: ["ed25519"] }. Ed25519 only. Flow (agent side): 1. The agent hits {resource}/chat/completions RFC 9421-signed (Signature-Key carries its aa-agent+jwt) with no auth token -> 401 + `AAuth-Requirement: requirement=auth-token; resource-token=""`. 2. The agent takes that resource-token to its PS; the PS (after the person approves a budgeted mission) relays to {resource}/token as JSON {resource_token, agent_token, budget_attestation} and returns { auth_token, token_type:"AAuth", expires_in, funded, funding_url? }. 3. The agent calls {resource}/chat/completions with `Authorization: AAuth ` plus an RFC 9421 signature over `@method @authority @path signature-key authorization` (Authorization MUST be covered). Send max_completion_tokens (reservation bound). Usage reports cost (USD). 4. GET {resource}/grant (signed) -> { active, budget, spent } (USD amount strings). Errors: 401 invalid_token / mission_revoked|completed|expired; 402 budget_exhausted | balance_exhausted | mission_unfunded (+funding_url); 403 model_not_allowed; 400 max_tokens_required. Funding: a budgeted mission is a cap + proof of consent, not payment. Until a tokenpony user claims it via funding_url, inference returns 402 mission_unfunded. Any https PS is accepted (trust is funding-gated). Full profile: https://tokenpony.dev/spec and the TPX-A draft (draft-mcguinness-aauth-budget).