Build

Connect with OpenLLM

Add a Connect with OpenLLM button to your app. Your users run your AI features on their own Claude, ChatGPT, and local models, and you pay $0 for inference.

Your users already pay for AI. With a Connect with OpenLLM button, they bring it to your app. They sign in with OpenLLM, and your app runs AI on their account: their Claude, ChatGPT, and other subscriptions, their own API keys, and their local models.

  • You pay $0 for their inference.
  • You never see their provider keys.
  • They set limits for your app and can disconnect it at any time.

It's standard OAuth 2.1 (authorization code with PKCE) plus one step: on the consent screen, OpenLLM creates a key for your app and seals it to your app's public key. Your server collects it once.

You don't need a package:

Get access

OpenLLM reviews every app by hand before it can connect anyone. There is no self-serve app creation, and no credentials exist until your app is approved.

Apply

Fill in the form at openllm.sh/developers/apply: your email, your app's name, website, what it does, its callback URLs (https only, or http://localhost:… while you build), and a logo URL.

Talk to us

We email you to set up a short call. To follow your application, sign in to OpenLLM with the email you applied under. It shows under Settings → Developers → Apps.

Get approved

We email you when your app is approved. If we decline it, the email and your app's row carry the reason, and you can edit and resubmit.

Create your credentials

In Settings → Developers → Apps, press Set up on your approved app. Your browser makes the key pair and creates the app, then shows a .env block:

OPENLLM_CLIENT_ID=…
OPENLLM_CLIENT_SECRET=…        # kept in your vault; "New secret" makes another
OPENLLM_PRIVATE_KEY=…          # your app's X25519 private key, made in your browser
OPENLLM_ISSUER=…               # the OAuth issuer
OPENLLM_RESOURCE=https://openllm.sh/v1
OPENLLM_API_ORIGIN=https://openllm.sh
OPENLLM_REDIRECT_URI=…         # one of your callback URLs

The client secret and private key are sealed under your vault, so your app's page shows them again any time while your vault is unlocked. OpenLLM's server never sees them in plaintext. Keep every value server-side. If you lose the private key, you need a new app.

Add the routes

Copy openllm-connect.ts into your server code unchanged. It must never run in a browser: it holds your client secret and private key. It needs only fetch and WebCrypto with X25519 (Node 20+, Bun, Deno, Cloudflare Workers, Vercel Functions).

You need three routes.

Start

Your button links here. Make PKCE and state, keep them in a short-lived httpOnly cookie, and redirect.

import { openllmConfigFromEnv, startConnect } from "./openllm-connect";

const openllm = openllmConfigFromEnv();
const { url, state, codeVerifier } = await startConnect(openllm);
// set cookie openllm_oauth = JSON.stringify({ state, codeVerifier })
//   httpOnly, secure, sameSite=lax, maxAge 600, path=/
// redirect 302 → url

Callback

At the path of OPENLLM_REDIRECT_URI. Check state, finish, and store the connection. finishConnect opens the sealed key with your private key and returns the connection: the OpenLLM user id, the connection's own sk-llm-… key, and its tokens.

import { finishConnect, reconnectTokens } from "./openllm-connect";

// Read ?code and ?state, compare state with the cookie (reject on mismatch),
// and delete the cookie. ?error=access_denied means the user said no.
try {
  const connection = await finishConnect(openllm, { code, codeVerifier });
  await saveConnection(currentUser, connection); // encrypt at rest
} catch (err) {
  // Connected before: keep their stored key, take the new tokens.
  const stored = await loadConnection(currentUser);
  const next = stored && reconnectTokens(stored, err);
  if (!next) throw err;
  await saveConnection(currentUser, next);
}

A user's key can be collected only once. If they connect again, finishConnect throws already_picked_up and reconnectTokens combines the key you stored with the new tokens.

Call the API

Any OpenAI-compatible call, as that user:

import { openllmFetch } from "./openllm-connect";

const res = await openllmFetch(
  openllm,
  connection,
  "/v1/chat/completions",
  {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      model: "plus", // or "ultra", "lite", or any id from /v1/models
      messages: [{ role: "user", content: "Hello" }],
      stream: true,
    }),
  },
  (next) => saveConnection(currentUser, next), // tokens were refreshed
);

openllmFetch refreshes the access token a minute before it expires and passes you the new connection to store. It sets two headers on every request:

Authorization: Bearer <access token>     # proves it's your app, for this user
X-OpenLLM-Key: sk-llm-…                  # the connection's own key

The rest of the API works the same way: /v1/messages (Anthropic), /v1/responses, /v1/models, /v1/embeddings, /v1/images/generations, and /v1/audio/*. Calls are server-to-server only. Don't call /v1 from a browser.

The button

Link it to your start route:

<a href="YOUR_START_ROUTE">
  <img src="https://openllm.sh/connect/button.svg" alt="Connect with OpenLLM" height="44" />
</a>

What your users see

  1. Your button sends them to OpenLLM sign-in. New users get an account in a few seconds, with nothing to set up first.
  2. The consent screen shows your app's name, where it sends them back, and optional limits they can set for you: monthly tokens, models, and an expiry.
  3. They return to your callback. Their key runs on whatever their account can serve: their subscriptions through the OpenLLM app or daemon on their computer, their API keys, and the included models on their plan. Someone brand new may have nothing connected yet. See subscription_requires_daemon under Errors.

Users can change your limits or disconnect your app at any time under Connections → Apps.

Errors

API errors are { "error": { "code", "message" } }. The helper throws OpenLLMError with code, status, and needsReconnect. needsReconnect is true for connector_reconnect, connector_auth, and invalid_grant.

CodeMeaningWhat to do
connector_reconnectThe user disconnected your app, or reset their vaultDelete the stored connection and show the button again
connector_authThe token or key is invalid for this userSame as above
invalid_grantThe refresh token no longer works, but the user is still connectedKeep the stored connection and show the button. The callback needs it for reconnectTokens, because the key can't be collected twice
connector_opaque_tokenresource was missing on authorize or tokenThe helper always sends it
already_picked_upThis user was already connectedCall reconnectTokens(stored, err)
no_pending_grant / grant_expiredConsent didn't finish, or took over 15 minutesSend them through the button again
pkce_mismatchWrong code_verifier for this sign-inCheck your cookie handling
app_not_openHTTP 403. Your app isn't approved by OpenLLM yet, so only its owner can connect. People already connected keep workingFinish getting access
access_*The user's limits for your app (tokens, models, rate)Tell the user. They can raise them under Connections
subscription_requires_daemon / local_requires_daemonThe user's models need a provider or computer that isn't connected or is offlineAsk them to finish setup at openllm.sh/get-started, then retry

Disconnect and revocation

To disconnect a user from your side, call disconnect. Their key stops working at once.

import { disconnect } from "./openllm-connect";

await disconnect(openllm, connection);

The user can also disconnect your app under Connections → Apps, or reset their vault. Your next call then fails with connector_reconnect: delete the stored connection and show the button again.

Security

  • Your client secret, private key, and each user's sk-llm key and refresh token are credentials. Store them server-side, encrypted at rest. Never log them or send them to the browser.
  • Always check state on the callback.
  • The sealed key opens only with your private key, and it is bound to your client id, the user, and the connection. A copy is useless anywhere else.
  • Your app gets its own key for each user, never the user's own key.

Changing your app

Edit your app under Settings → Developers → Apps.

  • Name, homepage, and logo apply at once. Leave the logo empty and OpenLLM uses the one on your homepage. People see it when they connect.
  • A new callback host goes to review. Your current callbacks keep working until the change is approved, and people already connected stay connected.

Button and brand rules

Use the official button, linked to your start route:

FileUse on
button.svgDefault (dark button)
button-dark.svgLight backgrounds (dark button)
button-light.svgDark backgrounds (light button)
  • Give the button comparable visual prominence to your other sign-in options.
  • Say clearly which of your plans use the user's OpenLLM account.
  • Don't redraw or recolor the button. For the OpenLLM logo itself, see Brand assets.

Using a coding agent

An agent-readable copy of this page is at docs.openllm.sh/connect.md. Point your coding agent at it: it copies openllm-connect.ts unchanged, adds the start and callback routes and a per-user store with your project's framework, session, and database, and reads every OPENLLM_* value from the environment.

Last updated on

On this page