● Documentation

Carpe Diem — API Reference

Complete endpoint reference: authentication, chat, images, audio, video, embeddings, credits, and provider endpoints.

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.

ContractAddressVerify
CarpeEscrow (UUPS proxy)0x15917768b31CB1DC61d9d858f2419BF45044005dBaseScan · Sourcify ✓
↳ Implementation0x53344b3b0a89CaeACD9D0B50bf5B1D21C4A1e9b4BaseScan · Sourcify ✓
USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913BaseScan
DIEM (Venice)0xF4d97F2da56e8c3098f3a8D538DB630A2606a024BaseScan

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: 400 bad input · 401 AUTH_FAILED (signature mismatch / expired message) · 403 OFAC_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 by data: [DONE])
  • Headers: X-Carpe-*-Credits report the request cost and remaining balance
  • Errors: 402 PAYMENT_REQUIRED (insufficient credits) · 400 invalid model/params · 429 rate limited · 502 VENICE_ERROR · 503 no 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: model is 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.

ParamBehaviourScope
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):

  1. Use claude-opus-4-8 for the request that needs a guaranteed function call — opus-4-8 honours tool_choice fully. Switch back to Fable 5 for everything else.
  2. Or leave tool_choice unset 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:

  1. Prompt-driven: prefix the user message with "Show your reasoning step by step before answering." The chain appears in the standard content[].text block.
  2. Venice-native: switch to claude-opus-4-6 / 4-7 / claude-sonnet-4-5 / 4-6 and pass reasoning_effort: "high" — the chain is then returned in a separate reasoning_content field (Venice convention, not Anthropic's). claude-fable-5 does not expose reasoning effort levels (supportsReasoningEffort: false in GET /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: 402 PAYMENT_REQUIRED · 400 invalid model/params · 429 rate limited · 502 VENICE_ERROR · 503 no 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/generate is 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 a 502 at 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"? }variants 1–4 (default 1)
  • 200: Venice image payload (base64-encoded image(s))
  • Errors: 402 PAYMENT_REQUIRED · 400 invalid/missing prompt (max 10,000 chars) · 502 VENICE_ERROR · 503 no 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/queueAuth: API key or JWT · Body: same as /v1/image/generate · 202: { "queue_id", "status": "pending" } · Errors: 402 · 400 · 503 QUEUE_FULL (too many in-flight jobs) · 503 no providers / no capacity
  • POST /v1/image/generate/retrieveBody: { "queue_id" } · 200 (pending): JSON { "status": "pending", "queue_id" } — poll again · 200 (done): the binary image (Content-Type: image/*, header X-Carpe-Image-Status: completed) · Errors: 403 queue_id belongs to another wallet · 404 JOB_NOT_FOUND (unknown or expired) · upstream error code on failed generation
  • POST /v1/image/generate/completeBody: { "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"? }
  • image format: 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/edit is synchronous and the edge caps a request at ~60–120s. The highest-quality edit models — notably gpt-image-2-edit — routinely run longer and return a 502 at 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/queueAuth: 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) · 413 image > 5 MB · 503 QUEUE_FULL · 503 no providers / no capacity
  • POST /v1/image/edit/retrieveBody: { "queue_id" } · 200 (pending): { "status": "pending", "queue_id" } — poll again · 200 (done): the binary image (Content-Type: image/*) · Errors: 403 other wallet · 404 JOB_NOT_FOUND · upstream error on failed edit
  • POST /v1/image/edit/completeBody: { "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"? }scale 1–4 (default 2)
  • image format: raw base64, no data: prefix. Different from /v1/image/edit which wants the full data URI.
  • Constraints: source ≥ 256×256 pixels. Smaller inputs return 400 BAD_REQUEST (Invalid or corrupt image).
  • 200: upscaled image payload · Errors: 400 scale 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.

  • POSTAuth: API key or JWT · Body: the image to share · 200: { "id", "url" }
  • GET /v1/image/share/:idAuth: 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"? }input 1–50,000 chars
  • voice is 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 voice entirely, Venice picks the model's default.
    • To pick a specific voice, query GET /v1/models — every entry with carpe_diem_type: "tts" carries a voices array of accepted values (e.g. tts-kokoro has 54, tts-gemini-3-1-flash has 30).
  • 200: binary audio (e.g. audio/mpeg)
  • Billing: per character
  • Errors: 402 PAYMENT_REQUIRED · 400 invalid/missing input / unknown voice for this model · 502 VENICE_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: 400 multipart required / no file · 502 VENICE_ERROR

POST /v1/audio/music/queue · POST /v1/audio/music/retrieve

Music generation — asynchronous (same pattern as video).

  • queueAuth: API key or JWT · Body: { "model", "prompt", "lyrics_prompt"?, … } · 200: { "queue_id" }
  • retrieveBody: { "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):

BehaviourModels
Required — sending only prompt returns 400minimax-music-v2, minimax-music-v25, minimax-music-v26
Optional — accepts payloads with or without itace-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 unfamiliar duration / aspect_ratio / param set without being charged — Venice returns 400 if the model rejects a field.
  • 200: Venice's quote payload
  • Errors: 400 (model rejected a field — e.g. unsupported duration) · 502 VENICE_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 }

  • duration is required. String with "s" suffix (e.g. "5s", not 5 or 5.0). Omitting it returns 400. The accepted enum is per-family — empirically validated via /v1/video/quote 2026-06-07 (seedance re-validated 2026-06-24 — the 2-0 line now accepts "15s"):

    FamilyAccepted duration
    sora-*"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_ratio is optional. "16:9" and "9:16" are accepted by every family. "1:1" works on most (rejected by sora-*, veo3*, longcat*, happyhorse-*). "4:3" / "3:4" accepted only by seedance*, 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 an https:// URL or a data: 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/quote first with the exact payload — same verbatim passthrough, no charge, returns 400 if any field (duration, aspect_ratio, image params, …) is rejected by the model. Cheaper than debugging in queue (which commits a hold against your balance).

  • 200: Venice's queue payload. The job id is in id (current schema) or queue_id (older shape) — use whichever is present when you call retrieve. Credit headers included.

  • Errors: 402 PAYMENT_REQUIRED · 400 INVALID_MODEL / BAD_REQUEST (prompt / unsupported duration / unsupported aspect_ratio) · 503 NO_PROVIDERS / INSUFFICIENT_PROVIDER_CAPACITY / TEE_NOT_READY · 502 VENICE_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: 400 BAD_REQUEST (missing queue_id/model) · 404 VIDEO_JOB_NOT_FOUND (unknown/expired) · 403 FORBIDDEN (queue_id belongs to another account) · 410 VIDEO_KEY_REVOKED (upstream provider key revoked mid-job — re-queue) · 502 VENICE_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 in video_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-play is a text model, topaz-video-upscale is video upscaling, etc.). Mapping:

    carpe_diem_typeEndpoint
    text, code/v1/chat/completions or /v1/messages
    embedding/v1/embeddings
    image/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/upscale
    tts/v1/audio/speech
    asr/v1/audio/transcriptions
    music/v1/audio/music/queue
    video, imageToVideo/v1/video/queue
  • voices — present on tts entries only, lists the model's accepted voice enum (passed through from Venice's model_spec.voices). Empty array means Venice didn't publish a catalog for that model — omit voice and 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: 503 BALANCE_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's depositWithSwap
  • Errors: 400 missing/invalid token or amount · 502 QUOTE_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 — signature must recover to your provider wallet (the operator never sees the private key)
  • Body: { "apiKey", "message", "signature", "persist": true }
    • apiKey — your Venice inference key (must match VENICE_INFERENCE_KEY_<40-48 chars>, ≤ 200 chars).
    • message — the human-readable string you signed. The server only requires that it contain a line Timestamp: <ms-since-epoch> (rejected if older than 5 min). Recommended canonical format (matches the dashboard):
      Provision Venice API key
      Wallet: 0x<your-wallet>
      Timestamp: 1748345678901
      
    • signaturepersonal_sign (EIP-191) over message. The recovered address becomes the provider wallet.
    • persist (optional, default true) — set to false for an ephemeral key (RAM-only, lost on operator restart).
  • 200: { "status": "Provisioned", "provider": "0x…", "keyId": "<16-hex>", "walletKeyCount", "totalProviders", "totalKeys", "persist" }
  • Errors: 400 missing/invalid input · 403 INVALID_SIGNATURE · 400 REPLAY_REJECTED (timestamp expired/missing) · 400 INVALID_KEY_FORMAT · 409 DUPLICATE_KEY · 409 MAX_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: true marks days with reward but no consumption history (data predates the local ledger); the total aprPct is 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.

HTTPcodeMeaningRetry?
400BAD_REQUEST · INVALID_MODEL · MODEL_ERRORMalformed request / unknown model / bad paramsNo — fix it
401AUTH_REQUIRED · AUTH_FAILED · TOKEN_EXPIREDMissing/invalid key or expired sessionNo — re-auth
402PAYMENT_REQUIREDInsufficient creditsNo — buy credits
403OFAC_BLOCKED · FORBIDDEN · JWT_REQUIREDNot allowed for this callerNo
404NOT_FOUND · VIDEO_JOB_NOT_FOUNDUnknown / expired resourceNo
410VIDEO_KEY_REVOKEDProvider key revoked mid-jobRe-queue
413PAYLOAD_TOO_LARGERequest body too largeNo
429ENDPOINT_RATE_LIMITED · UPSTREAM_RATE_LIMITOperator throttle / Venice 429Yes — back off
451Geo-restricted regionNo
502VENICE_ERROR · QUOTE_FAILEDUpstream Venice / quote failureYes — transient
503NO_PROVIDERS · NO_PROVIDER_CAPACITY · INSUFFICIENT_PROVIDER_CAPACITY · MODEL_INFRA_SATURATED · TEE_NOT_READY · BALANCE_CHECK_FAILEDNo capacity / warming upYes — 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"? }. Returns degraded (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).

  • Contract0x23417CF66DF7cde16bE929576f6246cCa180b05F on 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 uint256 micros (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.