Carpe Diem — SDK Integration Guide
Base URL:
https://carpe-diem.xyz/api/operator/v1Carpe Diem is fully OpenAI and Anthropic compatible. Any SDK or tool that works with OpenAI or Anthropic works with Carpe Diem — just change the base URL and API key. Works with Claude Code, Cursor, Cline, and all standard SDKs.
Table of Contents
- Quick Start
- Authentication
- Chat Completions
- Streaming
- Image Generation
- Image Editing
- Video Generation (Async)
- Models
- Payment & Billing
- Usage Tracking
- Error Handling
- Rate Limits
- Health & Status
- Provider Integration
- Endpoint Reference
1. Quick Start
Get an API Key
- Connect your wallet on the dashboard
- Sign a message to create a session
- Go to API Keys → Create Key
- Copy the key (shown only once) — it starts with
cdm_
Python (OpenAI SDK)
from openai import OpenAI
client = OpenAI(
base_url="https://carpe-diem.xyz/api/operator/v1",
api_key="cdm_your_api_key_here",
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)
JavaScript (OpenAI SDK)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://carpe-diem.xyz/api/operator/v1",
apiKey: "cdm_your_api_key_here",
});
const response = await client.chat.completions.create({
model: "llama-3.3-70b",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.choices[0].message.content);
cURL
curl https://carpe-diem.xyz/api/operator/v1/chat/completions \
-H "Authorization: Bearer cdm_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"model": "llama-3.3-70b",
"messages": [{"role": "user", "content": "Hello!"}]
}'
Python (Anthropic SDK)
import anthropic
client = anthropic.Anthropic(
base_url="https://carpe-diem.xyz/api/operator",
api_key="cdm_your_api_key_here",
)
message = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)
print(message.content[0].text)
Claude Code / Cursor / Cline
Set these environment variables and use Claude Code normally:
export ANTHROPIC_BASE_URL=https://carpe-diem.xyz/api/operator
export ANTHROPIC_API_KEY=cdm_your_api_key_here
Available Claude models: claude-opus-4-7, claude-opus-4-6, claude-opus-4-6-fast, claude-sonnet-4-6, claude-opus-4-5, claude-sonnet-4-5 — plus all Venice open-source models (LLaMA, Mistral, Qwen, Gemini, GPT, etc.).
2. Authentication
Carpe Diem supports two authentication methods. Both use the Authorization: Bearer <token> header.
API Keys (cdm_ prefix)
Persistent keys tied to your wallet. Best for bots, scripts, and production backends.
Create a key (requires JWT session):
curl -X POST https://carpe-diem.xyz/api/operator/auth/api-keys \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{"name": "my-bot"}'
Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"key": "cdm_a1b2c3d4e5f6...",
"prefix": "cdm_a1b2c3d4..."
}
The full key is shown only once. Store it securely.
List keys (requires JWT):
GET /auth/api-keys
Revoke a key (requires JWT):
DELETE /auth/api-keys/:id
Per-key payment rails
The payment rail (see §9) is a property of the key, set once at creation — so an app using your key never has to deal with payment. POST /auth/api-keys accepts three optional fields:
curl -X POST https://carpe-diem.xyz/api/operator/auth/api-keys \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "my-bot",
"rail": "prepaid",
"fallback": true,
"prepaidAccount": "0xYourDedicatedSmartAccount"
}'
| Field | Type | Description |
|---|---|---|
rail | "auto" | "credits" | "prepaid" | Which rail backs this key. auto (default) uses prepaid if an account is registered, else credits. |
fallback | boolean | If the primary rail can't cover a request, fall back to the other rail instead of failing. Default false. |
prepaidAccount | address | Register the dedicated smart account this key pulls from (required for rail: "prepaid"). |
Omit all three for the classic credits behaviour — fully backward-compatible. Rail settings are scoped to the key; keys without a rail set inherit your wallet-level default.
JWT Sessions
Short-lived tokens from wallet signature. Best for dApps and frontends.
Create a session:
curl -X POST https://carpe-diem.xyz/api/operator/auth/session \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xYourWalletAddress",
"message": "Sign in to Carpe Diem\nWallet: 0xYourWalletAddress\nTimestamp: 1707654321000",
"signature": "0x..."
}'
Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"wallet": "0xyourwalletaddress",
"expiresIn": "7d"
}
Refresh a session (before expiry):
curl -X POST https://carpe-diem.xyz/api/operator/auth/refresh \
-H "Authorization: Bearer <jwt_token>"
Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"wallet": "0xyourwalletaddress",
"expiresIn": "7d"
}
Sessions have a maximum lifetime of 24 hours. After that, you must re-authenticate with a wallet signature.
Which to use?
| Use case | Method |
|---|---|
| Bot / script / backend | API Key |
| dApp / browser frontend | JWT Session |
| Creating/managing API keys | JWT Session (required) |
3. Chat Completions
POST /v1/chat/completions
Fully OpenAI-compatible. Proxied through Carpe Diem's multi-provider network.
Request
{
"model": "llama-3.3-70b",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Explain quantum computing in simple terms." }
],
"temperature": 0.7,
"top_p": 0.9,
"max_tokens": 1024,
"stream": false,
"venice_parameters": {
"enable_web_search": "auto",
"enable_web_scraping": true
}
}
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model ID (see Models). Lowercase, max 100 chars. |
messages | array | Yes | Conversation messages (max 2000). Each has role and content. |
temperature | number | No | Sampling temperature, 0–2. Default varies by model. |
top_p | number | No | Nucleus sampling, 0–1. |
max_tokens | integer | No | Maximum completion tokens, 1–16384. |
stream | boolean | No | Enable SSE streaming. Default false. |
venice_parameters | object | No | Passthrough to Venice. Web search (enable_web_search: off, auto or on) and URL scraping (enable_web_scraping) are off unless set here; what they retrieve counts as input tokens, and each search or page read adds Venice's $0.01 fee, with the same multiplier as the tokens. |
Response (non-streaming)
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1707654321,
"model": "llama-3.3-70b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Quantum computing uses..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 25,
"completion_tokens": 150,
"total_tokens": 175
}
}
4. Streaming
Set "stream": true to receive Server-Sent Events (SSE).
SSE Format
Each event is a line prefixed with data: followed by JSON. The stream ends with data: [DONE].
data: {"id":"chatcmpl-abc123","choices":[{"delta":{"role":"assistant"},"index":0}]}
data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":"Quantum"},"index":0}]}
data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":" computing"},"index":0}]}
data: {"id":"chatcmpl-abc123","choices":[],"usage":{"prompt_tokens":25,"completion_tokens":150}}
data: [DONE]
Python Streaming
from openai import OpenAI
client = OpenAI(
base_url="https://carpe-diem.xyz/api/operator/v1",
api_key="cdm_your_api_key_here",
)
stream = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": "Tell me a story"}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
JavaScript Streaming
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://carpe-diem.xyz/api/operator/v1",
apiKey: "cdm_your_api_key_here",
});
const stream = await client.chat.completions.create({
model: "llama-3.3-70b",
messages: [{ role: "user", content: "Tell me a story" }],
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) process.stdout.write(content);
}
Manual SSE Parsing (JavaScript)
const response = await fetch("https://carpe-diem.xyz/api/operator/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer cdm_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "llama-3.3-70b",
messages: [{ role: "user", content: "Hello" }],
stream: true,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
for (const line of text.split("\n")) {
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
const data = JSON.parse(line.slice(6));
const content = data.choices?.[0]?.delta?.content;
if (content) process.stdout.write(content);
}
}
5. Image Generation
POST /v1/image/generate
Request
{
"model": "flux-2-pro",
"prompt": "A sunset over a cyberpunk city",
"negative_prompt": "blurry, low quality",
"width": 1024,
"height": 1024,
"steps": 30,
"cfg_scale": 7.0,
"seed": 42,
"safe_mode": false,
"format": "webp"
}
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Image model ID (see Models). |
prompt | string | Yes | Text description of the image. |
negative_prompt | string | No | What to avoid in the image. |
width | integer | No | Image width in pixels. Default 1024. |
height | integer | No | Image height in pixels. Default 1024. |
steps | integer | No | Inference steps (more = higher quality, slower). |
cfg_scale | number | No | Prompt adherence strength. |
seed | integer | No | Deterministic seed for reproducibility. |
safe_mode | boolean | No | Enable safety filter. Default false. |
format | string | No | Output format: "webp", "png", "jpeg". Default "webp". |
Response
The response is the raw JSON from the image provider, typically containing base64-encoded image data.
Pricing
| Model | Cost per image |
|---|---|
| nano-banana-pro | $0.02 (2 credits) |
| flux-2-pro | $0.03 (3 credits) |
| flux-2-max | $0.05 (5 credits) |
| gpt-image-1-5 | $0.08 (8 credits) |
Heavy models — async queue (/queue → /retrieve → /complete)
POST /v1/image/generate is synchronous, and the edge proxy in front of the API caps a single request at ~60s. Models that routinely run longer — gpt-image-2, nano-banana-pro, recraft-v4-pro, flux-2-*, qwen-image-* — will return a 502 at the edge even though the image was produced. For those, use the async pattern (identical shape to Video): queue returns a queue_id in <1s, then you poll retrieve until it returns the binary image. There is no duration limit, and billing is charged only on a stored, retrievable success — a failed generation is never billed.
POST /v1/image/generate/queue— same body as/v1/image/generate→202 { "queue_id", "status": "pending" }POST /v1/image/generate/retrieve— body{ "queue_id" }→ pending: JSON{ "status": "pending" }· done: the binary image (Content-Type: image/*) · failed: the upstream errorPOST /v1/image/generate/complete— body{ "queue_id" }→ frees the result (best-effort; a 15-min TTL is the backstop)
Python
import time, requests
BASE = "https://carpe-diem.xyz/api/operator/v1"
H = {"Authorization": f"Bearer {API_KEY}"}
queue_id = requests.post(f"{BASE}/image/generate/queue", headers=H, json={
"model": "gpt-image-2",
"prompt": "a neon koi pond",
}).json()["queue_id"]
while True:
r = requests.post(f"{BASE}/image/generate/retrieve", headers=H,
json={"queue_id": queue_id})
if r.headers.get("content-type", "").startswith("image/"):
with open("out.webp", "wb") as f:
f.write(r.content) # completed
break
if r.status_code >= 400:
raise RuntimeError(r.text) # failed (not billed)
time.sleep(2) # pending — poll again
requests.post(f"{BASE}/image/generate/complete", headers=H,
json={"queue_id": queue_id}) # optional cleanup
JavaScript
const BASE = "https://carpe-diem.xyz/api/operator/v1";
const H = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
const { queue_id } = await (await fetch(`${BASE}/image/generate/queue`, {
method: "POST", headers: H,
body: JSON.stringify({ model: "gpt-image-2", prompt: "a neon koi pond" }),
})).json();
let imageBlob;
while (true) {
const r = await fetch(`${BASE}/image/generate/retrieve`, {
method: "POST", headers: H, body: JSON.stringify({ queue_id }),
});
const ct = r.headers.get("content-type") || "";
if (ct.startsWith("image/")) { imageBlob = await r.blob(); break; } // completed
if (!r.ok) throw new Error(await r.text()); // failed
await new Promise((s) => setTimeout(s, 2000)); // pending
}
await fetch(`${BASE}/image/generate/complete`, { // optional
method: "POST", headers: H, body: JSON.stringify({ queue_id }),
});
6. Image Editing
POST /v1/image/edit
Edit an existing image using AI-powered inpainting, style transfer, or montage.
Request
{
"model": "flux-2-max-edit",
"prompt": "Change the sky to a sunset",
"image": "data:image/png;base64,iVBORw0KGgo...",
"aspect_ratio": "16:9"
}
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Image edit model ID. |
prompt | string | Yes | Edit instruction. |
image | string | Yes | Base64-encoded image (with or without data: prefix). Max 5 MB. |
aspect_ratio | string | No | Output aspect ratio (e.g., "16:9", "1:1"). |
Response
{
"images": ["iVBORw0KGgo..."],
"format": "png"
}
The images array contains base64-encoded PNG data.
Pricing
| Model | Cost per edit |
|---|---|
| nano-banana-pro-edit | $0.02 (2 credits) |
| qwen-edit | $0.04 (4 credits) |
| seedream-v4-edit | $0.05 (5 credits) |
| flux-2-max-edit | $0.06 (6 credits) |
| gpt-image-1-5-edit | $0.08 (8 credits) |
Heavy edit models — async queue (/queue → /retrieve → /complete)
Like generation, POST /v1/image/edit is synchronous and the edge proxy caps a single request at ~60–120s. Real edits with the highest-quality models — notably gpt-image-2-edit — routinely run longer than that and will return a 502 at the edge even though the edit was produced. For those, use the async pattern (same shape as async generation and Video): queue returns a queue_id in <1s, then you poll retrieve until it returns the binary image. There is no duration limit, and billing is charged only on a stored, retrievable success — a failed edit is never billed.
POST /v1/image/edit/queue— same body as/v1/image/edit→202 { "queue_id", "status": "pending" }POST /v1/image/edit/retrieve— body{ "queue_id" }→ pending: JSON{ "status": "pending" }· done: the binary image (Content-Type: image/*) · failed: the upstream errorPOST /v1/image/edit/complete— body{ "queue_id" }→ frees the result (best-effort; a 15-min TTL is the backstop)
import requests, time, base64
queue_id = requests.post(f"{BASE}/image/edit/queue", headers=H, json={
"model": "gpt-image-2-edit",
"prompt": "Reframe as a cinematic wide shot, add a city skyline through the window",
"image": "data:image/png;base64,iVBORw0KGgo...",
"aspect_ratio": "16:9",
}).json()["queue_id"]
while True:
r = requests.post(f"{BASE}/image/edit/retrieve", headers=H, json={"queue_id": queue_id})
if r.headers["content-type"].startswith("application/json") and r.json().get("status") == "pending":
time.sleep(3); continue
r.raise_for_status()
open("edited.png", "wb").write(r.content) # binary image
break
requests.post(f"{BASE}/image/edit/complete", headers=H, json={"queue_id": queue_id}) # optional
Tip: route
gpt-image-2-edit(and any future slow edit model) through this async path by default. Fast edit models (e.g.nano-banana-pro-edit) still work on the synchronous/v1/image/edit.
7. Video Generation (Async)
Video generation is asynchronous — you queue a job, then poll for the result.
Step 1: Get a Quote
POST /v1/video/quote
{
"model": "ltx-2-fast-text-to-video",
"prompt": "A cat playing piano"
}
Returns cost_usdc — what /v1/video/queue charges for this payload at the current price (Venice's own quote × the dynamic multiplier) — and venice_quote_usd, Venice's figure. The queue prices the job the same way, so the quote is optional.
Step 2: Queue the Job
POST /v1/video/queue
{
"model": "wan-2.5-preview-text-to-video",
"prompt": "A cat playing piano",
"duration": "5s"
}
duration is required and per-model — a string with an "s" suffix ("5s", not 5). Read the accepted set off constraints.durations in GET /v1/models; it differs by family and version (the seedance-2-0 line takes every whole second from "4s" to "15s", seedance-2-5 to "30s", seedance-1-5 stops at "12s"). Validating any payload is free: POST /v1/video/quote returns 400 if a field is rejected.
On a Seedance reference-to-video edit or extend, duration: "auto" (or "-1") and aspect_ratio: "adaptive" (or "auto") make the output follow the source clip. Neither is listed in constraints; both are accepted. See the API reference for what they require.
Image conditioning passes through verbatim. Image-to-video and frame-control models take image fields alongside prompt — the operator forwards your body to Venice unchanged, filtering nothing:
{
"model": "wan-2.5-preview-image-to-video",
"prompt": "the photo comes to life",
"duration": "5s",
"image_url": "https://example.com/frame.png",
"end_image_url": "https://example.com/last-frame.png"
}
image_url— source / first frame (image-to-video, and the start frame for first-last-frame models).https://URL ordata:URI.end_image_url— last frame, for first-last-frame interpolation models.image_urls— array of reference images, for models that condition on a set.
Any other parameter the model accepts (seed, negative_prompt, motion/camera controls, …) also reaches Venice unchanged — consult the model's spec.
Response includes a queue_id for polling. VPS-backed models (Grok Imagine Private variants) also include a download_url:
{
"model": "grok-imagine-text-to-video-private",
"queue_id": "abc123",
"download_url": "https://...pre-signed-url..."
}
Save the download_url immediately — Venice only returns it once and it's valid for 24 hours. For VPS-backed models, you'll use this URL to fetch the video after status hits completed (the retrieve endpoint never returns it).
Step 3: Poll for Result
POST /v1/video/retrieve
{
"queue_id": "abc123",
"model": "wan-2.5-preview-text-to-video"
}
While processing:
{
"status": "processing",
"average_execution_time": 145000,
"execution_duration": 32000
}
When complete (standard models):
{
"status": "completed",
"video_url": "/v1/video/file/abc123",
"video": "data:video/mp4;base64,AAAAIGZ0eXBpc29t..."
}
When complete (VPS-backed / Grok Imagine):
{ "status": "completed" }
— no URL fields. Fetch the download_url you saved from /queue.
Error Handling (Critical)
POST /v1/video/retrieve returns several fatal status codes that should stop your polling loop immediately. Do not swallow these silently — re-queueing is the only recovery for most of them.
| Status | code | Meaning | Action |
|---|---|---|---|
200 | — | Processing or completed | Continue / use video |
404 | VIDEO_JOB_NOT_FOUND | Unknown / expired queue_id, or operator restart wiped the pin | Re-queue |
410 | VIDEO_KEY_REVOKED | Provider key removed mid-job, or pre-signed URL >24h old | Re-queue |
403 | FORBIDDEN | queue_id does not belong to this wallet | Check auth |
422 | VENICE_ERROR | Content moderation rejection | Modify prompt, do NOT retry |
429 | — | Rate limited | Back off and retry |
5xx | — | Transient operator/Venice error | Retry with exponential backoff |
Polling Pattern (Python)
import time
import requests
BASE = "https://carpe-diem.xyz/api/operator/v1"
HEADERS = {
"Authorization": "Bearer cdm_your_api_key_here",
"Content-Type": "application/json",
}
# Queue the job
queue_res = requests.post(f"{BASE}/video/queue", headers=HEADERS, json={
"model": "wan-2.5-preview-text-to-video",
"prompt": "A cat playing piano",
"duration": "5s",
})
queue_res.raise_for_status()
queue_data = queue_res.json()
queue_id = queue_data["queue_id"]
download_url = queue_data.get("download_url") # set for VPS-backed models
MAX_WAIT_SECONDS = 600
POLL_INTERVAL_SECONDS = 5
deadline = time.time() + MAX_WAIT_SECONDS
while time.time() < deadline:
r = requests.post(f"{BASE}/video/retrieve", headers=HEADERS, json={
"queue_id": queue_id,
"model": "wan-2.5-preview-text-to-video",
})
# FATAL codes — stop, surface to user, re-queue if appropriate
if r.status_code in (404, 410, 422):
raise RuntimeError(f"Generation failed: HTTP {r.status_code}: {r.text}")
# TRANSIENT codes — retry on the next poll iteration
if r.status_code in (429,) or 500 <= r.status_code < 600:
time.sleep(POLL_INTERVAL_SECONDS)
continue
r.raise_for_status()
result = r.json()
if result["status"] == "completed":
# Standard models: video_url/video are present in `result`
# VPS-backed: use the `download_url` saved from /queue above
video_ref = result.get("video_url") or result.get("video") or download_url
if not video_ref:
raise RuntimeError("Completed but no video URL available")
print(f"Video ready: {video_ref}")
break
time.sleep(POLL_INTERVAL_SECONDS)
else:
raise TimeoutError(f"Video did not complete within {MAX_WAIT_SECONDS}s")
Common pitfall — naive polling loops that ignore HTTP status codes (while True: poll(); if "completed" in result: break) will silently retry on 422 (content violation) and 404 (job lost) until your watchdog fires. Always branch on the status code first.
Pricing
Text-to-Video:
| Model | Cost |
|---|---|
| ltx-2-fast-text-to-video | $0.10 (10 credits) |
| kling-2.6-pro-text-to-video | $0.25 (25 credits) |
Image-to-Video:
| Model | Cost |
|---|---|
| ovi-image-to-video | $0.22 (22 credits) |
| ltx-2-fast-image-to-video | $0.26 (26 credits) |
| kling-2.5-turbo-pro-image-to-video | $0.39 (39 credits) |
| sora-2-image-to-video | $0.44 (44 credits) |
| wan-2.5-preview-image-to-video | $0.55 (55 credits) |
| kling-2.6-pro-image-to-video | $0.77 (77 credits) |
| wan-2.6-image-to-video | $0.83 (83 credits) |
| veo3-fast-image-to-video | $0.88 (88 credits) |
| vidu-q3-image-to-video | $0.97 (97 credits) |
| veo3-full-image-to-video | $1.76 (176 credits) |
| kling-o3-pro-image-to-video | $1.77 (177 credits) |
8. Models
GET /models
Returns all available models grouped by type. No authentication required.
Response
{
"text": [
{ "id": "llama-3.3-70b", "name": "Llama 3.3 70B", "type": "text", "tier": "standard", "offline": false },
{ "id": "deepseek-r1-671b", "name": "Deepseek R1 671B", "type": "text", "tier": "frontier", "offline": false }
],
"code": [
{ "id": "qwen-2.5-coder-32b", "name": "Qwen 2.5 Coder 32B", "type": "code", "tier": "standard", "offline": false }
],
"image": [
{ "id": "flux-2-pro", "name": "Flux 2 Pro", "type": "image", "tier": "premium", "offline": false }
],
"imageEdit": [
{ "id": "flux-2-max-edit", "name": "Flux 2 Max Edit", "type": "imageEdit", "tier": "frontier", "offline": false }
],
"video": [
{ "id": "ltx-2-fast-text-to-video", "name": "LTX 2 Fast", "type": "video", "tier": "standard", "offline": false }
],
"imageToVideo": [
{ "id": "sora-2-image-to-video", "name": "Sora 2", "type": "imageToVideo", "tier": "premium", "offline": false }
]
}
Model Tiers
| Tier | Description | Examples |
|---|---|---|
| standard | Fast, cost-effective | Llama 3.3 70B, Qwen 2.5 Coder |
| premium | Higher quality, moderate cost | Flux 2 Pro, Sora 2 |
| frontier | State-of-the-art, highest cost | GPT 5.2, Gemini 3 Pro, Claude Opus 4.7 |
Model pricing is dynamic (15–100% of base price). See Payment & Billing.
9. Payment & Billing
Payment rails
Carpe Diem settles every request in USDC on Base at the same dynamic price — but there are three ways to fund that spend. Pick by who's calling:
| Rail | Custody | Withdrawable | Auth | Best for |
|---|---|---|---|---|
| Prepaid (recommended) | dedicated smart account you own | Yes | cdm_ key / JWT | apps & humans — same effort as credits, but funds stay yours |
| Credits | usage entitlement (protocol-held) | No | cdm_ key / JWT | topping up a non-refundable balance you don't withdraw |
| Wallet (x402) | your wallet | n/a (nothing pre-funded) | none — a signed payment per request | autonomous AI agents |
Prepaid and Credits are both used through a cdm_ key — the rail is a property of the key, chosen at creation (see §2 Per-key payment rails). To a caller the two are identical — same endpoints, same headers — and take the same single deposit step; only custody differs. Prepaid (recommended) pulls the exact per-request cost from an ERC-4337 smart account whose owner is your wallet — self-custodial, and you can withdraw the remainder anytime. Credits draw down a pooled, non-refundable balance (your USDC becomes protocol funds; no withdrawal).
Wallet (x402) needs no key and no pre-funded balance: an agent holding USDC on Base signs each payment (or one reusable session permit) and Carpe settles the exact cost on-chain per request. Use the /v1/x402/… endpoints. See the API reference for the full x402 flow and the two schemes (carpe-session sign-once vs exact per-request).
The billing details below (credit unit, dynamic pricing, balance headers, snapshot endpoints) apply to the Credits and Prepaid rails; x402 agents read their cost from the X-Payment-Amount-Usdc response header instead.
Credit System
| 100 credits | = $1 = 1 USDC |
| Minimum balance | 1 credit ($0.01) |
| Non-refundable | Credits are a one-way purchase |
Dynamic Pricing
Your price is the higher of two signals, so you pay for your own demand — not just the time of day:
- Daily pool usage — how much of the day's shared DIEM pool has been consumed. Mostly free → near the floor; nearly depleted → near the ceiling. The curve stays low while the pool is healthy and ramps up steeply as consumption approaches ~70% of the pool.
- Your request's footprint — its size relative to the remaining capacity that can serve it. Small requests stay near the pool price; a large request (e.g. a video consuming a big share of remaining capacity) pays a premium for its footprint.
Bounds (both signals): floor ~15%, ceiling 100% of the Venice base price — the 5% protocol fee is inside the ceiling, so you never pay more than going to Venice directly.
Text/code model pricing is per-token (input and output priced separately). Image and video models have fixed per-request costs.
Insufficient Credits (HTTP 402)
When your balance is too low, you'll receive:
{
"error": "Insufficient credits",
"code": "PAYMENT_REQUIRED",
"available": "0.0500",
"minRequired": "0.01",
"credits_available": 5.0,
"credits_required": 1.0
}
available and minRequired are decimal USDC strings; credits_* fields are in credits (1 credit = $0.01). Agents should treat 402 as a hard stop — retrying with the same wallet won't help until a deposit settles.
Real-time balance for AI agents
Inference responses always carry the wallet's effective credit state, so agents don't need to poll a separate endpoint:
| Header | Description |
|---|---|
X-Carpe-Available-Credits | The credit balance the user can actually spend right now. Equals escrow − pending − holds. This is what UIs and agents should display and track — it's the number that determines whether the next call will be authorized. |
X-Carpe-Pending-Credits | Debt accumulated since the last on-chain settlement. Already deducted from available, surfaced for transparency / audit. |
X-Carpe-Balance-Credits | Cumulative on-chain escrow balance (V8.X audit trail of historical deposits — never decremented). Useful for accounting reconciliations, not for "remaining credits" UIs. |
X-Carpe-Cost-Usdc-Micro | Cost of this call in micro-USDC (1e-6 USDC). Non-streaming only. |
X-Carpe-Input-Tokens / X-Carpe-Output-Tokens | Token counts used to compute the cost. Non-streaming only. |
X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset | Per-wallet request rate limit, OpenAI-compatible. |
For streaming chat completions, the cost is delivered as extensions to the standard OpenAI usage object in a final chat.completion.chunk, written after the upstream's last event and immediately before the stream's single data: [DONE], which is always the last line:
data: {"id":"chatcmpl-carpe-…","object":"chat.completion.chunk","created":1780845678,"model":"claude-opus-4-7","choices":[],"usage":{"prompt_tokens":234,"completion_tokens":187,"total_tokens":421,"carpe_cost_usdc_micro":12345,"carpe_credits":1.2345}}
data: [DONE]
A client that stops reading at [DONE] (the OpenAI SDKs do) therefore still receives the cost. The upstream may also emit its own usage chunk earlier in the stream (token counts only); the last usage you see is Carpe's and is the authoritative one. The same framing applies to /router/chat/completions when the router streams.
Strict OpenAI parsers (e.g. opencode, AI SDK typed validators) ignore unknown fields inside usage by convention — the chunk is shape-valid, so the parser never throws. Tolerant clients can read usage.carpe_cost_usdc_micro to update a credit display in real time.
Prior to operator v145 (2026-06-08) the cost was emitted in a non-canonical
{ "carpe_billing": { … } }chunk after[DONE]. That shape broke strict parsers. v145 retires it: readusage.carpe_cost_usdc_microgoing forward. Older operators also wrote the usage chunk after the upstream's[DONE]with no terminator behind it; clients that read to end of stream keep working either way.
Stream errors
A stream that dies after the 200 has been committed cannot become an HTTP error code — the status is already on the wire. The operator instead emits one final chunk in OpenAI's streaming error envelope, which strict parsers accept as part of the chunk union:
data: {"error":{"message":"Provider dropped the stream. Please retry.","type":"upstream_error","code":"PROVIDER_STREAM_INTERRUPTED"}}
code | Meaning | Retry? |
|---|---|---|
PROVIDER_STREAM_INTERRUPTED | The upstream connection dropped mid-answer. | Yes — a different provider key is usually picked. |
PROVIDER_STREAM_IDLE | The upstream stopped sending data for STREAM_IDLE_TIMEOUT_MS (90s) and the socket was reclaimed. | Yes. |
STREAM_MAX_DURATION | The answer ran past the operator's absolute ceiling (STREAM_MAX_DURATION_MS, 1h). | Not as-is — split the request into smaller steps. |
A stream that ends this way carries no finish_reason and no usage, and is not billed. On /v1/messages the same conditions produce Anthropic's event: error envelope instead.
Prior to 2026-09-01 this was a non-canonical
{ "carpe_error": { … } }chunk. It satisfied neither branch of the OpenAI chunk union, so strict clients surfaced a schema-validation dump instead of the message — the same defectcarpe_billinghad, in the object next to it. Streaming answers were also capped at the model's wall-clock timeout regardless of whether they were still making progress; they are now bounded by silence, not by duration.
Snapshot endpoints
When an agent needs to check its budget without making an inference call:
GET /v1/credits (or alias GET /v1/billing/balance) — requires authentication.
{
"escrowUsdc": 12.47,
"pendingUsdc": 0.35,
"holdsUsdc": 0,
"availableUsdc": 12.12,
"escrowCredits": 1247,
"pendingCredits": 35,
"holdsCredits": 0,
"availableCredits": 1212,
"updatedAt": "2026-05-10T15:42:08.123Z"
}
GET /v1/rate_limits — requires authentication. Returns the configured limit, current window, and remaining requests for the caller.
{
"max": 100,
"windowMs": 60000,
"keying": "per-wallet (or per-IP for unauthenticated)",
"remaining": 87,
"resetSeconds": 42
}
Buy Credits
- Dashboard: Visit the buyer dashboard → Buy Credits
- Direct deposit: Send USDC to the escrow contract on Base
- Any token: Use the cross-chain deposit widget (powered by LI.FI) — deposit ETH, USDT, DAI, WETH, cbETH from any chain
10. Usage Tracking
Usage History (Paginated)
GET /buyer/usage?limit=20&offset=0
Requires authentication.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Results per page (1–100). |
offset | integer | 0 | Pagination offset. |
Response:
{
"events": [
{
"id": 42,
"model": "llama-3.3-70b",
"provider": "0xAbc...",
"prompt_tokens": 150,
"completion_tokens": 300,
"cost_usdc": 0.000045,
"multiplier": 0.35,
"created_at": "2026-02-18 14:30:00"
}
],
"total": 1542
}
Usage Summary (Aggregated)
GET /buyer/usage/summary?days=7
Requires authentication.
| Parameter | Type | Default | Description |
|---|---|---|---|
days | integer | 7 | Lookback period (1–90). |
Response:
{
"byModel": [
{ "model": "llama-3.3-70b", "totalRequests": 150, "totalTokens": 45000, "totalCost": 0.0135 }
],
"byDay": [
{ "date": "2026-02-18", "totalCost": 0.005, "totalRequests": 42 }
],
"totals": {
"requests": 150,
"tokens": 45000,
"cost": 0.0135
}
}
Note:
costvalues are in USDC. Multiply by 100 to convert to credits.
11. Error Handling
Error Response Format
All errors follow a consistent format:
{
"error": "Human-readable error message",
"code": "ERROR_CODE"
}
Error Codes
| HTTP | Code | Description | Retry? |
|---|---|---|---|
| 400 | BAD_REQUEST | Invalid request parameters | No |
| 400 | INVALID_MODEL | Model name invalid or malformed | No |
| 400 | MODEL_ERROR | Model not available on provider | No |
| 400 | VENICE_BAD_REQUEST | Provider rejected the request | No |
| 400 | NOT_JWT | Tried to refresh an API key (use JWT) | No |
| 400 | REPLAY_REJECTED | Provision message expired (>5 min) | No |
| 401 | AUTH_REQUIRED | Missing or invalid auth token | No |
| 401 | AUTH_FAILED | Wallet signature verification failed | No |
| 401 | TOKEN_EXPIRED | JWT expired, re-authenticate | No |
| 402 | PAYMENT_REQUIRED | Insufficient credits | No |
| 403 | JWT_REQUIRED | API key management requires JWT session | No |
| 403 | ADMIN_REQUIRED | Admin-only endpoint | No |
| 403 | INVALID_SIGNATURE | Provision signature invalid | No |
| 409 | DUPLICATE_KEY | Venice API key already provisioned by another provider | No |
| 413 | PAYLOAD_TOO_LARGE | Image exceeds 5 MB | No |
| 429 | RATE_LIMITED | Too many requests (per-wallet limit, see /v1/rate_limits) | Yes — wait, honour Retry-After |
| 429 | UPSTREAM_RATE_LIMIT | All providers rate-limited | Yes — backoff |
| 429 | FAUCET_COOLDOWN | Testnet faucet: 1 claim per 24h | Yes — wait |
| 502 | VENICE_ERROR | Provider returned an error | Yes — backoff |
| 502 | QUOTE_FAILED | Deposit swap quote failed | Yes — backoff |
| 503 | TEE_NOT_READY | No provider keys provisioned yet | Yes — later |
| 503 | NO_PROVIDERS | No providers available | Yes — later |
| 503 | NO_PROVIDER_CAPACITY | All providers at capacity | Yes — backoff |
| 503 | UPSTREAM_AUTH_FAILED | All provider keys invalid | No — re-provision |
| 503 | BALANCE_CHECK_FAILED | On-chain balance check failed | Yes — backoff |
| 503 | SETTLEMENT_DISABLED | Settlement not configured | No |
| 503 | FAUCET_DISABLED | Testnet faucet not configured | No |
Retry Strategy
400, 401, 402, 403, 409, 413 → Do NOT retry
429 → Retry after delay (respect Retry-After header if present)
502, 503 → Retry with exponential backoff
Python Retry Wrapper
import time
from openai import OpenAI, APIError, RateLimitError
client = OpenAI(
base_url="https://carpe-diem.xyz/api/operator/v1",
api_key="cdm_your_api_key_here",
)
def chat_with_retry(messages, model="llama-3.3-70b", max_retries=3):
for attempt in range(max_retries):
try:
return client.chat.completions.create(
model=model,
messages=messages,
)
except RateLimitError:
wait = 2 ** attempt
print(f"Rate limited, waiting {wait}s...")
time.sleep(wait)
except APIError as e:
if e.status_code in (502, 503):
wait = 2 ** attempt
print(f"Server error {e.status_code}, retrying in {wait}s...")
time.sleep(wait)
else:
raise
raise Exception("Max retries exceeded")
JavaScript Retry Wrapper
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://carpe-diem.xyz/api/operator/v1",
apiKey: "cdm_your_api_key_here",
});
async function chatWithRetry(messages, model = "llama-3.3-70b", maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.chat.completions.create({ model, messages });
} catch (error) {
if (error.status === 429 || error.status === 502 || error.status === 503) {
const wait = 2 ** attempt * 1000;
console.log(`Error ${error.status}, retrying in ${wait}ms...`);
await new Promise((r) => setTimeout(r, wait));
} else {
throw error;
}
}
}
throw new Error("Max retries exceeded");
}
12. Rate Limits
| Limit | Value |
|---|---|
| Requests per minute | 30 per wallet |
| Time window | 1 minute (rolling) |
When rate-limited, you'll receive:
{
"error": "Too many requests",
"code": "RATE_LIMITED"
}
Rate limits are per wallet address, regardless of how many API keys you have.
13. Health & Status
Health Check
GET /health
No authentication required.
{
"status": "healthy",
"components": {
"database": { "status": "healthy" },
"settlement": { "status": "healthy", "detail": "enabled" },
"providers": { "status": "healthy", "detail": "3 provider(s)" },
"settlementFreshness": { "status": "healthy", "detail": "last success 2 min ago" }
}
}
| Overall status | HTTP code | Meaning |
|---|---|---|
healthy | 200 | All systems operational |
degraded | 200 | Some components impaired but functional |
unhealthy | 503 | Critical component down |
Prometheus Metrics
GET /metrics
Returns metrics in Prometheus text format. No authentication required.
Includes: settlement counters, provider scores, pending debts, pricing model count.
14. Provider Integration
Providers supply Venice API keys to the network and earn 65% of revenue in DIEM tokens.
Register as a Provider
POST /tee/provision
{
"apiKey": "your_venice_api_key",
"message": "Provision Carpe Diem\nWallet: 0xYourProviderWallet\nTimestamp: 1707654321000",
"signature": "0x..."
}
The message must include a timestamp within the last 5 minutes (replay protection).
Response:
{
"status": "Provisioned",
"secure": true,
"provider": "0xYourProviderWallet",
"totalProviders": 3
}
List Providers
GET /tee/providers
No authentication required.
{
"count": 3,
"providers": [
{
"address": "0xAbc...",
"maskedKey": "vnc_a1b2****",
"metrics": {
"successRate": 0.95,
"avgLatency": 1200,
"headroom": 0.8,
"score": 0.85,
"requestCount": 1542
}
}
]
}
Provider Selection
Carpe Diem automatically routes requests across providers using a composite score:
- Success rate (50% weight, time-decayed)
- Latency (30% weight)
- Headroom (20% weight)
- Score floor of 20% ensures penalized providers can recover
- Tiebreaker: least recently selected provider wins
Revenue
Providers earn 65% of each request's cost, settled on-chain in DIEM tokens. The remaining 35% is retained as protocol revenue (treasury + operating costs + future external audit funding).
Admin: Revoke a Provider
DELETE /tee/providers/:address
Requires admin authentication.
15. Endpoint Reference
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /health | No | System health check |
GET | /metrics | No | Prometheus metrics |
GET | /models | No | List all available models |
POST | /auth/session | No | Create JWT session (wallet signature) |
POST | /auth/refresh | JWT or refresh token | Refresh JWT session (email sessions: rotating refresh token, 30 days) |
POST | /auth/email/start | No | Email sign-in: send a 6-digit code + fallback link |
POST | /auth/email/verify-code | No | Email sign-in with the code → JWT + refresh token |
POST | /auth/email/verify | No | Email sign-in with the link → JWT + refresh token |
POST | /auth/logout | Refresh token | End one email session |
POST | /auth/logout-all | JWT | End every email session of the account |
POST | /auth/step-up/start | JWT (email) | Send a re-verification code |
POST | /auth/step-up/verify | JWT (email) | Re-verify → 10-min token for API key create/revoke |
POST | /auth/api-keys | JWT | Create API key (email session: re-verified token) |
GET | /auth/api-keys | JWT | List API keys |
DELETE | /auth/api-keys/:id | JWT | Revoke API key (email session: re-verified token) |
POST | /v1/chat/completions | Yes | Chat completions — OpenAI-compatible (streaming/non-streaming) |
POST | /v1/messages | Yes | Messages API — Anthropic-compatible (Claude Code, Cursor, Cline) |
POST | /v1/image/generate | Yes | Generate image |
POST | /v1/image/edit | Yes | Edit image |
POST | /v1/video/quote | Yes | Get video generation quote |
POST | /v1/video/queue | Yes | Queue video generation |
POST | /v1/video/retrieve | Yes | Retrieve video result |
GET | /v1/credits | Yes | Balance snapshot (credits or prepaid) |
POST | /v1/prepaid/register | Yes | Register your dedicated prepaid smart account |
GET | /v1/prepaid/status | Yes | Prepaid account balance + remaining allowance |
GET | /v1/credits/migrate/quote | Yes | Migratable credit balance + EIP-712 domain |
POST | /v1/credits/migrate | Yes | Migrate credits → your prepaid account (prepaid-only) |
POST | /v1/x402/… | None (signed) | Pay-per-request from a wallet (agents) — see API reference |
GET | /router/markets | None | Best-price router: live market snapshot (rolling out) |
POST | /router/quote | None | Quote a request across Carpe + external markets (read-only) |
POST | /router/chat/completions | Yes | Chat via the best-price router + X-Carpe-Route-* headers — see API reference |
GET | /buyer/usage | Yes | Usage history (paginated) |
GET | /buyer/usage/summary | Yes | Usage summary (aggregated) |
GET | /deposits/quote | No | Get deposit swap quote |
POST | /tee/provision | Signature | Register as provider |
GET | /tee/providers | No | List providers with scores |
GET | /tee/status | No | TEE encryption status |
DELETE | /tee/providers/:address | Admin | Revoke provider |
GET | /provider/usage/summary | No | Provider usage summary |
GET | /provider/stats | No | Provider stats |
GET | /providers/:wallet/yield | Wallet/Admin | APR on the DIEM you sold over 7 days, every revenue counted (Carpe, Carpe Reach, rebate) |
GET | /providers/:wallet/rewards/daily | Wallet/Admin | Per-day consumed vs DIEM reward + APR |
GET | /billing/pending | Admin | View pending debts |
POST | /billing/settle-now | Admin | Trigger immediate settlement |
POST | /testnet/faucet | No | Claim testnet tokens (Base Sepolia) |