Conventions
- Base URL:
https://carpe-diem.xyz/api/operator - Auth: pass
Authorization: Bearer <token>β<token>is either an API key (cdm_β¦) or a wallet session JWT. Each endpoint states what it accepts. - Format: JSON in/out, except file uploads (
multipart/form-data) and binary responses (audio, video). - OpenAI-compatible endpoints live under
/v1.
Contracts (Base mainnet)
Deployed on Base mainnet (chain ID 8453). The escrow source is verified and reproducible from the repository β Sourcify reports a full bytecode match.
| Contract | Address | Verify |
|---|---|---|
| CarpeEscrow (UUPS proxy) | 0x15917768b31CB1DC61d9d858f2419BF45044005d | BaseScan Β· Sourcify β |
| β³ Implementation | 0x53344b3b0a89CaeACD9D0B50bf5B1D21C4A1e9b4 | BaseScan Β· Sourcify β |
| USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | BaseScan |
| DIEM (Venice) | 0xF4d97F2da56e8c3098f3a8D538DB630A2606a024 | BaseScan |
TEE attestation (digest β running enclave) is verifiable from the Privacy page.
1. Authentication
POST /auth/session
Create a wallet session (SIWE). Returns a JWT.
- Auth: none
- Body:
{ "wallet": "0xβ¦", "message": "<signed SIWE message>", "signature": "0xβ¦" } - 200:
{ "token": "<jwt>", "wallet": "0xβ¦", "expiresIn": "30m" } - Errors:
400bad input Β·401AUTH_FAILED (signature mismatch / expired message) Β·403OFAC_BLOCKED
POST /auth/refresh
Exchange a still-valid JWT for a fresh one.
- Auth: Bearer JWT
- 200:
{ "token", "wallet", "expiresIn" } - 401: expired β re-authenticate
POST /auth/api-keys
Create a persistent API key. JWT only (an API key can't mint keys).
- Auth: Bearer JWT
- Body:
{ "name": "optional label" } - 201:
{ "id", "key": "cdm_β¦", "prefix": "cdm_xxxxxxxxβ¦" }β full key shown once.
GET /auth/api-keys
List your keys (no secrets).
- Auth: Bearer JWT
- 200:
{ "keys": [{ "id", "name", "prefix", "createdAt", "revokedAt" }] }
DELETE /auth/api-keys/:id Β· POST /auth/api-keys/:id/revoke
Revoke a key (two forms; the POST alias is for clients that mishandle DELETE).
- Auth: Bearer JWT
- 200:
{ "status": "revoked", "id" }Β· 404 if not found
2. Chat & Messages
POST /v1/chat/completions
OpenAI-compatible chat completions.
- Auth: API key or JWT
- Body: standard OpenAI shape β
{ "model", "messages": [...], "temperature"?, "max_tokens"?, "top_p"?, "stop"?, "stream"?, "tools"? } - 200: OpenAI completion object (
choices[].message); SSE stream when"stream": true(terminated bydata: [DONE]) - Headers:
X-Carpe-*-Creditsreport the request cost and remaining balance - Errors:
402PAYMENT_REQUIRED (insufficient credits) Β·400invalid model/params Β·429rate limited Β·502VENICE_ERROR Β·503no providers / TEE not ready
POST /v1/messages
Anthropic-compatible Messages API (Claude Code, Cursor, Cline).
- Auth: API key or JWT
- Body: Anthropic shape β
{ "model", "max_tokens", "messages": [...], "system"?, "stream"? } - 200: Anthropic message object (
content[].text); SSE stream when"stream": true - Note:
modelis a Venice model id (see Models), not a Claude model name - Errors: same set as
/v1/chat/completions
Claude-specific caveats
Two parameters behave differently on Claude models because of upstream constraints. Verified 2026-06-10 by cross-testing the same payloads against openai-gpt-52 and claude-opus-4-8 through the same operator (both work there), so the limits live upstream β Carpe Diem forwards the payload faithfully in all cases.
| Param | Behaviour | Scope |
|---|---|---|
tool_choice: { "type": "function", "function": { "name": "..." } } (forced specific tool) β and tool_choice: "required" (any tool, but force one) | Rejected on claude-fable-5 with HTTP 400 and message tool_choice forces tool use is not compatible with this model. Works on every other Claude (e.g. claude-opus-4-8) and on OpenAI/Grok/etc. | claude-fable-5 only β Anthropic published this as part of Fable 5's intentional safety envelope, which also blocks responses in cybersecurity / biology / chemistry and reroutes those to Claude Opus 4.8 (CNBC, TechCrunch). |
thinking: { "type": "enabled", "budget_tokens": N } (Anthropic-native extended thinking block) | Silently dropped β the response is shape-valid but never contains a { "type": "thinking" } block, only { "type": "text" }. The model still reasons internally and returns correct answers; only the separate thinking block is missing. | All Claude models on Venice. Venice exposes Anthropic reasoning through its own reasoning_content field (see Venice's reasoning models guide) using reasoning_effort: low | medium | high instead of Anthropic's native thinking parameter. |
Workarounds for tool_choice forced (Fable 5 only):
- Use
claude-opus-4-8for the request that needs a guaranteed function call β opus-4-8 honourstool_choicefully. Switch back to Fable 5 for everything else. - Or leave
tool_choiceunset and instruct the model in the system prompt:"You MUST call the <fn_name> function. Do not respond with prose."Fable 5 follows multi-constraint instructions reliably (it scored 7/7 on a strict instruction-following audit).
Workarounds for thinking:
- Prompt-driven: prefix the user message with
"Show your reasoning step by step before answering."The chain appears in the standardcontent[].textblock. - Venice-native: switch to
claude-opus-4-6/4-7/claude-sonnet-4-5/4-6and passreasoning_effort: "high"β the chain is then returned in a separatereasoning_contentfield (Venice convention, not Anthropic's).claude-fable-5does not expose reasoning effort levels (supportsReasoningEffort: falseinGET /v1/models).
3. Embeddings
POST /v1/embeddings
OpenAI-compatible vector embeddings.
- Auth: API key or JWT
- Body:
{ "model", "input": "text" | ["text", β¦] }β single string or array (batch) - 200:
{ "object": "list", "data": [{ "object": "embedding", "index": 0, "embedding": [float, β¦] }], "model", "usage": { "prompt_tokens", "total_tokens" } } - Billing: per input token
- Errors:
402PAYMENT_REQUIRED Β·400invalid model/params Β·429rate limited Β·502VENICE_ERROR Β·503no providers / TEE not ready
4. Images
Pricing is a fixed cost per image by model (then the dynamic multiplier applies); discover models via GET /v1/models.
Heavy models β use the async queue.
/v1/image/generateis synchronous and the edge proxy in front of the operator caps a single request at ~60s. Models that routinely take longer (e.g.gpt-image-2,nano-banana-pro,recraft-v4-pro) will return a502at the edge even though the image was generated. For those, use the async pattern below (/queue+/retrieve), which has no duration limit. Billing is only charged on a stored, retrievable success β a failed generation is never billed.
POST /v1/image/generate
Synchronous (one request β result). Best for fast models that finish well under 60s.
- Auth: API key or JWT
- Body:
{ "model", "prompt", "variants"? }βvariants1β4 (default 1) - 200: Venice image payload (base64-encoded image(s))
- Errors:
402PAYMENT_REQUIRED Β·400invalid/missing prompt (max 10,000 chars) Β·502VENICE_ERROR Β·503no providers
POST /v1/image/generate/queue β /retrieve β /complete (async)
For heavy models. Same shape as the video/audio async pattern β each call returns in <1s, so no edge timeout regardless of generation duration.
- POST
/v1/image/generate/queueβ Auth: API key or JWT Β· Body: same as/v1/image/generateΒ· 202:{ "queue_id", "status": "pending" }Β· Errors:402Β·400Β·503QUEUE_FULL (too many in-flight jobs) Β·503no providers / no capacity - POST
/v1/image/generate/retrieveβ Body:{ "queue_id" }Β· 200 (pending): JSON{ "status": "pending", "queue_id" }β poll again Β· 200 (done): the binary image (Content-Type: image/*, headerX-Carpe-Image-Status: completed) Β· Errors:403queue_id belongs to another wallet Β·404JOB_NOT_FOUND (unknown or expired) Β· upstream error code on failed generation - POST
/v1/image/generate/completeβ Body:{ "queue_id" }Β· 200:{ "status": "released" }β best-effort cleanup; the job also expires on its own TTL (15 min)
Flow: queue β poll retrieve every ~2s until you get binary (or an error) β optionally complete. Retries are idempotent: re-queue starts a fresh job; in-flight jobs aren't cancelled but don't accumulate as zombies.
BASE=https://carpe-diem.xyz/api/operator/v1
AUTH="Authorization: Bearer $CARPE_KEY"
# 1. Queue β { "queue_id", "status": "pending" }
QID=$(curl -s -X POST "$BASE/image/generate/queue" -H "$AUTH" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-image-2","prompt":"a neon koi pond"}' | jq -r .queue_id)
# 2. Poll: pending β JSON; done β binary image; failure β error JSON
while :; do
ct=$(curl -s -o out.webp -w "%{content_type}" -X POST "$BASE/image/generate/retrieve" \
-H "$AUTH" -H "Content-Type: application/json" -d "{\"queue_id\":\"$QID\"}")
case "$ct" in
image/*) echo "saved out.webp"; break ;; # completed
application/json*) sleep 2 ;; # still pending β poll again
*) echo "error:"; cat out.webp; break ;;
esac
done
# 3. Optional cleanup (frees the buffer; TTL is the backstop)
curl -s -X POST "$BASE/image/generate/complete" -H "$AUTH" \
-H "Content-Type: application/json" -d "{\"queue_id\":\"$QID\"}" > /dev/null
POST /v1/image/edit
Transform an existing image from a prompt.
- Auth: API key or JWT
- Body:
{ "model", "prompt", "image": "<base64>", "aspect_ratio"? } imageformat: data URI βdata:image/<mime>;base64,<β¦>(e.g.image/png,image/jpeg,image/webp). HTTP URLs are not fetched server-side.- 200: edited image payload Β· Errors: same set as generate
Heavy edit models β use the async queue. Like generation,
/v1/image/editis synchronous and the edge caps a request at ~60β120s. The highest-quality edit models β notablygpt-image-2-editβ routinely run longer and return a502at the edge even though the edit was produced. Use the async pattern below; no duration limit, billed only on a stored, retrievable success.
POST /v1/image/edit/queue β /edit/retrieve β /edit/complete (async)
Same shape as /v1/image/generate/queue β each call returns in <1s, so no edge timeout regardless of edit duration.
- POST
/v1/image/edit/queueβ Auth: API key or JWT Β· Body: same as/v1/image/edit({ "model", "prompt", "image", "aspect_ratio"? }) Β· 202:{ "queue_id", "status": "pending" }Β· Errors:402Β·400(missing prompt/image) Β·413image > 5 MB Β·503QUEUE_FULL Β·503no providers / no capacity - POST
/v1/image/edit/retrieveβ Body:{ "queue_id" }Β· 200 (pending):{ "status": "pending", "queue_id" }β poll again Β· 200 (done): the binary image (Content-Type: image/*) Β· Errors:403other wallet Β·404JOB_NOT_FOUND Β· upstream error on failed edit - POST
/v1/image/edit/completeβ Body:{ "queue_id" }Β· 200:{ "status": "released" }β best-effort cleanup (15-min TTL backstop)
POST /v1/image/upscale
Increase resolution.
- Auth: API key or JWT
- Body:
{ "model": "upscaler", "image": "<base64>", "scale"? }βscale1β4 (default 2) imageformat: raw base64, nodata:prefix. Different from/v1/image/editwhich wants the full data URI.- Constraints: source β₯ 256Γ256 pixels. Smaller inputs return
400BAD_REQUEST(Invalid or corrupt image). - 200: upscaled image payload Β· Errors:
400scale out of range / unknown model / source too small
POST /v1/image/share Β· GET /v1/image/share/:id
Publish a generated image to a shareable link, then fetch it.
- POST β Auth: API key or JWT Β· Body: the image to share Β· 200:
{ "id", "url" } - GET
/v1/image/share/:idβ Auth: none (public) Β· returns the shared image
5. Audio
POST /v1/audio/speech
Text-to-speech (OpenAI-compatible).
- Auth: API key or JWT
- Body:
{ "model", "input", "voice"?, "response_format"? }βinput1β50,000 chars voiceis model-specific. OpenAI's generic voice names (alloy,echo, β¦) are not portable β empirically rejected by 11/11 Venice TTS models. Each model exposes its own enum:- Safest default: omit
voiceentirely, Venice picks the model's default. - To pick a specific voice, query
GET /v1/modelsβ every entry withcarpe_diem_type: "tts"carries avoicesarray of accepted values (e.g.tts-kokorohas 54,tts-gemini-3-1-flashhas 30).
- Safest default: omit
- 200: binary audio (e.g.
audio/mpeg) - Billing: per character
- Errors:
402PAYMENT_REQUIRED Β·400invalid/missing input / unknown voice for this model Β·502VENICE_ERROR
POST /v1/audio/transcriptions
Speech-to-text (OpenAI Whisper-compatible). multipart/form-data.
- Auth: API key or JWT
- Body (multipart):
file(audio) +model - 200:
{ "text": "β¦" } - Errors:
400multipart required / no file Β·502VENICE_ERROR
POST /v1/audio/music/queue Β· POST /v1/audio/music/retrieve
Music generation β asynchronous (same pattern as video).
- queue β Auth: API key or JWT Β· Body:
{ "model", "prompt", "lyrics_prompt"?, β¦ }Β· 200:{ "queue_id" } - retrieve β Body:
{ "queue_id", "model" }Β· 200:{ "status": "processing" }while running, or the finished track when"completed" - Same account-scoping and pending rules as video (see Video)
lyrics_prompt is model-specific β three categories (validated 2026-06-07):
| Behaviour | Models |
|---|---|
Required β sending only prompt returns 400 | minimax-music-v2, minimax-music-v25, minimax-music-v26 |
| Optional β accepts payloads with or without it | ace-step-15 |
Forbidden β sending it returns 400 (model has no lyrics) | elevenlabs-music, lyria-3-pro, stable-audio-25, elevenlabs-sound-effects-v2, mmaudio-v2-text-to-audio |
Semantics: prompt describes the musical style; lyrics_prompt carries the sung lyrics. Inspect GET /v1/models (carpe_diem_type: "music") for the live model list.
6. Video (async)
Video takes minutes, so it's asynchronous: (optionally) quote, queue, poll retrieve, fetch file.
POST /v1/video/quote
Price estimate before committing β optional but recommended (queue runs its own balance check anyway). Proxied to Venice's quote.
- Auth: API key or JWT
- Body: the same passthrough body as
/v1/video/queue(model,prompt,duration,aspect_ratio, and any image fields β see queue below). Forwarded to Venice verbatim. Use it to validate an unfamiliarduration/aspect_ratio/ param set without being charged β Venice returns400if the model rejects a field. - 200: Venice's quote payload
- Errors:
400(model rejected a field β e.g. unsupportedduration) Β·502VENICE_ERROR
POST /v1/video/queue
Start a job. Runs a balance check and refuses if you can't afford it.
-
Auth: API key or JWT
-
Body:
{ "model", "prompt", "duration", "aspect_ratio"?, ...image params } -
durationis required. String with"s"suffix (e.g."5s", not5or5.0). Omitting it returns400. The accepted enum is per-family β empirically validated via/v1/video/quote2026-06-07 (seedance re-validated 2026-06-24 β the2-0line now accepts"15s"):Family Accepted durationsora-*"4s""8s""12s""16s"veo3*"4s""6s""8s"wan-*"5s""10s"kling-*"5s""10s"seedance-2-0*"4s""5s""6s""8s""10s""12s""15s"seedance*(1-5 / older)"4s""5s""6s""8s""10s""12s"pixverse-*"3s""5s""8s""10s"ltx-*"5s""8s""10s"longcat*"5s""10s"grok-imagine*"5s""10s"happyhorse-*"3s""4s""5s""6s""8s""10s""12s"vidu-*"3s""5s""8s""10s""12s""16s" -
aspect_ratiois optional."16:9"and"9:16"are accepted by every family."1:1"works on most (rejected bysora-*,veo3*,longcat*,happyhorse-*)."4:3"/"3:4"accepted only byseedance*,pixverse-*,ltx-*,grok-imagine*,vidu-*. Omit to let Venice pick a sensible default. -
Image conditioning (image-to-video / frame control). For models that take a source image, pass the image fields alongside
prompt:image_url(string) β the source / first frame. Required by image-to-video models (*-image-to-video); the start frame for first-last-frame models. Accepts anhttps://URL or adata:URI.end_image_url(string) β the last frame, for first-last-frame interpolation models.image_urls(string[]) β multiple reference images, for models that condition on a set.
-
Nothing is filtered β the body passes through verbatim. The operator forwards your request body to the upstream model as-is; it does not strip, rename, or validate video parameters. Any field the chosen model accepts (the image fields above, plus
seed,negative_prompt, motion/camera controls, etc.) reaches Venice unchanged β consult the model's own spec for its full parameter set. The operator only adds provider routing; it never edits your payload. -
Validate before queueing. Call
POST /v1/video/quotefirst with the exact payload β same verbatim passthrough, no charge, returns400if any field (duration,aspect_ratio, image params, β¦) is rejected by the model. Cheaper than debugging inqueue(which commits a hold against your balance). -
200: Venice's queue payload. The job id is in
id(current schema) orqueue_id(older shape) β use whichever is present when you call retrieve. Credit headers included. -
Errors:
402PAYMENT_REQUIRED Β·400INVALID_MODEL / BAD_REQUEST (prompt / unsupported duration / unsupported aspect_ratio) Β·503NO_PROVIDERS / INSUFFICIENT_PROVIDER_CAPACITY / TEE_NOT_READY Β·502VENICE_ERROR
POST /v1/video/retrieve
Poll until the job completes. Both queue_id and model are required.
- Auth: API key or JWT β any key on the same account that queued
- Body:
{ "queue_id", "model" } - 200: while running, the upstream status lower-cased (e.g.
{ "status": "processing" }/"queued"/ β¦); when done{ "status": "completed", "video_url": "/v1/video/file/<id>" } - Errors:
400BAD_REQUEST (missingqueue_id/model) Β·404VIDEO_JOB_NOT_FOUND (unknown/expired) Β·403FORBIDDEN (queue_id belongs to another account) Β·410VIDEO_KEY_REVOKED (upstream provider key revoked mid-job β re-queue) Β·502VENICE_ERROR - Note: the operator pins the job to the Venice key that created it and routes every retrieve back to it automatically β you only ever handle your
cdm_key.
GET /v1/video/file/:id
Download the finished video (cached on the operator, not base64).
- Auth: none β served by its opaque
id(returned invideo_url) - 200: the video file (
video/mp4) Β· 404 NOT_FOUND (expired/unknown)
7. Models & capacity
GET /models
Native catalog grouped by category (text / image / video / β¦), with tiers & capabilities.
- Auth: none Β· 200: catalog grouped by type
GET /v1/models
OpenAI-compatible flat model list (works with client.models.list()), enriched with carpe_diem_type, tier, privacy, capabilities, context_length, voices (TTS), pricing.
-
Auth: none Β· 200:
{ "object": "list", "data": [{ "id", "object": "model", "owned_by", "tier", "carpe_diem_type", β¦ }] } -
carpe_diem_typeβ the catalog discriminator that routes a model to its endpoint. Values:text | code | embedding | image | imageEdit | upscale | tts | asr | music | video | imageToVideo. Use this rather than parsing model names β names are not reliable indicators (venice-uncensored-role-playis atextmodel,topaz-video-upscaleis video upscaling, etc.). Mapping:carpe_diem_typeEndpoint text,code/v1/chat/completionsor/v1/messagesembedding/v1/embeddingsimage/v1/image/generate(sync) or/v1/image/generate/queue(async, heavy models)imageEdit/v1/image/edit(sync) or/v1/image/edit/queue(async, heavy models e.g. gpt-image-2-edit)upscale/v1/image/upscaletts/v1/audio/speechasr/v1/audio/transcriptionsmusic/v1/audio/music/queuevideo,imageToVideo/v1/video/queue -
voicesβ present onttsentries only, lists the model's acceptedvoiceenum (passed through from Venice'smodel_spec.voices). Empty array means Venice didn't publish a catalog for that model β omitvoiceand let the model use its default.
GET /pricing
Per-model pricing + fixed (per-image / per-video) costs.
- Auth: none Β· 200:
{ "models", "fixedCost", "updatedAt" }
GET /v1/capacity Β· ?model=<id>
Marketplace capacity snapshot β the numbers behind dynamic pricing.
- Auth: none (cached 10s)
- 200 (no model):
{ "headroomUsd", "activeKeyCount", "keyCount", "keysByState", "multiplier": { "demand", "markup", "total" }, "warmingUp", "diemUtilization": { "u", "totalCapDiem", "totalCurrentDiem", "totalConsumedDiem", "keyCount", "hasData" } } - 200 (
?model=): per-model view{ "active_providers", "total_providers", "available_rpm", "queue_depth", "health" }
GET /v1/limits/:model
Venice's per-model rate limits (RPM / TPM / RPD), aggregated across provider keys β to self-throttle before a 429.
- Auth: none (cached 30s)
- 200:
{ "model", "totals": { "rpm", "tpm", "rpd" }, "keys": [...], "coverage" }
GET /v1/rate_limits
Your request-rate limit at the operator (per-wallet throttle) β distinct from Venice's per-model limits above.
- Auth: API key or JWT
- 200:
{ "max", "windowMs", "keying", "remaining", "resetSeconds" }
8. Credits & billing
GET /v1/credits Β· GET /v1/billing/balance
Same payload β your live spendable balance, for agents to check before a call.
- Auth: API key or JWT
- 200:
{ "escrowUsdc", "pendingUsdc", "holdsUsdc", "availableUsdc", "escrowCredits", "pendingCredits", "holdsCredits", "availableCredits", "updatedAt" } escrow= purchased Β·pending= spent, not yet settled Β·holds= reserved for in-flight requests Β·available= actually spendable. 1 credit = $0.01.- Errors:
503BALANCE_CHECK_FAILED (balance source unavailable)
GET /buyer/usage
Per-request usage history (buyer side), newest first, paginated.
- Auth: API key or JWT
- Query:
limit(1β100, default 20),offset - 200:
{ "events": [{ "id", "model", "provider", "prompt_tokens", "completion_tokens", "cost_usdc", "multiplier", "created_at" }], "total" }
GET /buyer/usage/summary
Aggregated by day and model.
- Auth: API key or JWT Β· Query:
days(1β90, default 7) - 200:
{ "byModel": [...], "byDay": [...], "totals": { "requests", "tokens", "cost" } }
GET /buyer/api-keys/usage
Usage broken down per API key.
- Auth: API key or JWT Β· Query:
days - 200:
{ "keys": [...], "windowDays" }
GET /buyer/debt
Pending (unsettled) debt for your wallet.
- Auth: API key or JWT
- 200:
{ "pendingUsdc", "pendingUsdcMicro", "pendingCredits" }
9. Deposits
GET /deposits/quote
Quote for buying credits with a non-USDC token β swaps token β USDC into the escrow in a single depositWithSwap tx. (Plain USDC deposits need no quote.)
- Auth: none
- Query:
token(ERC-20 address),amount(integer, raw token units) - 200:
{ "swapTarget", "approvalTarget", "swapData", "minUsdcOut", "estimatedUsdcOut" }β feed these into the escrow'sdepositWithSwap - Errors:
400missing/invalid token or amount Β·502QUOTE_FAILED
10. Provider API
For DIEM holders who provision a Venice key (see Guide Β§6).
POST /tee/provision
Provision (or re-provision) your Venice key into the TEE.
- Auth: wallet signature β
signaturemust recover to your provider wallet (the operator never sees the private key) - Body:
{ "apiKey", "message", "signature", "persist": true }apiKeyβ your Venice inference key (must matchVENICE_INFERENCE_KEY_<40-48 chars>, β€ 200 chars).messageβ the human-readable string you signed. The server only requires that it contain a lineTimestamp: <ms-since-epoch>(rejected if older than 5 min). Recommended canonical format (matches the dashboard):Provision Venice API key Wallet: 0x<your-wallet> Timestamp: 1748345678901signatureβpersonal_sign(EIP-191) overmessage. The recovered address becomes the provider wallet.persist(optional, default true) β set tofalsefor an ephemeral key (RAM-only, lost on operator restart).
- 200:
{ "status": "Provisioned", "provider": "0xβ¦", "keyId": "<16-hex>", "walletKeyCount", "totalProviders", "totalKeys", "persist" } - Errors:
400missing/invalid input Β·403INVALID_SIGNATUREΒ·400REPLAY_REJECTED(timestamp expired/missing) Β·400INVALID_KEY_FORMATΒ·409DUPLICATE_KEYΒ·409MAX_KEYS_REACHED
GET /tee/status
TEE security status + provisioned providers (public β transparency).
- Auth: none
- 200:
{ "status", "encryption", "memoryProtection", "providerCount", "keyCount", "persistCount", "ramOnlyCount", "backup", "providers": [...], "wallets": [...] }
GET /tee/providers
List provider addresses.
- Auth: none
GET /attestation
Hardware attestation of the running TEE (verifiable proof of the code in the enclave).
- Auth: none
DELETE /tee/providers/:address Β· DELETE /tee/providers/:address/keys/:keyId
Revoke a whole provider, or a single key.
- Auth: admin JWT or a wallet signature matching
:address - 200: revoked β the key(s) stop receiving traffic immediately
GET /provider/stats
Request counts + earnings. Public returns aggregate only; authenticated returns your own; admin sees all + per-provider.
- Auth: optional (more detail when authenticated)
GET /provider/usage
Per-request log of what your key served, with timestamps (see Guide Β§6.4).
- Auth: API key or JWT (your own); admin can target any via
?provider=0x⦠- Query:
limit(1β100, default 20),offset - 200:
{ "events": [{ "id", "model", "prompt_tokens", "completion_tokens", "cost_usdc", "multiplier", "created_at" }], "total" }
GET /provider/usage/summary
Aggregated by day and model.
- Auth: your own; admin via
?provider=0xβ¦or?all=1Β· Query:days(1β90, default 7) - 200:
{ "byModel", "byDay", "totals" }
GET /providers/:wallet/yield
Two APRs for the earnings dashboard.
- Auth: the wallet itself (or admin)
- 200:
{ "apr24h", "apr7d", "rewards24hDiem", "consumed24hDiem", "rewards7dDiem", "weightedAvgAvailable7dDiem", "warmupRemainingHours", "asOf" }
GET /providers/:wallet/rewards/daily
Per-UTC-day breakdown of consumption vs DIEM reward, with APR β powers the provider Revenue dashboard.
- Auth: the wallet itself (or admin)
- Query:
days(1β90, default 30) - 200:
{ "wallet", "days", "rows": [{ "day", "consumedUsd", "rewardDiem", "rewardUsd", "aprPct", "consumedUnavailable" }], "totals": { "consumedUsd", "rewardDiem", "rewardUsd", "aprPct", "consumedDataFrom" } } - Notes:
aprPct = rewardDiem / consumedUsd Γ 365 Γ 100(DIEM-price-independent).consumedUnavailable: truemarks days with reward but no consumption history (data predates the local ledger); the totalaprPctis aligned to days that have consumption.
11. Errors & retries
All errors return JSON: { "error": "<message>", "code": "<CODE>" }. Some add fields β e.g. 402 includes credits_available / credits_required; rate-limit errors include a reset hint.
| HTTP | code | Meaning | Retry? |
|---|---|---|---|
| 400 | BAD_REQUEST Β· INVALID_MODEL Β· MODEL_ERROR | Malformed request / unknown model / bad params | No β fix it |
| 401 | AUTH_REQUIRED Β· AUTH_FAILED Β· TOKEN_EXPIRED | Missing/invalid key or expired session | No β re-auth |
| 402 | PAYMENT_REQUIRED | Insufficient credits | No β buy credits |
| 403 | OFAC_BLOCKED Β· FORBIDDEN Β· JWT_REQUIRED | Not allowed for this caller | No |
| 404 | NOT_FOUND Β· VIDEO_JOB_NOT_FOUND | Unknown / expired resource | No |
| 410 | VIDEO_KEY_REVOKED | Provider key revoked mid-job | Re-queue |
| 413 | PAYLOAD_TOO_LARGE | Request body too large | No |
| 429 | ENDPOINT_RATE_LIMITED Β· UPSTREAM_RATE_LIMIT | Operator throttle / Venice 429 | Yes β back off |
| 451 | β | Geo-restricted region | No |
| 502 | VENICE_ERROR Β· QUOTE_FAILED | Upstream Venice / quote failure | Yes β transient |
| 503 | NO_PROVIDERS Β· NO_PROVIDER_CAPACITY Β· INSUFFICIENT_PROVIDER_CAPACITY Β· MODEL_INFRA_SATURATED Β· TEE_NOT_READY Β· BALANCE_CHECK_FAILED | No capacity / warming up | Yes β transient |
Retry strategy
- 429 β back off and retry; respect the reset hint (or
GET /v1/rate_limits). - 502 / 503 β transient (Venice, provider, or RPC); retry with exponential backoff (e.g. 1s β 2s β 4s, a few attempts).
- 402 β stop and top up credits.
- Other 4xx (400 / 401 / 403 / 404 / 410 / 413) β fatal; fix the request, don't retry blindly.
12. Health
GET /health
Service health for monitoring / uptime checks.
- Auth: none
- 200:
{ "status": "healthy" | "degraded", "components": { "database", "settlement", "providers", "creditsBootstrap", "settlementFreshness" } }β each component is{ "status", "detail"? }. Returnsdegraded(still HTTP 200) if any component is degraded.
13. On-chain verification
The operator publishes per-user weekly snapshots of points and USDC credits on-chain via OperatorSnapshotRegistry so the full state is reconstructible from Base alone, without trusting the operator's database. Every Friday after the weekly points snapshot, the operator emits one event per active user for each topic, locked one-shot per (weekNumber, topic).
- Contract β
0x23417CF66DF7cde16bE929576f6246cCa180b05Fon Base (deploy block 46,499,216) - Events:
PointsSnapshot(address indexed user, uint64 indexed weekNumber, uint256 pointsEarnedMicros, uint256 totalPointsMicros)CreditsSnapshot(address indexed user, uint64 indexed weekNumber, uint256 balanceUsdcMicros)
- Scaling β all amounts are
uint256micros (1e-6 units), matching USDC's 6-decimal convention. Divide by 1e6 to recover the human-readable value (e.g.19_204_567_000= 19,204.567 points;5_000_000= $5.00).
GET /snapshot/status
Public summary of which weeks have been published.
- Auth: none
- 200:
{ "currentWeek", "registry", "pointsPublished": [<weeks>], "creditsPublished": [<weeks>] }
Reconstruct your data yourself
Scan the events directly via your favorite Base RPC (mainnet.base.org) with eth_getLogs filtered by the contract address and an address topic (your wallet). The reference CLI is at operator/scripts/reconstruct-from-chain.ts β it produces a JSON dump re-injectable into an empty SQLite if you ever need to verify the full state without going through the operator API.
14. Pay-per-request (x402) β preview
Status: preview, testnet only (Base Sepolia) for now, behind the operator flag
X402_DIRECT_ENABLED. The shape below is stable enough to build against; the mainnet rollout will reuse it verbatim.
x402 is an alternative payment rail: instead of depositing USDC and spending a balance (the Credits flow), the caller pays directly from their wallet, per request, by signing one gasless EIP-2612 permit. No deposit, no account, no gas β ideal for agents.
How it differs from credits β in the API
| Credits (deposited balance) | x402 (direct wallet) | |
|---|---|---|
| Endpoint | /v1/chat/completions, /v1/image/generate, β¦ | the /v1/x402/* mirror (table below) |
| Auth / payment | Authorization: Bearer <api-key | JWT> | X-PAYMENT header (signed permit) β no Authorization |
| Prerequisite | a funded balance (USDC deposited to the escrow) | nothing β sign a permit on the fly |
| Settlement | off-chain balance debit, settled on-chain in batches | the escrow pulls the exact cost on-chain per request |
| Response | credit headers + usage in body | headers X-Payment-Tx, X-Payment-Amount-Usdc |
The two namespaces are independent: x402 routes ignore Authorization; credit routes ignore X-PAYMENT.
Endpoints
| x402 endpoint | Mode | Mirrors |
|---|---|---|
POST /v1/x402/chat/completions | sync | /v1/chat/completions |
POST /v1/x402/audio/speech | sync | /v1/audio/speech |
POST /v1/x402/image/generate | sync | /v1/image/generate |
POST /v1/x402/image/generate/queue β /v1/x402/image/generate/retrieve | async | heavy image models |
POST /v1/x402/image/edit | sync | /v1/image/edit |
POST /v1/x402/image/edit/queue β /v1/x402/image/edit/retrieve | async | heavy edit models |
POST /v1/x402/video/queue β /v1/x402/video/retrieve | async | /v1/video/* |
Request bodies are identical to the credit endpoints they mirror (same model, messages / prompt / input, etc.).
Flow (two round-trips)
1. No payment β 402 challenge. Call the endpoint without X-PAYMENT; you get the payment requirements:
// 402 Payment Required
{
"x402Version": 1,
"error": "X-PAYMENT header required",
"accepts": [{
"scheme": "exact",
"network": "base-sepolia", // "base" on mainnet
"maxAmountRequired": "1826", // max chargeable, atomic USDC (6 dec) β a string
"payTo": "0xβ¦escrow", // permit spender
"asset": "0xβ¦USDC", // permit verifyingContract
"resource": "/v1/x402/chat/completions",
"maxTimeoutSeconds": 600,
"extra": { "name": "USD Coin", "version": "2" } // the USDC EIP-712 domain
}]
}
2. Sign an EIP-2612 permit and resend with X-PAYMENT. The permit authorizes the escrow (payTo) to pull up to a self-chosen session budget (value) before deadline. Sign the EIP-712 Permit against the domain { name, version } from extra, chainId, verifyingContract: asset:
import { Wallet, JsonRpcProvider, Contract } from "ethers";
const acc = challenge.accepts[0];
const value = 1_000_000n; // $1.00 session budget (β₯ maxAmountRequired, β₯ $0.10 min)
const nonce = await usdc.nonces(owner); // EIP-2612 nonce on the `asset` token
const deadline = BigInt(Math.floor(Date.now()/1000) + 3600); // β₯ now + 120s
const signature = await wallet.signTypedData(
{ name: acc.extra.name, version: acc.extra.version, chainId, verifyingContract: acc.asset },
{ Permit: [
{ name:"owner", type:"address" }, { name:"spender", type:"address" },
{ name:"value", type:"uint256" }, { name:"nonce", type:"uint256" }, { name:"deadline", type:"uint256" },
]},
{ owner, spender: acc.payTo, value, nonce, deadline },
);
const auth = { owner, spender: acc.payTo, value: value.toString(),
nonce: nonce.toString(), deadline: deadline.toString(), signature };
const xPayment = btoa(JSON.stringify({ payload: auth })); // base64
curl -X POST "$BASE/v1/x402/chat/completions" \
-H "X-PAYMENT: $XPAYMENT" -H "Content-Type: application/json" \
-d '{"model":"llama-3.3-70b","messages":[{"role":"user","content":"hi"}]}'
# β 200 + body
# X-Payment-Tx: 0x⦠(the on-chain pull)
# X-Payment-Amount-Usdc: 0.000890 (actual cost charged, β€ your budget)
X-PAYMENT=base64(JSON.stringify({ payload: <permit auth> })). The bare<permit auth>(without thepayloadwrapper) is also accepted.valueis a session budget, not the per-request price. It must be β₯maxAmountRequiredand β₯ $0.10 (0.10USDC). The escrow pulls the actual per-request cost (β€value) each time.- Reuse the same
X-PAYMENTacross many requests β sign once, resend the header until the budget is spent ordeadlinepasses (must stay β₯ 120 s out). The first request applies the permit on-chain; later ones pull against the granted allowance.
Async (heavy image / video)
Same idea, but the permit travels on the β¦/queue call; β¦/retrieve is polled with just { queue_id } (no payment header). The operator pulls on stored success only β a failed generation is never charged (video is charged at queue-success, matching the credit flow). retrieve surfaces X-Payment-Tx / X-Payment-Amount-Usdc once settled.
Errors
402βX-PAYMENTrequired (the challenge) Β·X402_BAD_PAYMENT(malformed header) Β·X402_INVALID(permit fails verification β wrong spender, budget below minimum, expired/too-soondeadline, bad signature) Β·X402_SETTLE_FAILED(inference served but the on-chain pull failed)400INVALID_MODEL/BAD_REQUESTΒ·502UPSTREAM_ERROR/UPSTREAM_FAILEDΒ·503TEE_NOT_READY/NO_PROVIDERS