Auth for LLMs
Never worry about billing your users for AI again
One connect step gives every user their own AI access: a subscription, a local model or a key. Soba meters what each one uses and charges them on your plan, at your price.
Users who connect their own access cost you nothing to serve. Anyone who does not falls through to a metered tier.
❯ npx @soba/worker login --broker https://your-app.com
What your users
connect.
Soba Components
One flow, whatever they bring
The friction is not the same for everyone: an OAuth popup for a ChatGPT plan, install-then-approve for a Claude daemon on their own machine. Soba absorbs that difference, so your app has one integration and one state to render.
See exactly what ships today ▶Components in design
Every user brings whatever AI access they already have, in one step.
A hosted fallback page covers Vue, Svelte, Rails and Django by redirect, the same trick a checkout uses.
Connect what you have
Four things a user can bring
Every user brings AI access they already hold, and your app asks for a class of compute rather than a machine, so the integration is the same whichever one they choose. Only the last option reaches your account.
Explore the protocol ▶They bring the compute. You never hold a credential or model a token bill.
A wrong route cannot bill you. The worker checks allow, not just the broker.
Native agent support
Want to support Claude Code natively? Ship this.
One integration, whichever agent they already run.
Your app
One call, carrying your prompt and your tool schemas.
soba.run()
Their machinebackground service
Soba drives the agent CLI they installed and signed into themselves.
- claude-code
- codex
- gemini-cli
- ollama
- Runs whether or not anyone is watching.
- Their hardware, their plan, their account.
- Soba never holds the login.
Back in your UI
- Web
- Native
- Mobile
Tokens arrive the same way on every surface you ship.
Their install, their account
The worker drives the CLI the user already installed and logged into, in place, on their own hardware. Nothing is copied off the machine, re-hosted or resold, and a paired machine only ever runs for the person who paired it.
Dials out, never listens
It holds a single outbound socket to the broker. No port is opened on the laptop, so there is nothing on it to scan, and a dropped socket simply cancels its runs.
Asks on the machine it affects
Nothing is askable until the owner opts in. The prompt lands here, a timeout or an unreachable approver reads as deny, and every decision is appended to the audit log.
Claude Code, Codex, Gemini CLI and Ollama are the products of their respective owners. Soba is an independent tool, not affiliated with or endorsed by any of them, and each run stays on the user's own machine under their own account and that provider's terms.
The split
You own the tools. Soba owns the loop.
An agentic chat is only two things: calls to a model, and calls to your own code. Soba drives the loop and puts every model call on compute the user already pays for. Your app defines the tools and runs them against your data.
An agentic chat
A model that reads, decides and acts inside your product.
Your tools
You define them and you run them, under your auth, against your database.
app.notes.createThe LLM calls
Every turn of the loop, on compute the user already pays for.
costMicros=0That is the whole integration. You never buy a token, and Soba never touches your database.
Soba does
Chooses the compute, drives the agent loop, enforces the permission policy, meters what is metered, renders the pricing and connect UI, and takes the payment.
You do
Define the tools, execute them, and own everything they touch. Nothing about your data model, your auth or your database has to leave your side of the line.
Connecting a machine
The shape gh auth login uses
It replaces "open the app, find settings, mint a token, copy it, paste it into a terminal", which is four steps, one of them a bearer credential pasted straight into shell history. Here the credential is never displayed and never pasted, and the person approves in a browser where they are already signed in to your app.
The broker chooses that URL, and the broker is the component we ask people to trust least. An unchecked value is a phishing page one compromised control plane away.
The URL is printed first. A URL someone has read and agreed to is a different thing from one a server opened on their machine.
--no-browser prints the URL and polls, so a box with no display pairs the same way a laptop does.
❯ npx @soba/worker login --broker https://your-app.com
Your code: WXYZ-1234
Approve at: https://your-app.com/link
Press Enter to open that in your browser (or n to skip)…
[soba] waiting for you to approve…
[soba] paired. The token is in ~/.soba/worker.json (0600).
It was never shown or pasted.
[soba] starting.
Billing, handled
Two meters, one integration
What the compute costs you, and what you charge your users for it. Your app declares which cost classes it accepts and stops thinking about the first; the plan you sell sits on top of it, priced at your rate rather than ours.
What the compute costs you
| Cost class | Who already paid | Metered by the broker |
|---|---|---|
| user-hardware | The user's own silicon: a local model on a machine they bought | Not metered |
| user-subscription | A plan they already pay for: Claude Pro or Max, ChatGPT Plus or Pro | Not metered |
| open-weights | An open model on GPU, bought wholesale | Metered |
| frontier | A frontier provider API | Metered, unless the key is theirs |
What you charge your users in design
The plan sits on top, at your price
A plan is a stored object (price, unit, allowance, expiry, route) with a per-user ledger priced at your rate rather than at our cost. Codes grant a plan for a period, with redemption caps. Payments run as a rail Soba drives rather than an integration you build: onboarding, checkout, entitlement, payouts, refunds, dunning, tax.
Today's metering is Soba's own cost accounting, the number above. The layer that bills your users is specified and not built, and the Status section says so in as many words.
Finding the binary is not being able to run it
A signed-out CLI is kept out of the advertised set and reported to its owner with the fix, instead of being routed to and failing at spawn on every run.
A row without one is skipped
Guessing whose money this is, is exactly the mistake the field exists to prevent. Only the machine's owner can declare it.
The tier arrives before the output
It is stamped on the opening status event and on usage, so a downgrade is something your interface can show rather than something a user has to feel.
You cannot beat free with a 20% discount.
Every other router competes on price per token, so its supply has to be paid for and its floor is somebody's margin. Soba's supply was bought by the person asking for the run. Routers earn on tokens flowing through them; this one earns on tokens that never flow at all.
❯ npm i -g @soba/worker && soba-worker install
[soba] launchd agent written to ~/Library/LaunchAgents
[soba] Restart=always · env from ~/.soba/service.env (0600)
❯ soba-worker --status --verify
service installed, running
claude-code authenticated user-subscription
ollama 3 models user-hardware
codex signed out run `codex login`
Keeping it running
A laptop sleeps. A $5 box does not.
Pairing is one command. The step that loses people is the next one. A worker is a foreground process, so it dies with the terminal and never comes back after a reboot. "Leave this window open forever" is the real friction, on a laptop and on a VPS alike.
A systemd unit is world-readable by design, so the token stays in a 0600 file the worker loads itself. Only locations go in the unit, and they are not secrets.
npm prunes those directories, so a service pointing at one works today and fails weeks later for no visible reason. What gets written is the pinned command.
nvm, fnm, volta and asdf install per-version binaries that stop existing the day you upgrade. Install says so and carries on.
Connect once, then stop thinking about it
Metered only on the tier someone has to buy. Pair a machine, send a run, and watch the usage event come back at zero.