Quick start
- Open the app and sign in with your wallet.
- Make sure you have credits. Deposit into a vault to earn them, or buy them with a card.
- Go to Developers, then Create key. Copy the key; it's shown once.
- Point any OpenAI SDK at
https://api.raeto.cc/v1with your key.
from openai import OpenAI
client = OpenAI(
base_url="https://api.raeto.cc/v1",
api_key="YOUR_RAETO_KEY",
)
reply = client.chat.completions.create(
model="openai/gpt-4o-mini",
messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.raeto.cc/v1",
apiKey: "YOUR_RAETO_KEY",
});
const reply = await client.chat.completions.create({
model: "openai/gpt-4o-mini",
messages: [{ role: "user", content: "Hello" }],
});
console.log(reply.choices[0].message.content);
curl https://api.raeto.cc/v1/chat/completions \ -H "Authorization: Bearer YOUR_RAETO_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}]}'
https://api.raeto.cc/v1. The API lives on the api. subdomain; raeto.cc is the website. Opening the base URL in a browser shows a short guide; requests go to paths under it, like /v1/models and /v1/chat/completions.Anything that speaks the OpenAI chat format works the same way: LangChain, the Vercel AI SDK, LlamaIndex, Cursor-style tools that take a custom base URL, and so on.
API keys
Create keys in the app under Developers. Give each app or environment its own key so you can see what each one spends and shut one off without touching the others.
| Setting | What it does |
|---|---|
| Name | A label for you, like "Discord bot" or "staging". It shows in the activity log. |
| Spending limit | Optional. Once the key has spent this much in credits, it's turned away with key_limit_reached until you raise or remove the limit. It's checked before each request, so a final request can finish slightly past it. |
| Allowed models | Optional. A list of model ids the key may use, such as openai/gpt-4o-mini or raeto/auto. Anything else returns model_not_allowed. |
| Pause | Turns the key off right away without deleting it. Resume it any time. |
| Delete | Revokes the key for good. |
Each wallet can have up to 20 keys. Keys are stored only as hashes, so we can't show one again. If you lose a key, delete it and make another.
Models
Use any model id from the list endpoint. It returns 400+ models from OpenAI, Anthropic, Google, Meta, DeepSeek, Mistral and more, with their per-token prices.
curl https://api.raeto.cc/v1/modelsAuto routing
Set model to raeto/auto and our routing engine picks the cheapest model that can handle each request: quick questions go to a fast model, code to a stronger one, and hard reasoning to the top tier. That's how credits go up to 2–3x further.
Auto routing is for wallets holding at least 1,000 $RAETO. The response tells you what was picked in the x-raeto-model and x-raeto-tier headers. To make auto start from a stronger tier, send x-route-floor: mid or top.
Using your own provider account instead? See Routing only.
Routing only NEW
Already have your own OpenAI, Anthropic or OpenRouter account? Keep it. Send Raeto the request you're about to make, and we tell you which model to use. Then you call that model yourself, with your own key. Raeto never sees your provider key or the model's reply.
Each routing decision costs a flat $0.0002 from your Raeto balance ($0.20 per 1,000 requests), whatever the request's size. It works with any developer key, and there's no $RAETO requirement.
import OpenAI from "openai"; import { Raeto } from "./raeto.mjs"; const raeto = new Raeto({ apiKey: process.env.RAETO_API_KEY }); const openai = new OpenAI(); // your own provider key const request = { messages: [{ role: "user", content: "Summarize this ticket in one line" }] }; const pick = await raeto.route({ ...request, models: { fast: "gpt-4o-mini", mid: "gpt-4.1", top: "o3" } }); const reply = await openai.chat.completions.create({ ...request, model: pick.model }); // pick.tier = "fast", pick.reasons = ["simple request"]
import os from openai import OpenAI from raeto import Raeto raeto = Raeto(api_key=os.environ["RAETO_API_KEY"]) openai = OpenAI() # your own provider key messages = [{"role": "user", "content": "Summarize this ticket in one line"}] pick = raeto.route(messages=messages, models={"fast": "gpt-4o-mini", "mid": "gpt-4.1", "top": "o3"}) reply = openai.chat.completions.create(model=pick["model"], messages=messages)
curl https://api.raeto.cc/v1/route \ -H "Authorization: Bearer $RAETO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"messages":[{"role":"user","content":"Summarize this ticket in one line"}], "models":{"fast":"gpt-4o-mini","mid":"gpt-4.1","top":"o3"}}'
Request
| Field | Meaning |
|---|---|
messages | Required. The chat you're about to send. Also send tools or images if the real request has them; they affect the pick. |
models | Optional. Your own model name for each tier: fast, mid, top. Leave out a tier and the next stronger one you gave is used. Without models, you get Raeto's picks as OpenRouter ids. |
floor | Optional. mid or top to never go below that tier. |
max_tokens | Optional. Only used for the cost estimate. |
Response
| Field | Meaning |
|---|---|
model | The model to call. |
tier, reasons | Which tier was picked, and why (for example code or long input). |
fallbacks | Stronger models to try, in order, if the pick fails or the answer isn't good enough. |
estimate | Only with Raeto's model list: estimated cost of this request, the top model's cost and the percent saved. |
fee_usd, balance_usd | What this decision cost, and your balance after it. |
Routing decisions show up in Developers → Activity as route → model. Key spending limits apply to them too.
Overview, savings calculator and pricing: raeto.cc/routing.
Requests
POST /v1/chat/completions takes the standard OpenAI body. Streaming ("stream": true), tools, JSON mode, images and other provider options pass straight through to the model.
Response headers
| Header | Meaning |
|---|---|
x-raeto-model | The model that answered. |
x-raeto-tier | fast, mid or top. Only when auto routing chose the model. |
x-raeto-cost | What this request took from your credits, in dollars. Non-streaming requests only; streamed requests show their cost in the activity log. |
Who am I
GET /v1/me returns what the key can still spend (available_usd), plus its name and limit. Called with a Sign in with Raeto token, it returns the user's user_id for your app instead of their wallet.
SDKs
The OpenAI SDKs above work as they are. If you'd rather have something smaller, our clients for JavaScript and Python have no dependencies, and they include helpers for Sign in with Raeto. Download one file and drop it into your project:
import { Raeto } from "./raeto.mjs";
const raeto = new Raeto({ apiKey: process.env.RAETO_API_KEY });
console.log(await raeto.ask("Explain Morpho in one line", { model: "openai/gpt-4o-mini" }));
console.log(raeto.last); // { model, tier, costUsd }
for await (const t of raeto.stream({ messages: [{ role: "user", content: "Count to 5" }] }))
process.stdout.write(t);
const me = await raeto.me(); // { available_usd, ... }
import os
from raeto import Raeto
raeto = Raeto(api_key=os.environ["RAETO_API_KEY"])
print(raeto.ask("Explain Morpho in one line", model="openai/gpt-4o-mini"))
print(raeto.last) # {"model", "tier", "cost_usd"}
for t in raeto.stream(messages=[{"role": "user", "content": "Count to 5"}]):
print(t, end="")
me = raeto.me() # {"available_usd", ...}
Both raise RaetoError with status and type from the error table. The JavaScript client runs in Node 18+, Deno, Bun and browsers; the Python one needs 3.8+.
Agents (MCP)
Raeto runs a remote MCP server, so Claude, Cursor and other agents can call any model with your credits: for a second opinion, to hand off to a cheaper model, or to reach one they don't have.
| Setting | Value |
|---|---|
| URL | https://api.raeto.cc/mcp |
| Transport | Streamable HTTP |
| Auth | Header Authorization: Bearer YOUR_RAETO_KEY |
claude mcp add --transport http raeto https://api.raeto.cc/mcp \ --header "Authorization: Bearer YOUR_RAETO_KEY"
// ~/.cursor/mcp.json { "mcpServers": { "raeto": { "url": "https://api.raeto.cc/mcp", "headers": { "Authorization": "Bearer YOUR_RAETO_KEY" } } } }
// Settings → Developer → Edit Config (claude_desktop_config.json). Needs Node.js. { "mcpServers": { "raeto": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.raeto.cc/mcp", "--header", "Authorization:${RAETO_AUTH}"], "env": { "RAETO_AUTH": "Bearer YOUR_RAETO_KEY" } } } }
Tools
| Tool | What it does |
|---|---|
ask_model | Sends a prompt (and optional system message) to a model and returns the reply, with the model used and what it cost. Leave model empty or use raeto/auto for auto routing. |
list_models | Searches models by name, with prices per million tokens. |
get_balance | How many credits the key can still spend. |
Sign in with Raeto
Add a "Sign in with Raeto" button to your app. Your users approve it once with their wallet, choose how much your app may spend, and from then on your app calls models with their credits. You never handle API keys or payments for them.
It's standard OAuth 2.0 (authorization code with PKCE), so most OAuth libraries work.
- In the app, go to Developers → Sign in with Raeto → Create an app. Add your redirect URL. Server apps get a client secret; browser and mobile apps use PKCE only.
- Send the user to
https://raeto.xyz/authorizewithresponse_type=code,client_id,redirect_uri,state,code_challengeandcode_challenge_method=S256. - They come back to your redirect URL with
?code=…&state=…. Check thatstatematches. - Swap the code at
POST https://api.raeto.cc/oauth/tokenfor an access token, then use it exactly like an API key.
import { Raeto, createPkce, authorizeUrl, exchangeCode } from "./raeto.mjs";
app.get("/login", async (req, res) => {
const { verifier, challenge } = await createPkce();
req.session.verifier = verifier;
req.session.state = crypto.randomUUID();
res.redirect(authorizeUrl({ clientId: CLIENT_ID, redirectUri: REDIRECT_URI, state: req.session.state, codeChallenge: challenge }));
});
app.get("/callback", async (req, res) => {
if (req.query.state !== req.session.state) return res.status(400).send("Bad state");
if (req.query.error) return res.send("Sign-in cancelled");
const t = await exchangeCode({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET, code: req.query.code,
codeVerifier: req.session.verifier, redirectUri: REDIRECT_URI });
req.session.raeto = t.access_token; // keep it server-side
const raeto = new Raeto({ apiKey: t.access_token });
res.send(await raeto.ask("Write a welcome line for " + t.user_id));
});
from raeto import Raeto, create_pkce, authorize_url, exchange_code verifier, challenge = create_pkce() # store verifier + state in the session login_link = authorize_url(CLIENT_ID, REDIRECT_URI, state, challenge) # on your redirect URL: t = exchange_code(CLIENT_ID, code, verifier, REDIRECT_URI, client_secret=CLIENT_SECRET) raeto = Raeto(api_key=t["access_token"]) print(raeto.ask("Hello"))
curl -X POST https://api.raeto.cc/oauth/token \ -d grant_type=authorization_code \ -d code=CODE_FROM_REDIRECT \ -d redirect_uri=https://yourapp.com/callback \ -d client_id=raeto_app_… \ -d client_secret=rs_… \ -d code_verifier=YOUR_PKCE_VERIFIER # → {"access_token": "rat-…", "token_type": "Bearer", "user_id": "u_…", "limit_usd": 10}
What your app gets
- An access token that works on
/v1/chat/completions,/v1/models,/v1/meand the MCP server, up to the limit the user chose. - A
user_idthat stays the same for that user in your app. It's different in every app, and apps never see the user's wallet address.
What users control
- They set your app's spending limit when they sign in, and can change it later.
- They can disconnect your app any time under Developers → Connected apps. The token stops working at once.
- Signing in again replaces the old token.
Rules
- PKCE with
S256is required for every app. Redirect URLs must match exactly and usehttps://(orhttp://localhostwhile testing). - Codes last 10 minutes and work once. If a code is used twice, the token it produced is revoked.
- Keep client secrets and access tokens on your server. Deleting an app signs every user out of it.
- Token errors use OAuth's shape:
{"error": "invalid_grant", "error_description": "…"}.
Billing
Every request is paid from your Raeto credit balance. You're charged for the tokens the model actually used, at the provider's price plus Raeto's service fee. Routing-only decisions cost a flat fee each instead, since no model runs on our side. Every charge shows up in Developers → Activity, with tokens in and out, cost, time taken, and which key made it.
Credits come from interest your vault deposits earn, credits you buy with USDG or a card, and rCREDIT you use. When your balance runs out, requests return insufficient_balance until you top up.
Errors
Errors use the OpenAI shape, {"error": {"message", "type", "code"}}, so existing SDK error handling keeps working. Check type to tell them apart.
| Status | Type | What to do |
|---|---|---|
| 401 | authentication_error | The key is wrong or was deleted. |
| 401 | key_disabled | The key is paused. Resume it in the app. |
| 402 | insufficient_balance | Out of credits. Deposit or buy more. |
| 402 | key_limit_reached | This key hit its spending limit. Raise or remove it. |
| 403 | model_not_allowed | The key isn't allowed to use that model. |
| 403 | auto_routing_locked | Auto routing needs 1,000 $RAETO in the key's wallet. Name a model instead. |
| 429 | rate_limit_error | Too many requests. Back off and retry. |
| 502 | upstream_error | The model provider didn't answer. Retry, or try another model. You aren't charged. |
Rate limits
Each wallet can make up to 120 requests a minute across all its keys. Need more? Reach out on X at @RaetoCC.