1. Get a key#
From your dashboard. It is shaped sk_soba_<id>.<secret>, shown once, and it is the
only credential your server needs.
Terminal
export SOBA_KEY="sk_soba_…"
2. Change the base URL#
JavaScript
import OpenAI from "openai";
const soba = new OpenAI({
baseURL: "https://api.soba.so/v1", // the only line that changes
apiKey: process.env.SOBA_KEY,
});
Everything else stays: your prompts, your tools, your streaming, your error handling.
3. Pass the user's id#
JavaScript
const response = await soba.chat.completions.create({
messages: [{ role: "user", content: "What is Soba?" }],
model: "auto",
user: session.user.id, // ← the one addition
});
user is a hint in the OpenAI API and load-bearing here. It is how a run is
attributed to a person, and therefore how Soba knows whose machine it may reach.
4. Prove it#
Terminal
curl https://api.soba.so/v1/chat/completions \
-H "Authorization: Bearer $SOBA_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "soba/echo", "user": "usr_123",
"messages": [{ "role": "user", "content": "ping" }] }'
soba/echo is answered by Soba itself. No machine, no provider key, nothing billed,
and it still proves the three things that actually break: the key, the base URL and
the user field.
You are done with the required part
Every run is now metered per user and bounded by a spend ceiling. A user with nothing
connected is handed back to the client you already have,
so nothing changes for them yet. Everything below is optional.
5. Let users bring their own AI#
JavaScript
<SobaProvider userId={session.user.id}>
<ConnectCompute />
</SobaProvider>
Drop that in your settings page. It is the connect step for someone with nothing
paired, and the status of their machines once they have: asleep, offline or serving
a run. To place those two apart, pass showStatus={false} and render
<ComputeStatus /> yourself. A local model or a key of their own starts serving
runs immediately, with no change at your call site. Their Claude or ChatGPT plan is
served by Claude Code or Codex, which chat/completions cannot drive; that is what
step 6 is for. See Components and
Pairing.
6. Reach Claude Code and Codex#
chat/completions is stateless and the caller owns the loop, so it reaches models.
Reaching a full agent runtime means letting Soba own the loop:
JavaScript
import { Soba } from "@soba-so/sdk";
const soba = new Soba({ apiKey: process.env.SOBA_KEY });
const run = await soba.run({
user: session.user.id,
model: "auto",
prompt: "Summarise everything I saved this week",
tools: [listNotes, readNote], // your code, executed in your app
});
for await (const event of run) {
if (event.type === "delta") process.stdout.write(event.text);
if (event.type === "done") console.log(event.usage);
}
See Runs and the SDK.
7. Charge for it#
A plan sets the price, the allowance, what happens past it, which
compute it may use, and what a user earns for connecting their own. It compiles to a
route on every run, so pricing and routing stay one decision.