# Carpe Diem — Documentation ## What Carpe Diem is {#introduction} Carpe Diem is a marketplace for AI inference on Base. You call language, image, audio, video and embedding models — Venice's catalogue, including Claude, GPT, DeepSeek, Kimi, Qwen and GLM models — through **one OpenAI- and Anthropic-compatible API**, and you pay only for what you use, usually below Venice's own list price. Requests are served two ways: - **Inside Carpe's TEE** (`/v1`): sealed hardware that neither the host nor the Carpe Diem team can read, in front of [Venice.ai](https://venice.ai), the privacy-first AI provider. - **At the best price** (`/router`): each request is priced across Carpe and several other markets, and the cheapest serves it — with a switch to stay on TEE markets or on markets that keep nothing. There are three ways in: | | For | Start here | |---|---|---| | **The API** | Your code, your coding agent (Claude Code, OpenCode, Aider…), your chat app | [Quick start](#quick-start), then [Integrations](/docs/integrations#sdks) | | **The MCP server** | Your AI assistant (Claude Desktop, Claude Code, Cursor, Codex, VS Code…), which gets image, video, voice, music, web search and every model as tools | [Integrations → MCP server](/docs/integrations#mcp) | | **The web app** | Trying models, generating images and video, without code | [Create](/create) | And DIEM holders can **earn** by selling the Venice inference allowance they do not use: see [Becoming a provider](#providers). ### Our place in the Venice ecosystem {#venice} Carpe Diem is not a competitor to Venice — it extends it. Staking DIEM on Venice grants a daily inference allowance of roughly $1 per DIEM; most holders never use all of it, and whatever is unspent at the daily reset is lost. Carpe Diem turns that idle allowance into a marketplace: a DIEM holder provisions a key and earns on demand they would otherwise have wasted, a developer gets Venice models at a lower, market-driven price, and more of the network's capacity is actually used. ## Quick start {#quick-start} From zero to a first answer in a few minutes. Everything happens in the **API** space of the site. ### 1. Sign in {#sign-in} Open [API → Start](/build?tab=start) and sign in with an EVM wallet (MetaMask, Rabby, WalletConnect…) — a free signature, no gas — or with your email address. No KYC either way. ### 2. Add funds {#add-funds} In [API → Balance](/build?tab=balance), with a wallet on Base: - **Credits** — deposit USDC (or another token, swapped to USDC in the same transaction) for a balance you burn down per request. 1 credit = $0.01. - **Prepaid account** — deposit USDC onto an account created for you and that you own; Carpe pulls only the exact cost of each request, and you can withdraw the rest anytime. Card payment, for accounts without a wallet, is being opened. The differences between the rails are in [Ways to pay](#ways-to-pay). ### 3. Create an API key {#create-key} In [API → Keys](/build?tab=keys), create a key. It looks like `cdm_…` and is shown **only once**: copy it somewhere safe. Create one key per app or agent, so each one's spending is visible on its own, and revoke any of them at any time. ### 4. Your first call {#first-call} ```bash tab="curl" group="lang" curl https://carpe-diem.xyz/api/operator/v1/chat/completions \ -H "Authorization: Bearer $CARPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "deepseek-v4-flash", "messages": [{"role": "user", "content": "Hello in one sentence."}]}' ``` ```python tab="Python" group="lang" from openai import OpenAI client = OpenAI(base_url="https://carpe-diem.xyz/api/operator/v1", api_key="cdm_your_key") r = client.chat.completions.create( model="deepseek-v4-flash", messages=[{"role": "user", "content": "Hello in one sentence."}], ) print(r.choices[0].message.content) ``` ```js tab="JavaScript" group="lang" import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://carpe-diem.xyz/api/operator/v1", apiKey: process.env.CARPE_API_KEY }); const r = await client.chat.completions.create({ model: "deepseek-v4-flash", messages: [{ role: "user", content: "Hello in one sentence." }], }); console.log(r.choices[0].message.content); ``` Any model id from [`/v1/models`](https://carpe-diem.xyz/api/operator/v1/models) works in `model`; the [Models](/models) page shows them with live prices. ### 5. Next {#next} - Plug Carpe into your tools — Claude Code, OpenCode, Cursor, the OpenAI or Anthropic SDK, LangChain, Open WebUI…: [Integrations](/docs/integrations). - Give your AI assistant image, video, voice and web tools: [MCP server](/docs/integrations#mcp). - Complete recipes with code and costs — images, video, voice, RAG, web research, extraction: [Use cases](/docs/use-cases). - Every endpoint: [API reference](/docs/api). ## How it works {#how-it-works} ### The big picture {#big-picture} A request makes a round trip through the **operator**, a private proxy running in a TEE in front of the model providers: ```carpe-flow ``` On `/v1`, the operator forwards your request to Venice with a provider's key, relays the answer and debits your balance. On `/router`, it first prices the request on every market — Carpe itself, and external ones — and sends it to the cheapest that can serve it (see [Two surfaces](#surfaces)). Either way you only ever hold your `cdm_` key. ### The building blocks {#building-blocks} **The escrow — payments and balances, on-chain.** The escrow is a single smart contract on Base. When you buy credits, your USDC is paid into it and an on-chain entry records your **credit balance** — the amount of inference you're entitled to spend. Credits are a usage entitlement, not a deposit: there is no withdrawal, and the incoming USDC becomes protocol funds that pay providers and run the network. (The other rails — the prepaid account and wallet payments (x402), see [Ways to pay](#ways-to-pay) — work differently: there the funds stay in an account *you* own and can withdraw from; the escrow only pulls the exact cost per request.) Because the contract is public, anyone can verify total balances and on-chain settlement at any time. **The operator — the private brain.** The operator authenticates your API key, picks an available provider, forwards your request to Venice, and relays the response back. It runs inside a TEE (Trusted Execution Environment) — sealed hardware where even the host machine and the Carpe Diem team cannot read what's inside. This is what keeps your prompts and the providers' Venice keys confidential. **Venice — the inference.** Venice.ai performs the actual model execution. The operator holds providers' Venice keys and uses them to serve your request; you never see or handle a Venice key yourself. #### On-chain balance vs. live credit balance {#live-balance} Your credit balance is tracked in two places, and it's worth knowing why: | | On-chain (escrow ledger) | Operator (live ledger) | |---|---|---| | Answers | "what's recorded on the blockchain" | "what I can spend right now" | | Updates | on purchase + at periodic settlement | on every request | | Lives | on Base (public, verifiable) | inside the TEE | When you make a request, your **live** balance is debited instantly inside the operator. The **on-chain** entry only changes when you buy more credits, or when the operator periodically settles a batch of usage on-chain. Between settlements the two can differ: the on-chain figure reflects purchases, while the live figure reflects purchases minus everything you've already spent. Think of it like a prepaid card: buying credits loads the card (recorded on-chain); each request is a tap that draws down the live balance instantly. The operator reconciles its books with the chain periodically — that's settlement. ### The life of a request {#request-life} From the moment you call the API to the moment providers get paid: 1. **You send a request** with your API key (`cdm_…`) to a standard endpoint like `/v1/chat/completions` (or `/router/chat/completions`, which first compares markets — see [Two surfaces](#surfaces)). 2. **The operator authenticates** the key and resolves it to your wallet. 3. **It runs a pre-flight check** — do you have enough live credits, and does a provider have enough Venice capacity to serve this? If not, the request is refused cleanly, before any inference happens. 4. **It picks a provider** based on real-time health, available capacity, and the current dynamic price. 5. **It forwards the request to Venice**, which runs the model and returns the response to the operator. 6. **It prices and debits** — the operator computes the cost (Venice's raw cost × the current price multiplier), debits your live credit balance instantly, and records the usage. 7. **It relays the response back to you**, including headers that report your remaining credits. Up to here nothing has touched the blockchain — it's all instant, inside the TEE. Paying providers happens separately: 8. **Settlement.** As usage accumulates, the operator periodically settles a batch on-chain in a single transaction: it converts the owed USDC into DIEM, distributes it to the providers who served the requests, and takes the protocol fee. This is the only on-chain write in the normal flow — batching it keeps the gas cost per request negligible. ## Two surfaces: `/v1` and `/router` {#surfaces} Same key, same request bodies, two base URLs: | | `https://carpe-diem.xyz/api/operator/v1` | `https://carpe-diem.xyz/api/operator/router` | |---|---|---| | Served by | Carpe alone, through its TEE, with Venice | The cheapest market for each request: Carpe, Surplus, AntSeed, OpenRouter, Chutes, Venice or Engy | | Confidentiality | Always through the TEE | May leave it — send `"privacy": true` to stay on TEE markets, `"privacy": "private"` to stay on markets that keep nothing | | Price | Carpe's [dynamic price](#pricing) | The winning market's price, plus a routing fee on external markets | | Tells you | `X-Carpe-Cost-Usdc-Micro` | `X-Carpe-Route-Market`, `X-Carpe-Route-Cost-Usdc`, `X-Carpe-Route-Saved-Usdc` | Quote a request on every market before sending it, for free: `POST /router/quote`. The [router section of the API reference](/docs/api#best-price-router-aggregator) has the path mapping, the market list and how to pin one. ## Ways to pay {#ways-to-pay} Pay-as-you-go: no subscription, no minimum. All rails pay the same price per request; they differ in how the money is held. | | Custody | Withdrawable | Needs a `cdm_` key | Best for | |---|---|---|---|---| | **Prepaid account** | An account you own (ERC-4337, controlled by your wallet) | Yes | Yes | Wallet users who want their funds to stay theirs | | **Credits** | A usage balance held by the protocol | No | Yes | Topping up and burning down; the only rail for email accounts | | **Wallet (x402)** | Your wallet, nothing pre-funded | — | No | Autonomous agents without an account | **Prepaid account.** Deposit USDC from [API → Balance](/build?tab=balance): the account is created and authorised in the same gas-free step. Carpe only pulls the exact cost of each request, and you can withdraw the rest to your wallet, or revoke, at any time. **Credits.** Deposit USDC (or swap another token in the same transaction) for a non-refundable usage balance: the USDC becomes protocol funds that pay providers. 1 credit = $0.01. Credits are never converted or withdrawn. **Wallet (x402).** An agent with a private key and USDC on Base pays each request from its wallet, with no signup and no key: it receives a `402` challenge, signs once (a permit capped at its budget) and resends. See [Pay per request](/docs/integrations#x402). **Which rail pays.** Your account has a payment mode — credits, prepaid, or auto (credits first, then the prepaid account) — set in [API → Balance](/build?tab=balance); a key can be given a mode of its own when you create it. If the rail cannot cover a request, the answer is `402 PAYMENT_REQUIRED`, before anything is run. Email accounts pay with credits. **Every response reports what it cost** and what is left (`X-Carpe-Available-Credits`), so an app or an agent can budget itself; `GET /v1/credits` returns the balance on demand. ## Dynamic pricing {#pricing} Carpe Diem's price floats with how much of the day's shared inference capacity is still available — the more of the daily DIEM pool is left, the less you pay. The key guarantee: **your total price, protocol fee included, never exceeds Venice's direct rate.** - **A floor and a hard ceiling.** Your all-in price ranges from a floor of about **15% of Venice's rate** up to a maximum of **100% of Venice's rate** — never more. - **The 5% fee is inside that ceiling, not on top of it.** The demand-based part is capped so that, even at peak, demand + fee land exactly at Venice's rate. The fee never pushes you above it. - **Driven by daily pool usage.** Each day, providers' staked DIEM refreshes a shared pool. When little of it has been used, prices sit near the floor; as it's consumed, they rise toward the ceiling. - **Your request's size matters too.** The price is the higher of two things: how depleted the daily pool is, and how big *your* request is relative to the capacity that can serve it. A small request (a chat turn, an image) stays near the pool price; a large one (a video that eats a big share of remaining capacity) pays a premium for its footprint — you pay for your own demand, not just the time of day. - **Resets daily** at 00:00 UTC. > **Example.** Say a request costs **$0.10** at Venice's direct rate. > - Early in the day, pool mostly free → you pay about **$0.015** (~15%). > - Late in the day, pool nearly used up → the same request rises toward **$0.10** — Venice's rate, fee included, and never above. > > Same request, same model — the price just reflects how much shared capacity is left. See the live state on the [Models](/models) page, or with `GET /v1/capacity` and [`/pricing`](https://carpe-diem.xyz/api/operator/pricing). The best-price router may find an even lower price on another market — see [Two surfaces](#surfaces). ## Authentication {#authentication} **API keys (`cdm_…`) — for calling the API.** Send `Authorization: Bearer cdm_…` on every request. Keys do not expire; you can hold several and revoke any of them instantly from [API → Keys](/build?tab=keys) — or let a leaked key revoke itself with `POST /v1/keys/self/revoke`. The Anthropic endpoints (`/v1/messages`) also accept the key as `x-api-key`. **Sessions — for managing your account.** Signing in on the site, with a wallet signature or an email code, opens a session (a token valid 30 minutes, renewed while you are active, for at most 8 hours). Creating or revoking keys and changing payment settings need a session: **an API key cannot manage keys**, so a leaked key cannot mint more. **x402 — no account at all.** An agent's signed USDC permit is both its payment and its identity. See [Ways to pay](#ways-to-pay). ## Models {#models} Carpe serves Venice's whole catalogue, synced automatically — about 400 models today: | Type (`carpe_diem_type`) | What it does | Endpoint | |---|---|---| | `text` | Chat, reasoning, code, vision | `/v1/chat/completions`, `/v1/messages` | | `embedding` | Vector embeddings | `/v1/embeddings` | | `image`, `imageEdit`, `upscale` | Generate, edit, upscale | `/v1/image/generate`, `/v1/image/edit`, `/v1/image/upscale` | | `video`, `imageToVideo`, `referenceToVideo` | Text-, image- or reference-to-video (async) | `/v1/video/queue` | | `tts`, `asr`, `music` | Speech, transcription, music and sound (async) | `/v1/audio/speech`, `/v1/audio/transcriptions`, `/v1/audio/music/queue` | `GET /v1/models` is the OpenAI-compatible list (it works with `client.models.list()`); each entry also carries: - **`privacy`** — `private` (Venice runs the model without keeping prompts or answers) or `anonymized` (served through a third party, such as Claude and GPT models, without your identity). - **`capabilities`** — vision, tool calling, reasoning, JSON schema, web search, TEE attestation… - **`constraints`** — the parameters the model accepts: durations and sizes for video, voices for speech, lengths for music. Read them before building a form. - **`tier`** — standard, premium or frontier: the base rate before the [dynamic price](#pricing). `GET /models` returns the same catalogue grouped by type, and `GET /router/models` adds, for each text model, the markets that can serve it. ## Privacy & security {#privacy} **No KYC.** A wallet or an email address is all an account is. With x402, not even that: the wallet is the identity. **Sealed execution.** The operator runs in a Trusted Execution Environment (Intel TDX on Phala Cloud): the host and the Carpe Diem team cannot read into it. Your prompts and answers, the providers' Venice keys and the live ledger stay inside. On `/v1` every request goes through it; on `/router`, a request may be served by an external market, unless you send `"privacy": true` (TEE markets only) or `"privacy": "private"` (markets that keep nothing). **Private models.** Beyond the TEE, pick a model listed `privacy: "private"`: Venice serves it without retaining anything. **Verifiable, not "trust us".** The TEE produces a hardware attestation of the exact code it runs: `GET /attestation`, or the [Privacy](/privacy) page. **Transparent money.** Deposits, settlements, fees and total balances are on Base, public and auditable. | Private (inside the TEE) | Public (on Base) | |---|---| | Your prompts & responses | Deposits & settlements | | Providers' Venice keys | Total balances & fees | | Live credit ledger | Contract code & roles | ## Limits & errors {#limits} - **Rate limits.** Per account: 600 requests a minute today, growing with your [parallel lanes](/docs/api#parallel-lanes-several-agents-on-one-key) (one per $10 of credit), plus a separate budget for free reads such as your balance. Read yours with `GET /v1/rate_limits`. Each model also has Venice's own limits: `GET /v1/limits/:model` and `GET /v1/capacity`. - **Errors** are JSON with a stable `code`; the chat endpoints use OpenAI's error shape, `/v1/messages` Anthropic's. The ones to handle: `402 PAYMENT_REQUIRED` (top up), `429` (wait `Retry-After`), `503` (no capacity right now — retry later), `400 INVALID_TOOLS` (a tool definition is malformed). The full table, with what to retry: [Errors & retries](/docs/api#11-errors-retries). - **Regions.** Sanctioned countries are refused (`451`). Buying inference from the United States is allowed; providing and staking are not. - **What is not there.** Carpe serves Chat Completions, not OpenAI's Responses API (`/v1/responses`), and has no `/v1/messages/count_tokens`. Tools that need either are listed in [Integrations](/docs/integrations#not-providers). ## Becoming a provider {#providers} ### The idea {#provider-idea} There are two ways to become a provider, and they differ only in where the Venice key comes from. **Bring your own key.** If you stake DIEM on Venice, you have a daily inference allowance you probably don't fully use. You lend that allowance to the marketplace: Carpe Diem routes paid requests to your Venice key, and you earn DIEM on every request you serve — turning idle capacity into revenue. See [Provisioning your Venice key](#provider-key). **Or stake DIEM here and let us mint one.** If you hold DIEM but have no Venice account, the operator can mint a key for you inside its enclave, where it stays sealed — you never see it, and neither do we. See [Staking DIEM instead](#provider-stake). Either way the money works the same: You earn **65% of the value of every request your key serves, after a protocol surcharge** (5% of the Venice base cost, skimmed first; the protocol keeps the surcharge plus 35% of the rest). Measured against what the buyer actually paid, your share works out to roughly **43–62%** depending on the dynamic price — the Revenue tab's *Where every dollar goes* panel shows your window's real figures. Earnings accrue in DIEM, yours to withdraw on-chain anytime. ### Provisioning your Venice key {#provider-key} 1. Create a Venice **inference-only** API key in your Venice account, ideally with a **consumption limit** and an **expiry**. An inference key is all Carpe Diem needs — there's no reason to provision an admin key. 2. Open the [Earn page](https://carpe-diem.xyz/earn?door=key) and connect the wallet that stakes your DIEM. 3. Sign a message (free, no gas) to prove you own the wallet, and submit your Venice key. 4. Choose persistence: **persisted** (encrypted at rest, survives reboots) or **RAM-only** (re-provision after each restart — maximum secrecy). Your Venice key is encrypted inside the TEE the moment it arrives — never visible to the host or the Carpe Diem team. You can register up to **5 keys per wallet** to multiply your capacity. > **Key hygiene.** The TEE protects your key in use — it's never written to disk, logged, or exposed, and cannot be extracted from the enclave (even an admin key could not be read out). Scoping the key to **inference-only, with a limit and expiry** is simply defense-in-depth that stays entirely under your control, and you can revoke it anytime — from Venice, or from the provider page, which removes it from the TEE immediately. ### Staking DIEM instead, and letting us mint the key {#provider-stake} If you have no Venice account, the [stake door of the Earn page](https://carpe-diem.xyz/earn?door=stake) does the whole setup from DIEM alone. You sign every transaction yourself; no key of yours is ever shared, because there is no key of yours — the operator mints one inside its enclave and seals it there. 1. **Credential.** Venice will only mint a key for a wallet holding some staked VVV. If you have none, the page prices a swap of a few cents' worth from your ETH, shows you the figures, and sends nothing until you confirm. 2. **Stake DIEM.** This is the fuel: staked DIEM is the inference capacity your key serves with. It stays staked in your own name and you can unstake it yourself, subject to Venice's cooldown of about a day. 3. **Mint.** You sign a challenge from Venice; the operator relays it and seals the resulting key in its TEE. 4. **Choose your markets.** Carpe's own buy-side is always on. **Carpe Reach** is opt-in — your key also serves more buyers, through our partner Surplus Intelligence, which holds a dedicated Venice key while you sell: tick it when you mint and accept its terms, shown right under the box, and selling starts as soon as your key is ready — one more free signature. Your reward splitter is created at mint either way, so you can also turn selling on later from the Markets tab. **How you get paid differs by market, and this is the part worth reading twice.** Carpe's own traffic pays in DIEM **directly to your wallet's escrow balance, with no fee** — the protocol already takes its margin on the request itself, so taking another cut here would be charging you twice. Carpe Reach pays USDC through a **RewardSplitter** deployed in your name: 90% you, 10% the treasury, immutable. That splitter is the only rail that carries a fee. It is deployed when your first key is minted, whether you tick Carpe Reach or not — it has to be recorded on the key then, and a key minted without one could never join Carpe Reach later. Topping up an existing key never deploys one. Everything is **pull**. Both balances accrue on-chain until you claim them; nothing is ever pushed into your wallet unasked. A key minted this way is owned by the operator on your behalf, so it does not appear in the *Keys* table of the provider dashboard — that table lists keys you provisioned yourself (the tab counts your staked keys on one line). Your capacity lives on the Stake tab, its markets on the Markets tab, and both claims — the escrow's DIEM and the splitter's USDC and DIEM — on the Revenue tab. ### Earning & withdrawing {#provider-earnings} As your key serves requests, your DIEM earnings accrue on-chain, credited at each settlement. Withdraw them anytime with the escrow's `withdrawDiem` — they go straight to your wallet. Track pending and lifetime earnings via `GET /provider/stats` and the [Earn console](https://carpe-diem.xyz/earn?tab=overview), where the claim is. ### Monitoring & control {#provider-monitoring} - **Per-request log** — `GET /provider/usage` returns every request your key served, newest first, each with a timestamp, model, and cost. It's the definitive record of exactly what Carpe Diem routed through your key and when — including very recent requests not yet settled on-chain. - **Stats & yield** — `GET /provider/stats` for request counts and earnings; `GET /providers/:wallet/yield` for your **APR on DIEM sold** (`aprOnSold7d`): every revenue — Carpe sales, Carpe Reach sales and the router's idle-capacity rebate — divided by the DIEM you sold on Carpe and on Carpe Reach over the last 7 days, annualised. `sold7d` says how many DIEM that was; Carpe Reach is measured through Carpe Diem's seller account with the partner. In the API, Carpe Reach is the `surplus` market and its fields. - **Revoke anytime** — remove one key or your whole provider registration with `DELETE /tee/providers/:address` (or per key with `…/keys/:keyId`). The key stops receiving traffic immediately. ## FAQ {#faq} **Do I need an account?** To use an API key, yes — a wallet or an email address, no KYC. An agent paying with x402 needs no account at all. **How do I pay?** In USDC on Base: credits, or a prepaid account you own; agents can pay per request from a wallet. Card payment is being opened. See [Ways to pay](#ways-to-pay). **Is it cheaper than going direct?** On `/v1`, the [dynamic price](#pricing) starts around 15% of Venice's rate and never exceeds it, fee included. On `/router`, you get the cheapest market for each request. **Which models?** Venice's whole catalogue — text (including Claude and GPT models), code, image, video, speech, music, embeddings. `GET /v1/models` or the [Models](/models) page. **Can I use it with Claude Code, Cursor, ChatGPT…?** Claude Code, OpenCode, Aider, Cline, Continue, Zed and VS Code can run on Carpe models; Claude Desktop, Claude Code, Cursor, Codex, VS Code and others can use the MCP server. ChatGPT and claude.ai on the web need a hosted MCP server, which is planned. Details: [Integrations](/docs/integrations). **Can anyone see my prompts?** On `/v1`, no: requests are processed inside the TEE, and with a `private` model the provider keeps nothing. On `/router`, an external market may serve the request unless you ask for `privacy`. **What if there is not enough capacity?** A pre-flight check refuses cleanly before anything runs or is charged; the router can also fall back to another market. **Which network?** Base (Ethereum L2): escrow, settlement and USDC. ### Support {#support} - [Integrations](/docs/integrations), [Use cases](/docs/use-cases) and the [API reference](/docs/api) - Protocol design: the [Whitepaper](/docs/whitepaper) - Community & support: [Telegram](https://t.me/CarpeDiemCommu) --- # Integrations Carpe Diem reaches your tools in two ways: - **The MCP server** (`carpe-diem-mcp`) gives an AI assistant — Claude Code, Claude Desktop, Cursor, Codex, VS Code, Zed… — around thirty Carpe tools: chat with any model, generate images, video, speech and music, search the web, compare prices, check your balance. The assistant decides when to call them; you stay in control of what it can spend. - **The API** is OpenAI- and Anthropic-compatible. Any SDK, coding agent or chat app that lets you change the base URL can use Carpe models directly, with nothing to install. You need an **API key** for both (or a funded wallet, for [x402](#x402)): create one in [**API → Keys**](/build?tab=keys), then add credits in **API → Balance**. Keys start with `cdm_`. ## Choose your path {#choose} | You want to… | Use | Section | |---|---|---| | Let your AI assistant generate images, video, audio, or call other models | MCP server | [MCP server](#mcp) | | Run a coding agent (Claude Code, OpenCode, Aider, Cline…) on Carpe models | API, as the agent's model provider | [Coding agents](#coding-agents) | | Call models from your own code | API, through the OpenAI or Anthropic SDK | [SDKs](#sdks) | | Give a team a private chat app | API, behind Open WebUI or LibreChat | [Chat apps & automation](#chat-apps) | | Let an autonomous agent pay per request, without an account | x402 (USDC on Base) | [Pay per request](#x402) | | Use Carpe inside ChatGPT or claude.ai on the web | Not yet available | [ChatGPT and claude.ai](#web-assistants) | > [!TIP] > MCP and the API combine well: run Claude Code **on** Carpe models through the API, and give it the MCP server too, so it can also generate the images or speech your project needs. ## Two surfaces: `/v1` and `/router` {#surfaces} Every integration below picks one of two base URLs. They take the same key and the same request bodies; they differ in who serves the request. | | `…/api/operator/v1` | `…/api/operator/router` | |---|---|---| | Served by | Carpe alone — Venice, through provider keys held in Carpe's TEE | The cheapest market for each request: Carpe or an external one (Surplus, AntSeed, OpenRouter, Chutes, Venice, Engy) | | Confidentiality | Always inside the TEE | May leave the TEE. Send `"privacy": true` to stay on TEE markets, `"privacy": "private"` to stay on markets that keep nothing | | Price | Carpe's price | The winning market's price, plus a routing fee on external markets only | | Paths | OpenAI chat and embeddings; Venice-style media (`/v1/image/generate`, `/v1/video/queue`) | OpenAI-style everywhere (`/router/chat/completions`, `/router/images/generations`) | Full base URLs: `https://carpe-diem.xyz/api/operator/v1` and `https://carpe-diem.xyz/api/operator/router`. Every response tells you who served it (`X-Carpe-Route-Market`) and what it cost; see the [API reference](/docs/api#best-price-router-aggregator). > [!NOTE] > Tools that speak the **Anthropic** format (Claude Code, the Anthropic SDK) append `/v1/messages` to their base URL, so they always use the `/v1` surface. The router's Anthropic endpoint, `/router/messages`, is reachable with plain HTTP. ## MCP server {#mcp} [`carpe-diem-mcp`](https://www.npmjs.com/package/carpe-diem-mcp) is an open-source (MIT) [Model Context Protocol](https://modelcontextprotocol.io) server. It runs on your machine, started by your assistant with `npx`, and talks to Carpe with your key. Source: [github.com/Irdanwen/carpe-diem-mcp](https://github.com/Irdanwen/carpe-diem-mcp). Requires **Node.js 20 or later**. ### What it gives your assistant {#mcp-tools} | Group | Tools | Cost | |---|---|---| | Catalogue | `carpe_list_models`, `carpe_model_details`, `carpe_pricing`, `carpe_quote`, `carpe_markets`, `carpe_status` | Free, no key needed | | Account | `carpe_balance`, `carpe_usage_history`, `carpe_usage_summary`, `carpe_key_usage`, `carpe_routing_summary`, `carpe_rate_limits`, `carpe_purchases`, `carpe_session_spend`, `carpe_x402_status`, `carpe_topup_link`, `carpe_revoke_this_key` | Free | | Text | `carpe_chat`, `carpe_embeddings`, `carpe_web_search`, `carpe_web_scrape` | Paid | | Image | `carpe_image_generate`, `carpe_image_edit`, `carpe_image_multi_edit`, `carpe_image_upscale`, `carpe_image_status` | Paid (status is free) | | Audio | `carpe_tts`, `carpe_transcribe`, `carpe_voice_clone` | Paid (cloning needs explicit consent) | | Video & music | `carpe_video_generate`, `carpe_video_status`, `carpe_music_generate`, `carpe_music_status` | Paid (status is free) | It also exposes three **resources** — `carpe://models`, `carpe://pricing`, `carpe://markets` — and three ready-made **prompts**: - `cheapest_model_for` — shortlist models for a task, quote each on both surfaces, run the cheapest acceptable one, report cost and market. - `cost_report` — balance, spend by model and by day, router savings, and a warning if the balance covers less than a week. - `private_inference` — answer a confidential request on `/v1` only, with a model Venice runs privately, and confirm it ran in the TEE. Every result says **what it cost** and **who served it** (market, and whether it was a TEE). Tools that exist on both surfaces take a `route` argument: `"carpe"` (`/v1`, the default) or `"router"` (`/router`). ### Spending stays under your control {#mcp-spend} The server prices every paid call **before** sending it — from free quotes — and refuses it locally, without calling Carpe, when it would exceed: - `CARPE_MAX_PER_CALL_USD` — per call (default `0.50`), or - what is left of `CARPE_MAX_SPEND_USD` — per session (default `5`). `carpe_session_spend` shows the tally. `CARPE_READ_ONLY=1` removes every tool that can spend or change anything. If a key leaks, `carpe_revoke_this_key` (with `confirm: "REVOKE"`) disables it at once. `carpe_topup_link` returns a checkout link for a **human** to open: the assistant never pays by card. Paid calls are retried only when Carpe certainly did not run them (rate limit, capacity), so a retry never bills twice. ### Install {#mcp-install} Pick your client. Each snippet starts the server with `npx`; replace `cdm_your_key` with your key. Then restart the client (or reload its MCP servers) and check it works by asking: *"Call carpe_status."* ```bash tab="Claude Code" group="client" # Available in every project (--scope user). Use --scope project to share it via .mcp.json. claude mcp add carpe --scope user \ --env CARPE_API_KEY=cdm_your_key \ --env CARPE_MAX_SPEND_USD=5 \ -- npx -y carpe-diem-mcp ``` ```jsonc tab="Claude Desktop" group="client" // Settings → Developer → Edit Config // macOS: ~/Library/Application Support/Claude/claude_desktop_config.json // Windows: %APPDATA%\Claude\claude_desktop_config.json { "mcpServers": { "carpe": { "command": "npx", "args": ["-y", "carpe-diem-mcp"], "env": { "CARPE_API_KEY": "cdm_your_key", "CARPE_MAX_SPEND_USD": "5" } } } } ``` ```jsonc tab="Cursor" group="client" // ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (this project) { "mcpServers": { "carpe": { "type": "stdio", "command": "npx", "args": ["-y", "carpe-diem-mcp"], "env": { "CARPE_API_KEY": "${env:CARPE_API_KEY}", "CARPE_MAX_SPEND_USD": "5" } } } } ``` ```jsonc tab="VS Code" group="client" // .vscode/mcp.json — VS Code asks for the key once and stores it securely { "inputs": [ { "type": "promptString", "id": "carpe-key", "description": "Carpe Diem API key (cdm_…)", "password": true } ], "servers": { "carpe": { "type": "stdio", "command": "npx", "args": ["-y", "carpe-diem-mcp"], "env": { "CARPE_API_KEY": "${input:carpe-key}", "CARPE_MAX_SPEND_USD": "5" } } } } ``` ```toml tab="Codex" group="client" # ~/.codex/config.toml — or: codex mcp add carpe --env CARPE_API_KEY=cdm_your_key -- npx -y carpe-diem-mcp [mcp_servers.carpe] command = "npx" args = ["-y", "carpe-diem-mcp"] env_vars = ["CARPE_API_KEY"] # passes your shell's CARPE_API_KEY through [mcp_servers.carpe.env] CARPE_MAX_SPEND_USD = "5" ``` ```jsonc tab="OpenCode" group="client" // opencode.json { "$schema": "https://opencode.ai/config.json", "mcp": { "carpe": { "type": "local", "command": ["npx", "-y", "carpe-diem-mcp"], "enabled": true, "environment": { "CARPE_API_KEY": "{env:CARPE_API_KEY}", "CARPE_MAX_SPEND_USD": "5" } } } } ``` ```jsonc tab="Gemini CLI" group="client" // ~/.gemini/settings.json (or .gemini/settings.json in a project). // Gemini CLI hides inherited variables named *KEY*: declare the key here. { "mcpServers": { "carpe": { "command": "npx", "args": ["-y", "carpe-diem-mcp"], "env": { "CARPE_API_KEY": "$CARPE_API_KEY", "CARPE_MAX_SPEND_USD": "5" } } } } ``` ```jsonc tab="Zed" group="client" // Zed settings.json { "context_servers": { "carpe": { "command": "npx", "args": ["-y", "carpe-diem-mcp"], "env": { "CARPE_API_KEY": "cdm_your_key", "CARPE_MAX_SPEND_USD": "5" } } } } ``` ```jsonc tab="Cline" group="client" // Cline → MCP Servers → Configure MCP Servers { "mcpServers": { "carpe": { "command": "npx", "args": ["-y", "carpe-diem-mcp"], "env": { "CARPE_API_KEY": "cdm_your_key", "CARPE_MAX_SPEND_USD": "5" }, "disabled": false } } } ``` ```yaml tab="Goose" group="client" # ~/.config/goose/config.yaml extensions: carpe: name: Carpe Diem type: stdio cmd: npx args: ["-y", "carpe-diem-mcp"] envs: { "CARPE_API_KEY": "cdm_your_key", "CARPE_MAX_SPEND_USD": "5" } enabled: true timeout: 300 ``` ```yaml tab="Continue" group="client" # .continue/mcpServers/carpe.yaml name: Carpe Diem version: 0.0.1 schema: v1 mcpServers: - name: Carpe Diem command: npx args: ["-y", "carpe-diem-mcp"] env: CARPE_API_KEY: ${{ secrets.CARPE_API_KEY }} CARPE_MAX_SPEND_USD: "5" ``` Other MCP clients — **LM Studio** (Program → Install → Edit `mcp.json`), **Devin Desktop** (formerly Windsurf: `~/.config/devin/mcp_config.json`), **LibreChat** — take the same `mcpServers` block as Claude Desktop above. > [!NOTE] > Formats checked against each client's documentation on 2026-11-06. Clients change their configuration often: if a snippet stops working, the client's own MCP page is the reference. ### Use it {#mcp-use} Talk to your assistant normally; it picks the tools. A few prompts that work well: - *"Find the cheapest model that can summarise this file well, then summarise it."* — uses `carpe_quote` on both surfaces before spending. - *"Generate a 16:9 hero image of a lighthouse at dawn, flat illustration, and save it in ./assets."* - *"Read this paragraph aloud in French with a calm voice and save the MP3."* - *"Make a 5-second clip of a paper boat on a calm lake. Quote it first."* — video and music are jobs; the assistant polls `carpe_video_status` and saves the file. - *"Search the web for the latest Base network upgrade and summarise it with sources."* - *"How much did I spend this week, on which models, and how much did the router save me?"* — or run the `cost_report` prompt. - *"Answer this privately, inside the TEE only: …"* — or run the `private_inference` prompt. Complete scenarios, with code and what they cost: [Use cases](/docs/use-cases). Media inputs accept an `https://` URL, a `data:` URI or a local file path. Images under 4 MiB and audio under 8 MiB come back inline; set `CARPE_OUTPUT_DIR` to also save them to disk. Videos and music are always saved to a file (a finished video stays on Carpe for about ten minutes). ### Configuration {#mcp-config} | Variable | Default | Meaning | |---|---|---| | `CARPE_API_KEY` | — | Your `cdm_…` key. Billed to your account. | | `CARPE_MAX_SPEND_USD` | `5` | Session budget. `0` disables it. | | `CARPE_MAX_PER_CALL_USD` | `0.50` | Per-call limit. `0` disables it. | | `CARPE_DEFAULT_ROUTE` | `carpe` | `carpe` (`/v1`, always TEE) or `router` (`/router`, cheapest market). | | `CARPE_DEFAULT_CHAT_MODEL` | `deepseek-v4-flash` | Model used when the assistant does not name one. | | `CARPE_OUTPUT_DIR` | — | Where images, audio, video and music are written. | | `CARPE_READ_ONLY` | `0` | `1` keeps only the free tools. | | `CARPE_X402_PRIVATE_KEY` / `CARPE_X402_BUDGET_USD` | — | Pay per request from a wallet instead of a key (see [x402](#x402)). | | `CARPE_HTTP_TIMEOUT_MS` | `120000` | Timeout of one call to Carpe. | Without a key or a wallet, the free tools (catalogue, prices, quotes, markets, status) still work. ### Local HTTP mode {#mcp-http} For a client that only speaks HTTP, run the server yourself: ```bash CARPE_API_KEY=cdm_your_key npx -y carpe-diem-mcp --http --port 3333 # → http://127.0.0.1:3333/mcp (Streamable HTTP) ``` It binds to `127.0.0.1`. Binding anywhere else requires `CARPE_MCP_AUTH_TOKEN` (16+ characters, sent as `Authorization: Bearer`), and browser origins are refused unless listed in `CARPE_MCP_ALLOWED_ORIGINS`. > [!WARNING] > Whoever reaches this server spends **your** key. Do not expose it on a public address. ### Troubleshooting {#mcp-troubleshooting} | Symptom | Fix | |---|---| | The server does not start, or `npx` is not found | Install Node.js 20+ and restart the client from a shell where `node --version` works. On Windows, if the client cannot start `npx`, use `"command": "cmd", "args": ["/c", "npx", "-y", "carpe-diem-mcp"]`. | | `NO_PAYMENT_METHOD` | Neither `CARPE_API_KEY` nor an x402 wallet reached the server. Check the `env` block and restart the client. | | `SPEND_LIMIT_PER_CALL` / `SPEND_LIMIT_SESSION` | The spend guard refused the call before sending it. Raise the limit, or start a new session. | | `402 PAYMENT_REQUIRED` | Your balance cannot cover the request: add credits in **API → Balance**, or ask the assistant for `carpe_topup_link`. | | `API_KEY_REQUIRED` | Account tools need a key; they are not available in x402 mode. | | `GEO_RESTRICTED` (HTTP 451) | Carpe is not available from your region. | | The key is rejected at start-up | Keys look like `cdm_` followed by hexadecimal characters; copy it again from **API → Keys**. | ## ChatGPT and claude.ai {#web-assistants} ChatGPT, and Claude on the web and on mobile, only accept **remote** MCP servers — a public HTTPS URL, signed in with OAuth. The Carpe MCP server runs on your machine, so it cannot be added there yet. A hosted Carpe MCP server with sign-in is planned; this page will say so when it is live. Until then: - **Claude** — use Claude Desktop or Claude Code, which run the server locally ([install](#mcp-install)). - **OpenAI** — Codex (CLI, IDE extension, desktop app) runs it locally ([install](#mcp-install)). > [!WARNING] > Do not put the local HTTP mode behind a public URL to reach it from ChatGPT or claude.ai: anyone who finds the URL can spend your credits. ## Coding agents {#coding-agents} Run your coding agent on Carpe models. The agent sends its requests to Carpe instead of its usual provider, with your key. Pick a model with tool calling (`capabilities.supportsFunctionCalling` in [`/v1/models`](https://carpe-diem.xyz/api/operator/v1/models)); the [Models](/models) page shows live prices. Good starting points: `claude-sonnet-5-5` and `claude-haiku-5-5` (Anthropic), `kimi-k2-7-code`, `qwen3-coder-480b-a35b-instruct-turbo`, `deepseek-v4-pro`. ### Claude Code {#claude-code} Claude Code speaks the Anthropic format; point it at Carpe's `/v1/messages`: ```bash export ANTHROPIC_BASE_URL=https://carpe-diem.xyz/api/operator export ANTHROPIC_AUTH_TOKEN=cdm_your_key # sent as Authorization: Bearer export ANTHROPIC_MODEL=claude-sonnet-5-5 export ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-5-5 # background tasks claude ``` - `ANTHROPIC_AUTH_TOKEN` is sent as `Authorization: Bearer`, like every other Carpe call. (`ANTHROPIC_API_KEY`, sent as `x-api-key`, is accepted on `/v1/messages` too.) - Requests run on `/v1`, through Carpe's TEE. Claude models are listed as `anonymized` in the catalogue, not `private`: pick a `private` model for confidential code. - Carpe has no `/v1/messages/count_tokens`; Claude Code then estimates token counts itself. - If Claude Code reports an error about beta headers, add `export CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1`. ### OpenCode {#opencode} Add Carpe as an OpenAI-compatible provider in `opencode.json`, then choose a model with `/models`: ```json { "$schema": "https://opencode.ai/config.json", "provider": { "carpe": { "npm": "@ai-sdk/openai-compatible", "name": "Carpe Diem", "options": { "baseURL": "https://carpe-diem.xyz/api/operator/v1", "apiKey": "{env:CARPE_API_KEY}" }, "models": { "kimi-k2-7-code": { "name": "Kimi K2.7 Code", "limit": { "context": 256000, "output": 65536 } }, "claude-sonnet-5-5": { "name": "Claude Sonnet 5.5", "limit": { "context": 1000000, "output": 128000 } } } } } } ``` Use `@ai-sdk/openai-compatible`, not `@ai-sdk/openai`: the latter calls `/v1/responses`, which Carpe does not serve. For the best price on every request, use `https://carpe-diem.xyz/api/operator/router` as `baseURL`. ### Aider {#aider} ```bash export OPENAI_API_BASE=https://carpe-diem.xyz/api/operator/v1 export OPENAI_API_KEY=cdm_your_key aider --model openai/kimi-k2-7-code ``` ### Cline {#cline} In Cline's settings: **API Provider → OpenAI Compatible**, Base URL `https://carpe-diem.xyz/api/operator/v1`, your key as API Key, and a model id from the catalogue (for example `kimi-k2-7-code`). ### Continue {#continue} ```yaml # config.yaml models: - name: Carpe — Kimi K2.7 Code provider: openai model: kimi-k2-7-code apiBase: https://carpe-diem.xyz/api/operator/v1 apiKey: ${{ secrets.CARPE_API_KEY }} useResponsesApi: false ``` ### Zed {#zed} ```json { "language_models": { "openai_compatible": { "carpe": { "api_url": "https://carpe-diem.xyz/api/operator/v1", "available_models": [ { "name": "kimi-k2-7-code", "display_name": "Kimi K2.7 Code (Carpe)", "max_tokens": 256000 } ] } } } } ``` Zed reads the key from the `CARPE_API_KEY` environment variable (the provider id in capitals, plus `_API_KEY`), or asks for it in the agent panel. ### VS Code (Copilot) {#vscode-copilot} Copilot Chat can use a custom endpoint: **Chat → Manage Language Models → Custom Endpoint**, or in `chatLanguageModels.json`: ```json [ { "name": "Carpe Diem", "vendor": "customendpoint", "apiKey": "${input:carpeKey}", "apiType": "chat-completions", "models": [ { "id": "kimi-k2-7-code", "name": "Kimi K2.7 Code (Carpe)", "url": "https://carpe-diem.xyz/api/operator/v1/chat/completions", "toolCalling": true, "maxInputTokens": 256000, "maxOutputTokens": 65536 } ] } ] ``` Your organisation's Copilot policy may disable custom models. ### Goose and `llm` {#goose-llm} ```jsonc tab="Goose" group="agent" // ~/.config/goose/custom_providers/carpe.json — the key comes from $CARPE_API_KEY { "name": "carpe", "engine": "openai", "display_name": "Carpe Diem", "api_key_env": "CARPE_API_KEY", "base_url": "https://carpe-diem.xyz/api/operator/v1/chat/completions", "models": [{ "name": "kimi-k2-7-code", "context_limit": 256000 }], "supports_streaming": true, "requires_auth": true } ``` ```bash tab="llm" group="agent" llm keys set carpe # paste cdm_your_key llm openai endpoint add https://carpe-diem.xyz/api/operator/v1 \ -m kimi-k2-7-code --id carpe --key carpe --supports-tools llm -m carpe "Explain this regex: ^cdm_[0-9a-f]{16,}$" ``` ### Not supported as a model provider {#not-providers} - **Codex CLI** only speaks OpenAI's Responses API (`/v1/responses`), which Carpe does not serve. Use Codex with the [MCP server](#mcp-install) instead. - **Gemini CLI** only talks to Gemini endpoints. Use it with the [MCP server](#mcp-install). - **Cursor** does not let you set a custom base URL for its own models. Use it with the [MCP server](#mcp-install). ## SDKs {#sdks} Any OpenAI or Anthropic SDK works: change the base URL and the key, keep your code. ```python tab="OpenAI (Python)" group="sdk" from openai import OpenAI client = OpenAI( base_url="https://carpe-diem.xyz/api/operator/v1", # or …/router for the best price api_key="cdm_your_key", ) r = client.chat.completions.create( model="deepseek-v4-flash", messages=[{"role": "user", "content": "Say hi in five words."}], ) print(r.choices[0].message.content) ``` ```js tab="OpenAI (JS)" group="sdk" import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://carpe-diem.xyz/api/operator/v1", // or …/router for the best price apiKey: process.env.CARPE_API_KEY, }); const r = await client.chat.completions.create({ model: "deepseek-v4-flash", messages: [{ role: "user", content: "Say hi in five words." }], }); console.log(r.choices[0].message.content); ``` ```python tab="Anthropic (Python)" group="sdk" from anthropic import Anthropic client = Anthropic( base_url="https://carpe-diem.xyz/api/operator", # the SDK appends /v1/messages auth_token="cdm_your_key", ) r = client.messages.create( model="claude-haiku-5-5", max_tokens=200, messages=[{"role": "user", "content": "Say hi in five words."}], ) print(r.content[0].text) ``` ```js tab="Anthropic (JS)" group="sdk" import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://carpe-diem.xyz/api/operator", // the SDK appends /v1/messages authToken: process.env.CARPE_API_KEY, }); const r = await client.messages.create({ model: "claude-haiku-5-5", max_tokens: 200, messages: [{ role: "user", content: "Say hi in five words." }], }); console.log(r.content[0].text); ``` ```ts tab="Vercel AI SDK" group="sdk" import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { generateText } from "ai"; const carpe = createOpenAICompatible({ name: "carpe", baseURL: "https://carpe-diem.xyz/api/operator/v1", apiKey: process.env.CARPE_API_KEY, includeUsage: true, }); const { text } = await generateText({ model: carpe("deepseek-v4-flash"), prompt: "Say hi in five words." }); console.log(text); ``` ```python tab="LangChain" group="sdk" from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="deepseek-v4-flash", base_url="https://carpe-diem.xyz/api/operator/v1", api_key="cdm_your_key", use_responses_api=False, # Carpe serves Chat Completions, not Responses ) print(llm.invoke("Say hi in five words.").content) ``` ```python tab="LlamaIndex" group="sdk" # pip install llama-index-llms-openai-like from llama_index.llms.openai_like import OpenAILike llm = OpenAILike( model="deepseek-v4-flash", api_base="https://carpe-diem.xyz/api/operator/v1", api_key="cdm_your_key", is_chat_model=True, is_function_calling_model=True, context_window=128000, ) print(llm.complete("Say hi in five words.")) ``` ```python tab="LiteLLM" group="sdk" from litellm import completion r = completion( model="openai/deepseek-v4-flash", # openai/ = any OpenAI-compatible server api_base="https://carpe-diem.xyz/api/operator/v1", api_key="cdm_your_key", messages=[{"role": "user", "content": "Say hi in five words."}], ) print(r.choices[0].message.content) ``` ```bash tab="curl" group="sdk" curl https://carpe-diem.xyz/api/operator/v1/chat/completions \ -H "Authorization: Bearer $CARPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Say hi in five words."}]}' ``` > [!WARNING] > Several libraries switch to OpenAI's Responses API on their own — the Vercel AI SDK's `openai()` provider, LangChain for some model names, Continue for o-series names. Carpe serves **Chat Completions**: use `createOpenAICompatible` (or `openai.chat(…)`), `use_responses_api=False`, `useResponsesApi: false`. What works the OpenAI way: streaming (`stream: true`, SSE), tools and `tool_choice`, `response_format` (JSON schema), images in messages (vision models), embeddings (`/v1/embeddings`). Images, audio, video and music have their own endpoints: see the [API reference](/docs/api). Calls go server to server — a browser page on another site cannot call the API directly (CORS). ## Chat apps & automation {#chat-apps} ### Open WebUI {#open-webui} **Admin settings → Connections → OpenAI → Add connection**: URL `https://carpe-diem.xyz/api/operator/v1` (or `/router`), your key. *Verify connection* lists the models. Or with environment variables: ```bash OPENAI_API_BASE_URL=https://carpe-diem.xyz/api/operator/v1 OPENAI_API_KEY=cdm_your_key ``` Open WebUI connects to MCP servers over HTTP only. To give it the Carpe tools, run the [local HTTP mode](#mcp-http) where Open WebUI can reach it (behind its auth token), or Open WebUI's `mcpo` proxy. ### LibreChat {#librechat} ```yaml # librechat.yaml endpoints: custom: - name: "Carpe Diem" apiKey: "${CARPE_API_KEY}" # or "user_provided": each user brings a key baseURL: "https://carpe-diem.xyz/api/operator/v1" models: default: ["deepseek-v4-flash", "claude-sonnet-5-5"] fetch: true mcpServers: carpe: type: stdio command: npx args: ["-y", "carpe-diem-mcp"] env: CARPE_API_KEY: "${CARPE_API_KEY}" CARPE_MAX_SPEND_USD: "5" ``` ### n8n {#n8n} Create an **OpenAI** credential, set **Base URL** to `https://carpe-diem.xyz/api/operator/v1` and paste your key. The *OpenAI Chat Model* node then lists Carpe models; leave *Use Responses API* off. (Make's OpenAI module has no base URL setting: use its HTTP module against the same endpoint.) ## Pay per request, without an account (x402) {#x402} An autonomous agent can pay each request from its own wallet — USDC on Base, no key, no signup — with the [x402](https://x402.org) protocol. - **With the MCP server**: set a wallet instead of a key. The server signs **one** USDC permit whose value is your budget: Carpe can never pull more than that, in total. ```json "env": { "CARPE_X402_PRIVATE_KEY": "0x…", "CARPE_X402_BUDGET_USD": "2", "CARPE_MAX_PER_CALL_USD": "0.25" } ``` Use a dedicated wallet that holds only what you are willing to spend. In this mode chat is not streamed, account tools are unavailable, and cloned voices cannot be used. - **From your own code**: `POST /v1/x402/…` without payment returns a `402` with the payment options; sign, and resend with an `X-PAYMENT` header. The flow, the two schemes (`carpe-session`, `exact`) and the error codes are in the [API reference](/docs/api#wallet-x402-direct). ## For AI agents reading this {#llms} A plain-text summary of Carpe for language models is at [`/llms.txt`](/llms.txt), and the full English documentation in one file at [`/llms-full.txt`](/llms-full.txt). The model catalogue (`/v1/models`), prices (`/pricing`), router markets (`/router/markets`) and quotes (`POST /router/quote`) are public and free. --- # Use cases Each recipe gives the goal, the path (MCP server, API or both), working code, **what it cost when we ran it** and **how private it is**. Set up once: ```bash export CARPE_API_KEY=cdm_your_key # API → Keys pip install openai requests # Python recipes ``` Costs below were measured on 2026-11-06 and move with the [dynamic price](/docs#pricing). Before running something large, ask for a quote — it is free: `POST /router/quote` for text, `POST /v1/video/quote` for video, [`/pricing`](https://carpe-diem.xyz/api/operator/pricing) for everything else. | Recipe | Path | Typical cost | Private? | |---|---|---|---| | [Cheapest model for a task](#cheapest-model) | MCP · API | under $0.0001 per answer | Your choice | | [Confidential analysis in the TEE](#confidential) | MCP · API | ~$0.0005 per page | Yes — TEE, model that keeps nothing | | [A cheaper coding agent](#coding-agent) | API | per token — see the [Models](/models) page | Depends on the model | | [Images: generate, edit, upscale](#images) | MCP · API | ~$0.03 for the chain | TEE on `/v1` | | [A short video clip](#video) | MCP · API | ~$0.10 for 5 s | TEE on `/v1` | | [Voice: read aloud, transcribe](#voice) | MCP · API | ~$0.001 per paragraph | TEE on `/v1` | | [Music for a video](#music) | MCP · API | ~$0.016 per minute | TEE on `/v1` | | [Answer questions on your documents (RAG)](#rag) | API | ~$0.0001 per question | Your choice | | [Web research with sources](#web-research) | MCP · API | ~$0.006 per question | TEE on `/v1` | | [Structured extraction at scale](#extraction) | API | ~$0.0001 per document | Your choice | | [Keep spending under control](#spend-control) | MCP · API | free | — | | [An agent that pays for itself (x402)](#x402-agent) | MCP · API | what it uses, capped on-chain | Your choice | | [A private chat app for a team](#team-chat) | API | per message | TEE on `/v1` | ## Cheapest model for a task {#cheapest-model} **Goal:** get a good-enough answer at the lowest price, without testing models by hand. **With the MCP server**, run the `cheapest_model_for` prompt, or just ask: *"Find the cheapest model that can translate this into German well, then do it."* The assistant shortlists models, quotes each on `/v1` and `/router` with `carpe_quote` (free), runs the cheapest acceptable one and tells you what it cost and who served it. **With the API**, quote first, then send the request to the router — it picks the cheapest market by itself: ```python import os, requests from openai import OpenAI BASE = "https://carpe-diem.xyz/api/operator" # 1. Free quote: what would 500 tokens in / 300 out cost, on each market? q = requests.post(f"{BASE}/router/quote", json={"model": "deepseek-v4-flash", "input_tokens": 500, "max_tokens": 300}).json() print("best:", q["best"]["market"], q["best"]["costUsd"], "USD") # 2. Run it through the router: the cheapest market serves it. client = OpenAI(base_url=f"{BASE}/router", api_key=os.environ["CARPE_API_KEY"]) raw = client.chat.completions.with_raw_response.create( model="deepseek-v4-flash", messages=[{"role": "user", "content": "Translate into German: The meeting moved to Thursday."}], ) print(raw.parse().choices[0].message.content) print("served by", raw.headers.get("x-carpe-route-market"), "for", raw.headers.get("x-carpe-route-cost-usdc"), "USDC") ``` **Cost:** $0.00003 for this translation (served by OpenRouter when we ran it). **Privacy:** the router may serve it outside the TEE; add `extra_body={"privacy": True}` to stay on TEE markets. ## Confidential analysis in the TEE {#confidential} **Goal:** summarise a contract, a medical letter or internal code without it being kept by anyone. Use the `/v1` surface (always through Carpe's TEE) and a model listed as `privacy: "private"` in [`/v1/models`](https://carpe-diem.xyz/api/operator/v1/models) — Venice runs those without keeping prompts or answers. **With the MCP server**, run the `private_inference` prompt, or ask: *"Privately, inside the TEE only, summarise ./contract.txt in five bullet points."* The result says `tee: true`. ```python import os, requests from openai import OpenAI BASE = "https://carpe-diem.xyz/api/operator" models = requests.get(f"{BASE}/v1/models").json()["data"] private = [m["id"] for m in models if m.get("carpe_diem_type") == "text" and m.get("privacy") == "private"] print(len(private), "private text models, e.g.", private[:3]) client = OpenAI(base_url=f"{BASE}/v1", api_key=os.environ["CARPE_API_KEY"]) document = "This agreement is made between Alpha SA and Beta GmbH. Term: 24 months. Either party may terminate with 90 days' notice." r = client.chat.completions.create( model="deepseek-v4-flash", # listed "private" messages=[ {"role": "system", "content": "You summarise legal documents in five short bullet points."}, {"role": "user", "content": document}, ], ) print(r.choices[0].message.content) ``` **Cost:** $0.0001 for this summary; count about $0.0005 for a full page. **Privacy:** TEE end to end on Carpe's side, and a model that keeps nothing. The `e2ee-*` models go further (end-to-end encrypted to the model's own enclave). Check the attestation at [`/attestation`](https://carpe-diem.xyz/api/operator/attestation) and on the [Privacy](/privacy) page. ## A cheaper coding agent {#coding-agent} **Goal:** keep your coding agent, pay less per task. Point the agent at Carpe and pick a model by price and skill: Claude models for the hardest work, `kimi-k2-7-code`, `qwen3-coder-480b-a35b-instruct-turbo` or `deepseek-v4-pro` for most of it. Step-by-step setup for Claude Code, OpenCode, Aider, Cline, Continue, Zed and VS Code: [Coding agents](/docs/integrations#coding-agents). Give the same agent the [MCP server](/docs/integrations#mcp-install) and it can also produce the assets your project needs — icons, mock-ups, voice-overs — without leaving the terminal. **Cost:** you pay per token at Carpe's price, shown live on the [Models](/models) page; the API space shows what each key spent. **Privacy:** prefer a `private` model for proprietary code; Claude models are `anonymized`. ## Images: generate, edit, upscale {#images} **Goal:** a hero image for a page, then a variant, then a print-size version. **With the MCP server:** *"Generate a 16:9 flat illustration of a red fox in a forest, then make the fox blue, then upscale it ×2. Save everything in ./assets."* — `carpe_image_generate`, `carpe_image_edit`, `carpe_image_upscale`. ```python import base64, os, time, requests BASE = "https://carpe-diem.xyz/api/operator/v1" H = {"Authorization": f"Bearer {os.environ['CARPE_API_KEY']}"} def queued(path, body): """Queue a slow image job, poll until the image comes back (binary).""" qid = requests.post(f"{BASE}/{path}/queue", headers=H, json=body).json()["queue_id"] while True: r = requests.post(f"{BASE}/{path}/retrieve", headers=H, json={"queue_id": qid}) if r.headers.get("content-type", "").startswith("image/"): return r.content if r.json().get("status") != "pending": raise RuntimeError(r.text) time.sleep(2) # 1. Generate (synchronous for fast models) → base64 WebP gen = requests.post(f"{BASE}/image/generate", headers=H, json={ "model": "z-image-turbo", "prompt": "a red fox in a forest, flat illustration", }).json() fox = gen["images"][0] open("fox.webp", "wb").write(base64.b64decode(fox)) # 2. Edit (queued: edit models take longer) — the source goes in as a data URI blue = queued("image/edit", { "model": "qwen-image-2-1-turbo-edit", "prompt": "make the fox blue", "image": f"data:image/webp;base64,{fox}", }) open("fox-blue.png", "wb").write(blue) # 3. Upscale ×2: raw base64, no data: prefix up = requests.post(f"{BASE}/image/upscale", headers=H, json={ "model": "upscaler", "image": base64.b64encode(blue).decode(), "scale": 2, }, timeout=180) up.raise_for_status() open("fox-blue-2x.png", "wb").write(up.content) print("cost of the last step:", up.headers.get("x-carpe-cost-usdc-micro"), "µUSDC") ``` **Cost:** about $0.005 to generate, $0.011 to edit and $0.011 to upscale ×2. **Privacy:** on `/v1`, through the TEE. A model too slow for a single request answers `409 MODEL_REQUIRES_ASYNC` with the endpoint to use: the `queued()` helper above works for `image/generate`, `image/edit` and `image/multi-edit` ([queue reference](/docs/api#post-v1-image-generate-queue-retrieve-complete-async)). ## A short video clip {#video} **Goal:** a 5-second clip for a post or a landing page. **With the MCP server:** *"Make a 5-second 16:9 clip of a paper boat on a calm lake. Quote it first."* — `carpe_video_generate` quotes, queues, then `carpe_video_status` waits and saves the MP4. ```python import os, time, requests BASE = "https://carpe-diem.xyz/api/operator" H = {"Authorization": f"Bearer {os.environ['CARPE_API_KEY']}"} job = {"model": "grok-imagine-1-5-lite-text-to-video", "prompt": "a paper boat on a calm lake, morning light", "duration": "5s", "aspect_ratio": "16:9"} print("quote:", requests.post(f"{BASE}/v1/video/quote", headers=H, json=job).json()["cost_usdc"], "USD") qid = requests.post(f"{BASE}/v1/video/queue", headers=H, json=job).json()["queue_id"] while True: r = requests.post(f"{BASE}/v1/video/retrieve", headers=H, json={"queue_id": qid}).json() if r["status"] == "completed": url = r.get("video_url") or r.get("url") video = requests.get(url if url.startswith("http") else BASE + url) # kept ~10 minutes open("boat.mp4", "wb").write(video.content) break if r["status"] == "failed": raise RuntimeError(r) time.sleep(5) ``` **Cost:** $0.097 for these 5 seconds, exactly the quote (models range from cents to dollars: always quote first). Durations, aspect ratios and resolutions differ per model: read them from `constraints` in [`/v1/models`](https://carpe-diem.xyz/api/operator/v1/models). **Privacy:** on `/v1`, through the TEE. Download the file as soon as it is ready. ## Voice: read aloud, transcribe {#voice} **Goal:** turn a paragraph into an MP3, and an audio file back into text. **With the MCP server:** *"Read this paragraph aloud in French and save the MP3. Then transcribe it to check."* — `carpe_tts`, `carpe_transcribe`. ```python import os, requests BASE = "https://carpe-diem.xyz/api/operator/v1" H = {"Authorization": f"Bearer {os.environ['CARPE_API_KEY']}"} speech = requests.post(f"{BASE}/audio/speech", headers=H, json={ "model": "tts-kokoro", "input": "Bonjour ! Ceci est un test de synthèse vocale.", "response_format": "mp3", }) speech.raise_for_status() open("hello.mp3", "wb").write(speech.content) text = requests.post(f"{BASE}/audio/transcriptions", headers=H, files={"file": ("hello.mp3", speech.content, "audio/mpeg")}, data={"model": "openai/whisper-large-v3"}).json() print(text["text"]) ``` **Cost:** $0.0003 for this sentence, both steps (speech is billed per character, transcription per minute of audio). Voices are per model: omit `voice` for the default, or pick one from the model's `voices` in `/v1/models`. **Privacy:** on `/v1`, through the TEE. Cloning a voice (`carpe_voice_clone`, `POST /v1/audio/voices`) requires the speaker's consent and is kept 7 days by the provider. ## Music for a video {#music} **Goal:** a background track sized to your clip. **With the MCP server:** *"Compose a 60-second calm lo-fi piano track, instrumental, and save it."* — `carpe_music_generate`, `carpe_music_status`. ```python import os, time, requests BASE = "https://carpe-diem.xyz/api/operator/v1" H = {"Authorization": f"Bearer {os.environ['CARPE_API_KEY']}"} qid = requests.post(f"{BASE}/audio/music/queue", headers=H, json={ "model": "ace-step-15", "prompt": "calm lo-fi piano loop, warm, slow", "duration_seconds": 60, }).json()["queue_id"] while True: r = requests.post(f"{BASE}/audio/music/retrieve", headers=H, json={"queue_id": qid, "model": "ace-step-15"}) if r.headers.get("content-type", "").startswith("audio/"): open("track.flac", "wb").write(r.content) # ace-step-15 delivers FLAC; a /v1 track can be fetched once break if r.json().get("status") == "failed": raise RuntimeError(r.text) time.sleep(5) ``` **Cost:** about $0.016 for 60 seconds on `ace-step-15`. Each music model accepts different lengths, options and file formats (`duration_options`, `supports_lyrics`, `default_format`…): read its `constraints` in `/v1/models` first. **Privacy:** on `/v1`, through the TEE. ## Answer questions on your documents (RAG) {#rag} **Goal:** a small question-answering bot over your own notes. ```python import os, math from openai import OpenAI client = OpenAI(base_url="https://carpe-diem.xyz/api/operator/v1", api_key=os.environ["CARPE_API_KEY"]) docs = [ "Refunds are possible within 30 days of purchase.", "Support answers in English and French, Monday to Friday.", "Enterprise plans include a dedicated account manager.", ] def embed(texts): r = client.embeddings.create(model="text-embedding-bge-m3", input=texts) return [d.embedding for d in r.data] def cos(a, b): return sum(x * y for x, y in zip(a, b)) / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b))) index = embed(docs) question = "Can I get my money back after three weeks?" q = embed([question])[0] best = max(range(len(docs)), key=lambda i: cos(q, index[i])) answer = client.chat.completions.create( model="deepseek-v4-flash", messages=[ {"role": "system", "content": f"Answer only from this context: {docs[best]}"}, {"role": "user", "content": question}, ], ) print(answer.choices[0].message.content) ``` **Cost:** $0.00015 for this question, embeddings included; embedding a large document set once costs a fraction of a cent per hundred pages. **Privacy:** both calls on `/v1`, through the TEE; `text-embedding-bge-m3` and `deepseek-v4-flash` are `private` models. The same works with [LangChain or LlamaIndex](/docs/integrations#sdks). ## Web research with sources {#web-research} **Goal:** a short, sourced answer about something recent. **With the MCP server:** *"Search the web for the latest Base network upgrade and summarise it with sources."* — `carpe_web_search`, `carpe_web_scrape`, then `carpe_chat`. ```python import os, requests from openai import OpenAI BASE = "https://carpe-diem.xyz/api/operator/v1" H = {"Authorization": f"Bearer {os.environ['CARPE_API_KEY']}"} results = requests.post(f"{BASE}/augment/search", headers=H, json={"query": "Base network latest upgrade"}).json()["results"][:5] context = "\n\n".join(f"[{i + 1}] {r['title']} ({r['url']})\n{r['content']}" for i, r in enumerate(results)) client = OpenAI(base_url=BASE, api_key=os.environ["CARPE_API_KEY"]) r = client.chat.completions.create( model="deepseek-v4-flash", messages=[ {"role": "system", "content": "Answer from the sources below and cite them as [n].\n\n" + context}, {"role": "user", "content": "What changed in the latest Base network upgrade?"}, ], ) print(r.choices[0].message.content) ``` To read a full page, `POST /v1/augment/scrape` with `{"url": "https://…"}` returns its content. **Cost:** $0.0054 per search or scrape, plus the answer ($0.0056 in all for this one). **Privacy:** on `/v1`, through the TEE; the search itself goes to the web. ## Structured extraction at scale {#extraction} **Goal:** turn thousands of emails, invoices or tickets into clean JSON, cheaply and without double billing on retries. ```python import json, os, uuid from openai import OpenAI client = OpenAI(base_url="https://carpe-diem.xyz/api/operator/router", api_key=os.environ["CARPE_API_KEY"], max_retries=3) schema = { "name": "invoice", "strict": True, "schema": { "type": "object", "properties": { "vendor": {"type": "string"}, "total": {"type": "number"}, "currency": {"type": "string"}, "due_date": {"type": "string"}, }, "required": ["vendor", "total", "currency", "due_date"], "additionalProperties": False, }, } def extract(text: str) -> dict: r = client.chat.completions.create( model="deepseek-v4-flash", messages=[{"role": "user", "content": f"Extract the invoice fields:\n\n{text}"}], response_format={"type": "json_schema", "json_schema": schema}, # Same key on a retry → the router replays the first answer instead of billing twice. extra_headers={"Idempotency-Key": str(uuid.uuid5(uuid.NAMESPACE_URL, text))}, ) return json.loads(r.choices[0].message.content) print(extract("ACME Corp — Invoice 1042 — Total due: 1,250.00 EUR by 2026-12-01.")) ``` **Cost:** $0.00004 for this invoice on the router. **Privacy:** the router may serve it outside the TEE: add `extra_body={"privacy": "private"}` for markets that keep nothing (the idempotent replay is then off), or use `/v1`. Keep under the [rate limits](/docs/api#get-v1-rate_limits): read yours with `GET /v1/rate_limits`. ## Keep spending under control {#spend-control} **Goal:** know where the money goes, and make sure an agent cannot run away with it. - **Per key:** create one key per app or agent in **API → Keys**, so each one's spend is separate (`GET /buyer/api-keys/usage`). - **In the MCP server:** `CARPE_MAX_PER_CALL_USD` and `CARPE_MAX_SPEND_USD` refuse a call *before* it is sent; `CARPE_READ_ONLY=1` for an assistant that should only look things up. - **Reports:** run the `cost_report` prompt, or ask *"How much did I spend this week, on which models, and how much did the router save?"*. With the API: ```bash curl -s https://carpe-diem.xyz/api/operator/v1/credits -H "Authorization: Bearer $CARPE_API_KEY" curl -s "https://carpe-diem.xyz/api/operator/buyer/usage/summary?days=7" -H "Authorization: Bearer $CARPE_API_KEY" curl -s "https://carpe-diem.xyz/api/operator/buyer/routing/summary" -H "Authorization: Bearer $CARPE_API_KEY" ``` - **Every response** carries its cost (`X-Carpe-Cost-Usdc-Micro` on `/v1`, `X-Carpe-Route-Cost-Usdc` on `/router`) and your remaining balance (`X-Carpe-Available-Credits`), so an app can stop itself. - **If a key leaks:** revoke it in **API → Keys**, or let the key revoke itself: `POST /v1/keys/self/revoke` (the MCP tool `carpe_revoke_this_key`). ## An agent that pays for itself (x402) {#x402-agent} **Goal:** an autonomous agent with its own wallet and a hard budget — no account, no key to manage. Fund a **dedicated** wallet with a little USDC on Base, then give its private key to the MCP server with a budget: ```json { "mcpServers": { "carpe": { "command": "npx", "args": ["-y", "carpe-diem-mcp"], "env": { "CARPE_X402_PRIVATE_KEY": "0xYOUR_AGENT_WALLET_KEY", "CARPE_X402_BUDGET_USD": "2", "CARPE_MAX_PER_CALL_USD": "0.25" } } } } ``` The server signs **one** USDC permit whose value is the budget: Carpe can never pull more than $2 in total, whatever the agent does, and the spend is remembered across restarts. `carpe_x402_status` shows the wallet, its USDC and what is left. To do it from your own code, see [x402 in the API reference](/docs/api#wallet-x402-direct). **Cost:** what the agent uses, at the same prices. **Privacy:** no account, no email: the wallet is the identity. ## A private chat app for a team {#team-chat} **Goal:** a ChatGPT-like app for your team, on models you choose, without each person needing an account anywhere. Deploy [Open WebUI or LibreChat](/docs/integrations#chat-apps) with one Carpe key (or one key per user with LibreChat's `user_provided`). Point it at `https://carpe-diem.xyz/api/operator/v1` so every conversation goes through the TEE, and leave only `private` models visible for confidential work. Give the app the MCP server too (LibreChat) and people can ask for images, voice and web research from the same chat. **Cost:** per message, at Carpe's price; track it per key in **API → Usage**. **Privacy:** on `/v1`, through the TEE; with `private` models nothing is kept by the model provider. --- # Carpe Diem — API Reference ## Conventions - **Base URL:** `https://carpe-diem.xyz/api/operator` - **Auth:** pass `Authorization: Bearer ` — `` 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 **two** prefixes — pick one as your SDK base URL: - `…/api/operator/v1` — served by Carpe's own TEE network. - `…/api/operator/router` — the [best-price router](#best-price-router-aggregator): prices each request across Carpe and the external markets, serves the cheapest, falls back to Carpe. Same key, same bodies; swapping `/v1` → `/router` is the whole integration. - **Anthropic-compatible** endpoints: `POST /v1/messages` and `POST /router/messages`. Anthropic SDKs and Claude Code append `/v1/messages` to their base URL, so their base URL is `…/api/operator` and they always reach `/v1`. - **`x-api-key`** is accepted in place of `Authorization: Bearer` on the Anthropic endpoints and on `GET /v1/credits`, `GET /v1/billing/balance` and `GET /v1/rate_limits` only. - **Not served:** OpenAI's Responses API (`/v1/responses`) and Anthropic's `/v1/messages/count_tokens`. Point tools at Chat Completions or Messages. - **Server to server.** Browser pages on other origins are refused by CORS: call the API from your backend, a CLI or an agent. - **Regions:** sanctioned countries are refused everywhere (`451`). From the United States, buying inference is allowed; provider, staking and TEE administration routes are not. - **Integration guides** for SDKs, coding agents, chat apps and the MCP server: [Integrations](/docs/integrations). ## Response headers {#response-headers} Every inference response says what it cost and what you have left, so an app or an agent can budget without a separate call: | Header | Meaning | |---|---| | `X-Carpe-Cost-Usdc-Micro` | Cost of this call, in micro-USDC (1e-6 USDC). Non-streaming calls; on a stream, see below. | | `X-Carpe-Input-Tokens` · `X-Carpe-Output-Tokens` | Token counts the cost was computed from (non-streaming). | | `X-Carpe-Cached-Tokens` · `X-Carpe-Cache-Write-Tokens` · `X-Carpe-Cache-Saved-Usdc-Micro` | Prompt-cache reads and writes, and what the cache saved you. | | `X-Carpe-Available-Credits` | **What you can spend right now** (`escrow − pending − holds`). The number to display and track. | | `X-Carpe-Pending-Credits` | Spent since the last on-chain settlement; already deducted from `available`. | | `X-Carpe-Balance-Credits` | Cumulative on-chain escrow balance — for accounting, not for "remaining credits". | | `X-Carpe-Route-*` | On `/router`: who served (`-Market`), what you paid (`-Cost-Usdc`), what you saved (`-Saved-Usdc`), why (`-Reason`), every market tried (`-Attempts`), pins and fallbacks. See [`POST /router/chat/completions`](#post-router-chat-completions). | | `X-RateLimit-Limit` · `X-RateLimit-Remaining` · `X-RateLimit-Reset` | Your request budget, OpenAI-style. | | `X-Carpe-Lanes-Limit` · `X-Carpe-Lane` | [Parallel lanes](#parallel-lanes-several-agents-on-one-key): how many your credit buys, and the one that served this conversation. | | `Server-Timing` | Where the time went (`pre`, `upstream`, `post`, `total`; `ttft` on a stream). | | `Retry-After` | On `429`, `503` and `409 IDEMPOTENCY_IN_FLIGHT`: seconds to wait. | **On a stream**, the cost arrives as extensions to the standard OpenAI `usage` object, in a final `chat.completion.chunk` written just before the single `data: [DONE]`: ``` data: {"id":"chatcmpl-carpe-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":234,"completion_tokens":187,"total_tokens":421,"carpe_cost_usdc_micro":12345,"carpe_credits":1.2345}} data: [DONE] ``` Clients that stop reading at `[DONE]` (the OpenAI SDKs do) still receive it, and strict parsers ignore the extra fields. It is the stream's only `usage` object. A stream that fails after its headers ends with OpenAI's error envelope instead — see [Errors & retries](#11-errors-retries) — and is not billed. ## 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](https://basescan.org/address/0x15917768b31CB1DC61d9d858f2419BF45044005d) · [Sourcify ✓](https://repo.sourcify.dev/contracts/full_match/8453/0x15917768b31CB1DC61d9d858f2419BF45044005d/) | | ↳ Implementation | `0xA67327207219d84973D0d5B988e902B8e5f8945C` | [BaseScan](https://basescan.org/address/0xA67327207219d84973D0d5B988e902B8e5f8945C) | | USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | [BaseScan](https://basescan.org/address/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) | | DIEM (Venice) | `0xF4d97F2da56e8c3098f3a8D538DB630A2606a024` | [BaseScan](https://basescan.org/address/0xF4d97F2da56e8c3098f3a8D538DB630A2606a024) | TEE attestation (digest ↔ running enclave) is verifiable from the [Privacy page](https://carpe-diem.xyz/privacy). ## 1. Authentication ### POST `/auth/session` Create a wallet session (SIWE). Returns a JWT. - **Auth:** none - **Body:** `{ "wallet": "0x…", "message": "", "signature": "0x…" }` - **200:** `{ "token": "", "wallet": "0x…", "expiresIn": "30m" }` - **Errors:** `400` bad input · `401` AUTH_FAILED (signature mismatch / expired message) · `403` OFAC_BLOCKED ### Email accounts (no wallet) An email account has no wallet: it gets a **derived address with no private key**, which takes the `wallet` slot everywhere (credits, API keys, usage). Its JWT carries `authType: "email"`. Without a private key nothing can be signed, so card-bought credits stay closed usage credits. Sign-in sends **one mail** with a 6-digit code (to type in) and a sign-in link (fallback, same power). Using one closes the other. #### POST `/auth/email/start` - **Auth:** none · **Rate limit:** 5/min per IP, and per address 1 mail/min and 10 mails/hour - **Body:** `{ "email": "you@example.com" }` - **200:** always `{ "ok": true }` — whether or not the account exists, and also when the per-address limit is reached (then nothing is sent). - **Code:** 6 digits, valid **10 min**, single use, **5 wrong attempts** then closed. A new code replaces the previous one. - **Link:** `{APP_BASE_URL}/auth/verify?token=<64 hex>`, valid 15 min, single use. - **Errors:** `400` BAD_REQUEST (invalid email) #### POST `/auth/email/verify-code` - **Auth:** none · **Rate limit:** 10/min per IP - **Body:** `{ "email": "you@example.com", "code": "123456", "deviceLabel"?: "Firefox on Linux" }` — spaces and dashes in the code are ignored; `deviceLabel` (≤ 64 chars) labels the session. - **200:** `{ "token": "", "refreshToken": "", "refreshExpiresAt": "", "wallet": "0x…", "email", "expiresIn": "30m" }` — the account is created on first sign-in. - **Errors:** `400` BAD_REQUEST (malformed email or code; not counted as an attempt) · `400` CODE_MISMATCH `{ "attemptsLeft": 0–4 }` · `410` CODE_EXPIRED (expired, used, replaced, or no code pending for this address) · `429` TOO_MANY_ATTEMPTS (5 wrong attempts: request a new code) · `429` CODE_LOCKED `{ "retryAfterSeconds" }` (20 wrong codes for this address in 24 hours: codes are refused, the link in the email still works) #### POST `/auth/email/verify` The link from the mail (still accepted for mails already sent). - **Auth:** none · **Rate limit:** 20/min per IP - **Body:** `{ "token": "<64 hex>", "deviceLabel"?: "…" }` - **200:** same as `/auth/email/verify-code` - **401:** AUTH_FAILED (unknown, expired or used link) ### POST `/auth/refresh` Two forms. **Email session — refresh token** (body wins over any `Authorization` header): - **Body:** `{ "refreshToken": "" }` - **200:** `{ "token": "", "refreshToken": "", "refreshExpiresAt": "", "wallet", "expiresIn": "30m" }` - Every call **rotates** the refresh token: store the new one, the old one is spent. The session lasts **30 days from sign-in**; rotation never extends it. - **Reuse detection:** presenting a refresh token that was already exchanged revokes the whole session (every token derived from the same sign-in) → `401 REFRESH_REUSED`. Clients must serialise refreshes (one tab at a time). - **401:** REFRESH_REUSED · REFRESH_EXPIRED (30 days elapsed: sign in again) · REFRESH_INVALID (unknown, revoked or malformed) **Wallet session (SIWE) — still-valid JWT:** - **Auth:** Bearer JWT - **200:** `{ "token", "wallet", "expiresIn" }` - **401:** TOKEN_EXPIRED → re-authenticate. Sessions are capped at 8 hours from the original sign-in. · REFRESH_TOKEN_REQUIRED: an email session renews only with its refresh token (so signing out of all devices ends a stolen token within 30 minutes). ### POST `/auth/logout` Ends one email session (revokes its refresh token and every rotation of it). - **Auth:** none · **Body:** `{ "refreshToken": "" }` - **200:** `{ "ok": true }` — also for an unknown token ### POST `/auth/logout-all` Ends every email session of the account. JWTs already issued stay valid until they expire (30 min at most). - **Auth:** Bearer JWT (not an API key) - **200:** `{ "ok": true, "revokedSessions": }` - **Errors:** `401` AUTH_REQUIRED · `403` JWT_REQUIRED ### Re-verification (email sessions) An email session must re-verify with a fresh code before **creating or revoking an API key**, or **deleting the account**. Wallet sessions (SIWE) and API keys are unaffected: the wallet signature already proves control. #### POST `/auth/step-up/start` - **Auth:** Bearer JWT of an email session · **Rate limit:** 5/min per IP, and per address 1 mail/min and 10 mails/hour - **200:** `{ "ok": true, "emailHint": "y***@example.com", "expiresAt": "" }` — the code goes to the account's own address. - **Errors:** `400` STEP_UP_NOT_APPLICABLE (wallet session) · `401` AUTH_REQUIRED · `403` JWT_REQUIRED (API key) · `429` CODE_THROTTLED `{ "retryAfter": }` #### POST `/auth/step-up/verify` - **Auth:** Bearer JWT of an email session · **Rate limit:** 10/min per IP - **Body:** `{ "code": "123456" }` - **200:** `{ "token": "", "wallet", "expiresIn": "10m", "stepUpExpiresAt": "" }` — use this token for the sensitive call(s) within 10 minutes. Refreshing it (either form of `/auth/refresh`) gives an ordinary session token without the re-verification. - **Errors:** same as `/auth/email/verify-code` (CODE_MISMATCH with `attemptsLeft`, CODE_EXPIRED, TOO_MANY_ATTEMPTS, CODE_LOCKED), plus those of `/auth/step-up/start`. A sign-in code is never accepted here. ### POST `/auth/api-keys` Create a persistent API key. **JWT only** (an API key can't mint keys). - **Auth:** Bearer JWT — for an email session, the short token from `/auth/step-up/verify` - **Body:** `{ "name": "optional label" }` - **201:** `{ "id", "key": "cdm_…", "prefix": "cdm_xxxxxxxx…" }` — full key shown once. - **403:** STEP_UP_REQUIRED (email session without a re-verification less than 10 minutes old) · JWT_REQUIRED ### GET `/auth/api-keys` List your keys (no secrets). - **Auth:** Bearer JWT - **200:** `{ "keys": [{ "id", "name", "prefix", "createdAt", "revokedAt", "expiresAt", "rail", "railFallback" }] }` — `expiresAt` (ISO) is set only on the browser-device keys a partner obtains (7-day life); once past, the key is refused even if `revokedAt` is null. `rail` is the key's own payment rail (`auto` | `credits` | `prepaid`, set at creation) or `null` when the key follows your account's rail ([`GET /v1/prepaid/rail`](#get-v1-prepaid-rail)); `railFallback` says whether a request falls through to the other rail when its own cannot pay: the key's own setting, else your account's, and always for `auto`, which is credits first, then prepaid. A partner's key always pays in credits. Absent on older operators. ### 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 — for an email session, the short token from `/auth/step-up/verify` - **200:** `{ "status": "revoked", "id" }` · **404** if not found · **403** STEP_UP_REQUIRED (email session) ### Account deletion Delete your account yourself, at once. **JWT only** (an API key can't delete the account). #### GET `/me/account/deletion` What a deletion would do, to show before confirming. - **Auth:** Bearer JWT · **Rate limit:** 30/min per IP - **200:** `{ "method": "email_code" | "wallet_signature", "email": "a***@example.com" | null, "balanceCredits", "balanceUsd", "activeApiKeys", "partnerDevices", "provider", "requestsInFlight", "deletionPhrase", "domain", "chainId" }` — `provider: true` means the deletion is refused until the wallet's Venice keys are removed; `requestsInFlight: true`, until the running requests finish. #### POST `/me/account/delete` - **Auth:** Bearer JWT — for an email session, the short token from `/auth/step-up/verify` · **Rate limit:** 5/min per IP - **Body:** wallet session: `{ "message", "signature" }`, where `message` is exactly `Delete my Carpe Diem account\nDomain: \nChain ID: \nWallet: \nTimestamp: ` (`domain` and `chainId` as returned by `GET /me/account/deletion`, e.g. `carpe-diem.xyz` and `8453`; nothing before, after or in between), signed (`personal_sign`) by the session's wallet less than 5 minutes ago. Email session: `{}`. - **200:** `{ "ok": true, "closedAt": "", "forfeitedCredits", "revokedApiKeys", "revokedPartnerKeys", "revokedSessions" }` - **Effect:** every API key and session is revoked, including session tokens already issued and the keys a partner app obtained for your devices; the email address and partner links are erased; the remaining credits are forfeited; points and referrals are erased (an invitation the account accepted stays, without its address; points already published on-chain stay there). Signing in again with the same wallet or email opens a new, empty account: purchases, usage history and summaries, routing summaries and drains, API keys and the ledger show nothing from before the deletion. - **Errors:** `400` SIGNATURE_REQUIRED · MESSAGE_INVALID · MESSAGE_EXPIRED · `401` AUTH_REQUIRED · SIGNATURE_INVALID · `403` JWT_REQUIRED · STEP_UP_REQUIRED (email session) · `409` PROVIDER_ACCOUNT (remove your Venice keys on Provide first) · REQUESTS_IN_FLIGHT (a request is still running, such as a video or music generation: finish or cancel it, then try again; checked before the code or the signature) · `503` TEE_NOT_READY (the key vault is still loading after a restart, so a provider wallet cannot be told apart yet: try again in a few minutes; also on `GET /me/account/deletion`) ## 2. Chat & Messages > **Best-price twin.** The core endpoints in this section also exist under `/router/…`, which prices the request across Carpe and the external markets and serves the cheapest — same auth, same body, same response. See [Best-price router](#best-price-router-aggregator) for the exact path mapping. Endpoints *not* in that mapping still answer under `/router` — they are simply always served by Carpe, with no market comparison. ### 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 - **Web search:** off unless you ask for it. `"venice_parameters": { "enable_web_search": "auto" }` (or `"on"`) turns on Venice's web search, and `"enable_web_scraping": true` the reading of the URLs in the last user message. What they retrieve is added to the prompt and counted as input tokens. Each search that actually runs and each page actually read also carries Venice's flat fee of $0.01 (X search on Grok models: $0.01 per result), charged on top with the same dynamic-pricing multiplier as the tokens. - **Errors:** `402` PAYMENT_REQUIRED (insufficient credits) · `400` invalid model/params · `429` rate limited · `502` VENICE_ERROR · `503` no providers / TEE not ready. In OpenAI's envelope: `{ "error": { "message", "type", "code", "param" }, "code" }` (same on `/router/chat/completions`). ### 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 - **Web search:** off unless you ask for it, as on `/v1/chat/completions`: send `venice_parameters` in the body (`extra_body` in Anthropic's SDKs). - **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](https://www.cnbc.com/2026/06/09/anthropic-mythos-claude-fable-5.html), [TechCrunch](https://techcrunch.com/2026/06/09/anthropic-released-claude-fable-5-its-most-powerful-model-publicly-days-after-warning-ai-is-getting-too-dangerous/)). | | **`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](https://docs.venice.ai/guides/features/reasoning-models)) 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 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`). ### GPT-5.6 codename caveat — function tools and `reasoning_effort` `openai-gpt-56-terra` and `openai-gpt-56-luna` reject **function tools combined with any `reasoning_effort` other than `"none"`** on `/v1/chat/completions`. Verified 2026-09-02 against the live upstream; `openai-gpt-56-sol`, the third codename of the same generation, accepts the combination, as do `openai-gpt-55` and every earlier GPT-5.x. | Request | Result | |---|---| | `tools` + `reasoning_effort` absent | `400` — **omitting the parameter does not help**, the upstream applies a non-`"none"` default before validating | | `tools` + `reasoning_effort: "low" \| "medium" \| "high"` | `400 Function tools with reasoning_effort are not supported for gpt-5.6-terra in /v1/chat/completions` | | `tools` + `reasoning_effort: "none"` + `max_tokens` | `400 Unsupported parameter: 'max_tokens' … use 'max_completion_tokens'` — the budget field follows the effort, not the model | | `tools` + `reasoning_effort: "none"` + `max_completion_tokens` | `200`, `finish_reason: "tool_calls"` | | No `tools`, any effort, `max_tokens` | `200` — unaffected | **You do not have to handle this.** The operator rewrites the request for you: when a body carries function tools for an affected model it sets `reasoning_effort: "none"` and renames `max_tokens` to `max_completion_tokens`. It applies on every rail — `/v1/chat/completions`, `/v1/messages`, `/router/…` and the x402 endpoints — because the constraint belongs to the upstream, not to the endpoint you reached it through. When the rewrite happens, the response carries: ``` X-Carpe-Compat: reasoning_effort=none (requested medium; upstream refuses it alongside function tools) ``` That header is the only signal that you asked for reasoning and did not get it. **Tools win over effort** deliberately: an agent whose tool calls fail has nothing to fall back on, while an agent reasoning at `"none"` still completes the task. **To detect it up front**, read `carpe_diem_constraints` from `GET /v1/models` (or `GET /models`): ```json { "id": "openai-gpt-56-terra", "capabilities": { "supportsFunctionCalling": true, "reasoningEffortOptions": ["none", "low", "medium", "high", "xhigh", "max"] }, "carpe_diem_constraints": { "toolsRequireReasoningEffortNone": true } } ``` Note the two blocks disagree, and both are honest: `capabilities` is Venice's, and each of its claims is true *in isolation*. `carpe_diem_constraints` is ours, and it is the only place the *combination* is described. The field is **absent when there is nothing to declare** — never read a negative claim from its absence. **If you need reasoning and tools together**, use `openai-gpt-56-sol` (same generation, no constraint) or any GPT-5.5 / Claude / GLM model. ## 3. Embeddings > **Best-price twin.** The core endpoints in this section also exist under `/router/…`, which prices the request across Carpe and the external markets and serves the cheapest — same auth, same body, same response. See [Best-price router](#best-price-router-aggregator) for the exact path mapping. Endpoints *not* in that mapping still answer under `/router` — they are simply always served by Carpe, with no market comparison. ### 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 ## Web search & scrape (augment) Live web tools (Venice `augment/*`) — used by agent `web_search` / `web_fetch` integrations. ### POST `/v1/augment/search` · POST `/v1/augment/scrape` - **Auth:** API key or JWT - **`/search` body:** `{ "query": "…", … }` (Venice augment/search params passed through) — returns ranked web results. - **`/scrape` body:** `{ "url": "https://…", … }` — returns the page content. - **Billing:** flat per call (`AUGMENT_SEARCH_USD` / `AUGMENT_SCRAPE_USD`) - **200:** the Venice augment payload · **Errors:** `402` PAYMENT_REQUIRED · `400` bad params · `4xx` surfaced from Venice · `502` VENICE_ERROR · `503` no providers ## Decisions (JEV) Venice's **JEV** is a System-1 (non-autoregressive) decision model: instead of generating tokens one at a time, it returns a single typed decision (a probabilistic yes/no, a choice from a set, or a score) in one shot — typically far faster and cheaper than a chat completion for the same "just decide" task (routing, moderation gates, ranking, classification). Carpe proxies it with the same auth, payment rails and per-token billing as chat. ### POST `/v1/decisions` - **Auth:** API key or JWT - **Body:** `{ "model"?, "state", "questions" }` — passed through untouched. `model` is optional and defaults to `jev-latest`. `state` is the context/facts to decide on. `questions` is a **named object** (not an array): each entry declares a `type` — `"noul"` (probabilistic yes/no), `"choice"`, or `"score"` — plus its parameters (`noul` takes `instructions`). ```json { "model": "jev-latest", "state": "The weather report says it is a clear day with a bright blue sky.", "questions": { "skyBlue": { "type": "noul", "instructions": "Decide whether the sky is blue, based on the state." } } } ``` - **200:** the Venice decision payload verbatim — `{ "model", "answers": { "": { "type", "noul": 0.97 } }, "usage": { "input_tokens", "output_tokens" } }`. Each `answers.` mirrors the question's `type`; `noul` is a 0–1 confidence. - **Billing:** per token, off the `usage.input_tokens` / `usage.output_tokens` the decision surface reports (mapped to Carpe's input/output pricing exactly like chat). `jev-latest` is currently a **$0 beta** model upstream, so decisions are served **free** until Venice sets a price (which then applies automatically). - **Errors:** `402` PAYMENT_REQUIRED · `400` INVALID_MODEL (only if the model is absent from the upstream decision catalog) · `429` rate limited · `502` VENICE_ERROR · `503` no providers / TEE not ready - **Discover:** `GET /v1/decisions/models` lists the decision models Carpe can serve (public, mirrors the upstream `?type=decision` catalog). - **Also under `/router`:** `POST /router/decisions` and `GET /router/decisions/models` answer identically. No external market currently serves JEV, so the router always serves it on Carpe (no price comparison) — same auth, body and response. ## 4. Images > **Best-price twin.** The core endpoints in this section also exist under `/router/…`, which prices the request across Carpe and the external markets and serves the cheapest — same auth, same body, same response. See [Best-price router](#best-price-router-aggregator) for the exact path mapping. Endpoints *not* in that mapping still answer under `/router` — they are simply always served by Carpe, with no market comparison. 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"?, "resolution"?, "quality"? }` — `variants` 1–4 (default 1), billed per image - **Price = the tier rendered.** A model with a resolution × quality price matrix (`gpt-image-2`, `ideogram-v4-5`, `nano-banana-*`, …) is billed at the tier Venice renders: the `resolution` (`"1K"`, `"2K"`, `"4K"`; `size` is read in its place when it carries one of these names) and `quality` (`"low"`, `"medium"`, `"high"`) you ask, when the model publishes them in its `constraints`, else the model's `defaultResolution` / `defaultQuality` — which is what Venice renders and bills when a request names none (`gpt-image-2`: 1K/high). The tier priced is forwarded to Venice, so it is the tier rendered; a value the model does not publish (an OpenAI `"1024x1024"`) names no tier. - **200:** Venice image payload (base64-encoded image(s)) - **Errors:** `402` PAYMENT_REQUIRED · `400` invalid/missing prompt (max 10,000 chars) · `400` INVALID_MODEL (no price is known for this model — see `GET /v1/models`) · `502` VENICE_ERROR · `503` no providers / PRICE_UNAVAILABLE ### 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. > **No `variants` on the async path.** It returns a single binary image, so `variants > 1` is rejected with `400 VARIANTS_NOT_SUPPORTED` rather than billed. Use the synchronous `POST /v1/image/generate` when you want multiple variants. - **POST `/v1/image/generate/queue`** — **Auth:** 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/retrieve`** — **Body:** `{ "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/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. ```bash 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": "", "aspect_ratio"?, "resolution"?, "quality"? }` - **`image` format:** data URI — `data:image/;base64,<…>` (e.g. `image/png`, `image/jpeg`, `image/webp`). HTTP URLs are **not** fetched server-side. - **Price.** With no `resolution` / `quality`, an edit costs the model's per-edit price (its default tier). Name one the model publishes (`gpt-image-2-edit`: `"4K"`, `"low"`…) and it is billed on that tier — `size` is read in place of `resolution` when it carries a tier name — and the tier is forwarded in Venice's spelling. A value the model does not publish is forwarded as is, for Venice to accept or refuse; it is not priced. - **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/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) · `413` image > 5 MB · `503` QUEUE_FULL · `503` no 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:** `403` other wallet · `404` JOB_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/multi-edit` → `/multi-edit/queue` → `/multi-edit/retrieve` (async) Compose/edit from **1–3 source images** and a prompt (Venice `image/multi-edit`). - **Auth:** API key or JWT - **Body:** `{ "model", "prompt", "images": ["", …], "aspect_ratio"?, "output_format"?, "resolution"?, "quality"?, "safe_mode"? }` - **`images` format:** array of 1–3 items, each a data URI **or** raw base64, ≤ 5 MB each. - **Price:** as `/v1/image/edit`, plus Venice's fee for each input image beyond the first on the models that charge one (`flux-2-max-edit`: 3 images = $0.12 + 2 × $0.0345 before the multiplier). - **200:** edited image payload (`{ "images": [""], "format": "png" }`) · **Errors:** `400` (bad prompt/images) · `413` image > 5 MB · `409` `MODEL_REQUIRES_ASYNC` → use `/v1/image/multi-edit/queue` · `503` no providers / no capacity - **Async:** `/v1/image/multi-edit/queue` (→ `202 { queue_id }`) then `/v1/image/multi-edit/retrieve` — same poll/binary contract as `/v1/image/edit/queue`. ### POST `/v1/image/upscale` Increase resolution. - **Auth:** API key or JWT - **Body:** `{ "model": "upscaler", "image": "", "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. - **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 > **Best-price twin.** The core endpoints in this section also exist under `/router/…`, which prices the request across Carpe and the external markets and serves the cheapest — same auth, same body, same response. See [Best-price router](#best-price-router-aggregator) for the exact path mapping. Endpoints *not* in that mapping still answer under `/router` — they are simply always served by Carpe, with no market comparison. ### POST `/v1/audio/speech` Text-to-speech (OpenAI-compatible). - **Auth:** API key or JWT - **Body:** `{ "model", "input", "voice"?, "response_format"?, "streaming"? }` — `input` 1–4,096 chars, the provider's own limit: split a longer text into several requests. Past it the answer is `400 BAD_REQUEST`, before anything is held (on `/v1/x402/audio/speech`, before the payment challenge). `/router/audio/speech` sends a longer text to an external market when one offers the model, and answers the same `400` otherwise. - **`streaming: true`** streams the audio as the provider renders it (chunked, no `Content-Length`) instead of answering once the whole file is ready, so playback can start on the first bytes. Same price, by input characters, but charged when the render is over: - **Render completes**, or **you hang up** mid-stream (the provider has rendered and paid for it either way): charged. - **The provider breaks off** mid-stream (error, no data for 90 s, or the 10-minute ceiling): **not charged**. The connection is dropped instead of the body ending cleanly, so a cut-off file never looks whole — treat a stream that ends with a network error as failed. - **No audio at all** (the provider ends the stream before the first byte): `502 VENICE_ERROR`, not charged. - `X-Carpe-Cost-Usdc-Micro` is sent with the first bytes, before the outcome is known: it is what the render costs if it completes, and nothing is charged if the provider breaks off. The `X-Carpe-*-Credits` headers are a snapshot taken at the same moment, with this request still on hold. - **Which models stream:** `tts-xai-v1` (`mp3`, `pcm`, `wav`), `tts-gradium-v1` (`wav`, `pcm`, `opus`) and `tts-kokoro` (`mp3`, `pcm`, `wav`, `opus`, `aac`) — the models the provider really streams, measured on 2026-10-10 (first audio after about 0.3–2 s instead of the whole render). The other models accept the flag but send nothing before the render is done, so they are not worth streaming. A streamed `wav` may declare an unknown length in its header (`0xFFFFFFFF` or `0x7FFFFFFF`), which players read as "until the end of the stream". Any other model or format asking for `streaming: true` is answered **buffered**, exactly as without the flag (same body, with `Content-Length`). So is a request the provider turns down with streaming: it is retried once without it, at no extra cost. Every response to `streaming: true` carries **`X-Carpe-Audio-Streaming: streamed`** or **`buffered`**. - **`/v1/audio/speech` only.** `/router/audio/speech` and `/v1/x402/audio/speech` accept the field and ignore it: they always answer buffered (with `Content-Length`, without `X-Carpe-Audio-Streaming`). - **`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/voices` Voice cloning — upload a short reference sample, get a handle to speak with. **`multipart/form-data`.** - **Auth:** API key or JWT · a **funded** account (checked like any paid call, never charged) - **Body (multipart):** `file` (one speaker, at least `voice_cloning.min_sample_seconds`, in one of `voice_cloning.accepted_formats`) + `model` — a TTS model whose `constraints.voice_cloning` is set in `GET /v1/models` (today `tts-chatterbox-hd`) + `consent=true`: the voice is yours, or its speaker agreed to it being cloned. Anything but `true` is refused; the field becomes required (`400 VOICE_CONSENT_REQUIRED` without it) once Sub Rosa ships it — send it now. - **200:** `{ "id": "vv_…", "model", "expires_at" }` — pass `id` as `voice` to `POST /v1/audio/speech` with the **same** `model`. The handle is opaque: Venice's own identifier stays inside the TEE. - **The handle is yours and short-lived.** It is spoken only for the account that uploaded the sample, on `/v1/audio/speech` only (`/router` and x402 refuse it with `VOICE_HANDLE_V1_ONLY`), and it expires after `voice_cloning.retention_days`. Keep the sample: an expired or unknown handle answers `404 VOICE_EXPIRED`, and uploading again gives a new one. `503 VOICE_TEMPORARILY_UNAVAILABLE` means the provider holding the voice is short of capacity — retry later, do **not** upload again. - **Retention:** Venice keeps the sample for `voice_cloning.retention_days` (7) on its provider's storage, and nothing can delete it sooner. Carpe Diem keeps no copy (see [Privacy](/privacy#cloned-voices)). - **Limits:** 5 uploads a minute, 10 a day and 20 active voices per wallet — past them `429 ENDPOINT_RATE_LIMITED`, `429 VOICE_DAILY_LIMIT`, `409 VOICE_LIMIT`. - **Billing:** the upload is free; speech with the handle is billed like any other. A model that makes one container only (`tts-chatterbox-hd`: `wav`) publishes `supported_formats` / `default_format`, and speech without `response_format` uses that default. - **Errors:** `400` `VOICE_CLONING_UNSUPPORTED` (model does not clone) · `400` `VOICE_MODEL_MISMATCH` (handle used with another model) · `404` `VOICE_EXPIRED` · `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). This is also where **sound effects** and the speech models Venice serves off the music pipeline live: the endpoint follows the model's `carpe_diem_type`, not what it sounds like. A music id sent to `/v1/audio/speech` is refused by the upstream, which only serves `carpe_diem_type: "tts"`. - **queue** — **Auth:** API key or JWT · **Body:** `{ "model", "prompt", "lyrics_prompt"?, "duration_seconds"?, "force_instrumental"?, "voice"?, "speed"? }` · **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) Semantics: `prompt` describes the musical style; `lyrics_prompt` carries the sung lyrics. On a speaking model (`elevenlabs-tts-v4`, `elevenlabs-tts-v4-turbo`, `elevenlabs-tts-v3`, `elevenlabs-tts-multilingual-v2`, `seed-audio-1-0`) `prompt` is the text read aloud, `voice` picks one of the model's `voices`, and the job is billed per thousand characters (per second on `seed-audio-1-0`). **What a job costs** is fixed when it is queued, from the same rules Venice quotes with (`POST /audio/quote`): characters of the `prompt` counted in blocks of 100, seconds of `duration_seconds` — or the model's `default_duration` when you send none, since that is the length Venice renders — and every computed price rounded **up to the cent**, before Carpe Diem's multiplier. A 5-character line on `elevenlabs-tts-v3` therefore costs the same as a 100-character one. `duration_seconds` is a number or a string of digits (`"600"` is read, priced and sent as `600`, as Venice reads it); any other string (`"600.0"`, `"10m"`) is refused with `400 MUSIC_PARAM_REJECTED`. **Read every parameter off the catalogue, not off this page.** Every music model in `GET /v1/models` carries a `constraints` block — its own words, refreshed hourly — and the music bucket is heterogeneous enough that a value from the wrong model is a `400`: | `constraints` field | What it decides | |---|---| | `duration_options` | The exact lengths accepted. `ace-step-15` takes 60/90/120/150/180/210 and nothing between. | | `min_duration` / `max_duration` | The accepted range, when the model publishes bounds instead of a list (3–600s on `elevenlabs-music`, 1–22s on `elevenlabs-sound-effects-v2`). | | *neither, and no `default_duration`* | **The model takes no `duration_seconds` at all** — sending one is a `400`. This is the case on the `minimax-music-*` line and `lyria-3-pro`. | | `supports_lyrics` · `lyrics_required` · `lyrics_character_limit` | Whether `lyrics_prompt` is forbidden, required or optional, and how long it may be. | | `supports_force_instrumental` | Whether `force_instrumental` may be sent **at all**. Only 3 of the 12 models accept the key, and sending it with `false` is refused just the same — it is an unknown field upstream, not a no-op. | | `min_prompt_length` · `prompt_character_limit` | The prompt window. 10–512 on `ace-step-15`, 10–300 on `minimax-music-v2`. | | `supports_speed` · `min_speed` / `max_speed` | Whether `speed` may be sent, and its bounds. | | `voices` · `default_voice` | The voices a speaking model accepts in `voice`, and the one it uses when none is sent. | | `supports_custom_voice_id` | `voice` also accepts an ElevenLabs Voice ID, not only a listed name. Use a public or premade voice: the job runs on a provider's key, not on your ElevenLabs account, so a voice from your own library will not resolve. | | `supported_formats` · `default_format` | The container the finished track arrives in — `flac`, `mp3`, `wav` or `m4a`. Name the download accordingly. | A request that breaks one of those published limits is refused **before** it is priced or queued, on all three rails (credits, `/router`, x402): ```json { "error": "This model does not accept these values — duration_seconds: 30s is not one of this model's lengths (accepts 60, 90, 120, 150, 180, 210)", "code": "MUSIC_PARAM_REJECTED", "model": "ace-step-15", "details": { "issues": [{ "param": "duration_seconds", "reason": "…", "accepted": ["60", "90", "…"] }] } } ``` Every refused field is named in one response, so a two-mistake payload takes one round trip to fix rather than two. Anything the catalogue does not publish is still the upstream's call and comes back as its `400`, with its own `details.issues` attached. ## 6. Video (async) > **Best-price twin.** The core endpoints in this section also exist under `/router/…`, which prices the request across Carpe and the external markets and serves the cheapest — same auth, same body, same response. See [Best-price router](#best-price-router-aggregator) for the exact path mapping. Endpoints *not* in that mapping still answer under `/router` — they are simply always served by Carpe, with no market comparison. Video takes minutes, so it's asynchronous: (optionally) quote, queue, poll retrieve, fetch file. ### POST `/v1/video/quote` Price before committing — *optional but recommended* (queue prices the job itself and runs its own balance check anyway). Venice's free quote, converted to what Carpe charges. - **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). Use it to validate an unfamiliar `duration` / `aspect_ratio` / param set **without being charged**. - **A price here means the payload passed parameter validation.** Quote and queue run the same pre-flight against the model's published `constraints` (see `GET /v1/models`), so a quote can no longer price a render that queue would refuse. Anything the catalogue doesn't publish is still Venice's call, and comes back as its `400`. - **200:** `{ "cost_usdc", "quote", "venice_quote_usd", "multiplier" }` — `cost_usdc` (repeated as `quote`) is what `/v1/video/queue` charges for this payload at the current price: Venice's own quote × the dynamic multiplier, the same as every fixed-cost job. `venice_quote_usd` is Venice's figure — what the provider's key is debited. The multiplier moves with demand, so the charge at queue time can differ slightly. - **Errors:** `400` VIDEO_PARAM_REJECTED (a value the model's published enum excludes — carries `details.issues`) · `400` (Venice rejected a field we can't check) · `503` PRICE_UNAVAILABLE (Venice answered without a price — retry) · `502` VENICE_ERROR ### POST `/v1/video/queue` Start a job. Runs a balance check and refuses if you can't afford it. - **Priced on Venice's own quote for this payload.** Before anything is held, the operator asks Venice's `/video/quote` for the job — the model and its priced fields (`duration`, `aspect_ratio`, `resolution`, `audio`, the enhancement fields, `video_url`, `reference_video_total_duration`) — and charges that × the current multiplier when Venice accepts the job. A payload Venice refuses to price comes back here as its `4xx`, before any hold; a job Venice cannot price right now is refused with `503 PRICE_UNAVAILABLE`, nothing held — never billed on a guess. - **Auth:** API key or JWT - **Body:** `{ "model", "prompt", "duration", "aspect_ratio"?, ...image params }` - **Read the accepted values off the catalogue, not this page.** Every video model in `GET /v1/models` carries a `constraints` block with its own `durations`, `aspect_ratios`, `resolutions` and `prompt_character_limit` — that is the authoritative, always-current source, and what a picker should be built from. **No video model publishes nothing**: an id the upstream serves without publishing is measured against its validator directly and re-verified at every catalogue sync (`scripts/probe_unlisted_video.mjs`). The per-family table below is a hand-maintained snapshot kept for orientation, and it is coarser than the catalogue in both directions — it groups models that differ, and it has been wrong. The Seedance families accept every whole second in their range (`7s`, `9s`, `11s`, `13s`/`14s` on the `2-0` line, and every second up to `30s` on `2-5`, none of which appear below), they accept `21:9`, and their `resolutions` differ **per model** — `seedance-2-0*` reaches `4k` while `seedance-2-0-fast*`, `-mini*` and `seedance-2-5*` stop at `720p`. Build from `constraints`. **Model ids carry a tier suffix.** Venice publishes the whole Seedance family as `*-basic` — `seedance-2-5-reference-to-video-basic`, not `seedance-2-5-reference-to-video`. The older spellings still reach the same models upstream, but the catalogue lists each model once, under the id Venice publishes. Match on the catalogue id. - **`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"`): | Family | Accepted `duration` | | --- | --- | | `sora-*` | `"4s"` `"8s"` `"12s"` `"16s"` | | `veo3*` | `"4s"` `"6s"` `"8s"` | | `wan-*` | `"5s"` `"10s"` | | `kling-*` | `"5s"` `"10s"` | | `seedance-2-5*` | every second `"4s"`–`"30s"` | | `seedance-2-0*` (incl. `-fast`, `-mini`) | every second `"4s"`–`"15s"` | | `seedance-1-5*` | every second `"4s"`–`"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"` | - **Source-matched `duration` and `aspect_ratio` (Seedance reference-to-video).** On an *edit* or *extend* job you can ask the output to follow the source clip instead of naming a size or a length: | Field | Values | Behaviour | | --- | --- | --- | | `aspect_ratio` | `"adaptive"` or `"auto"` | output ratio matches the source video (Seedance 2.0 and 2.5 R2V) | | `duration` | `"auto"` or `"-1"` | output length matches the source video (Seedance **2.5** R2V edit; source must be 4–30 s) | Neither value appears in `constraints`, and the pre-flight lets both through on purpose — the published enum is a floor of what works, not a ceiling. Both **require `reference_video_urls`** on queue, and `reference_video_total_duration` on quote; a source-matched duration bills `ceil(reference_video_total_duration)` seconds. The two are independent — match one without the other. - **`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-*`; `seedance*` also takes `"21:9"`. Omit to let Venice pick a sensible default. On an **image-to-video** model the parameter is usually meaningless — the source image dictates the frame — and the catalogue says so with `aspect_ratios: []`. Hide the control when the list is empty rather than sending a value that does nothing. - **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. - **Reference conditioning (reference-to-video).** A `*-reference-to-video` model does not take a first frame — it takes a **set** of references it keeps consistent across the shot (a character, a product, a style). Reference them from the prompt where the model supports it. - **`reference_image_urls`** *(string[])* — reference images for character / style consistency. URLs or `data:` URIs. - **`reference_video_urls`** *(string[])* — reference clips, to inherit subject motion and camera movement, or to be edited / extended / stitched. ≤50 MB each. - **`reference_audio_urls`** *(string[])* — voice / SFX donors. Must be paired with at least one reference image or video — audio-only is rejected upstream. ≤15 MB each. - **`reference_video_total_duration`** *(number, seconds)* — the sum of every reference clip's length. Pass it on **quote and on queue** whenever reference clips are present: the job is priced on it. Required for a source-matched `duration`. - **The counts and clip lengths are per FAMILY**, and Venice publishes none of them — they come from its Seedance guide: | | Seedance 2.0 (incl. `-fast`, `-mini`) | Seedance 2.5 | | --- | --- | --- | | `reference_image_urls` | 1–9 | 1–30 | | `reference_video_urls` | ≤ 3 | ≤ 10 | | `reference_audio_urls` | ≤ 3 | ≤ 10 | | per reference clip | `[2, 15]` s | `[2, 30]` s | | all clips combined | ≤ 15 s | ≤ 30 s | | output duration | 4–15 s | 4–30 s | Shared floors, published per model in `constraints`: reference images need a short side ≥ `reference_image_min_short_side_pixels` (300) and an aspect ratio strictly inside (`reference_image_min_aspect_ratio`, `reference_image_max_aspect_ratio`) — `(0.4, 2.5)`. Video containers `.mp4` / `.mov` (H.264 / H.265); audio `.wav` / `.mp3`. - **`elements`** *(object[], max 4)* — advanced element control (e.g. Kling O3 R2V): `{ frontal_image_url, reference_image_urls (max 3), video_url }`, addressed in the prompt as `@Element1`, `@Element2`… - **`scene_image_urls`** *(string[], max 4)* — scene references, addressed in the prompt as `@Image1`, `@Image2`… - Which of these a given model accepts is **not published in the catalogue** — `constraints` covers `durations` / `aspect_ratios` / `resolutions`, not the conditioning fields. `POST /v1/video/quote` is the authority for the rest: it validates the exact payload for free and names the field it rejects. - **Seedance reference-to-video: one model, four workflows, routed by the PROMPT.** There is no `task` or `workflow` field. `seedance-*-reference-to-video-basic` performs four different jobs and infers which one from the shape of your prompt: | Workflow | What it does | Prompt shape | | --- | --- | --- | | **Reference** | a new video, using your files as donors for subject / motion / style / voice | `Refer to in to generate …` | | **Edit** | change one thing in a clip; anything unnamed is preserved | `Strictly edit