Apps: get listed and let users connect
Two things for people who build apps on UberLLM. Both work the same way as on OpenRouter, so an app that already does them only changes the base URL.
- Get listed. Send two headers and your app shows up on uberllm.dev/apps, ranked by the tokens it sends. Model pages list the apps that use them most.
- Let users connect. Send users to UberLLM, they say yes, and your app gets an API key on their account. They pay for their own usage; you never handle their payment details.
Get listed on the apps ranking
Add these headers to your requests (chat, messages or media, any endpoint):
| Header | What it does |
|---|---|
HTTP-Referer |
Your app's address, e.g. https://myapp.com. Required: it is what identifies your app. Referer works too. |
X-Title |
Your app's name as people should see it. X-OpenRouter-Title works too. |
X-OpenRouter-Categories |
Optional. Up to 2 of the categories below, comma-separated. |
X-OpenRouter-App-Visibility |
Optional. hidden keeps a new app off the public ranking. |
curl https://api.uberllm.dev/v1/chat/completions \
-H "Authorization: Bearer $UBERLLM_API_KEY" \
-H "Content-Type: application/json" \
-H "HTTP-Referer: https://myapp.com" \
-H "X-Title: My App" \
-H "X-OpenRouter-Categories: cli-agent" \
-d '{"model":"deepseek/deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}]}'
With the OpenAI SDK:
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://api.uberllm.dev/v1',
apiKey: process.env.UBERLLM_API_KEY,
defaultHeaders: {
'HTTP-Referer': 'https://myapp.com',
'X-Title': 'My App',
},
});
How it is counted:
- Only the address is kept:
https://myapp.com/some/page?user=…is stored ashttps://myapp.com. Paths, query strings and logins are dropped. A GitHub, GitLab, Codeberg, Bitbucket or Hugging Face link keeps itsowner/repo, so open-source apps get their own entry. - The name is cut to 60 characters. Hidden and control characters are removed.
- The first account that sends traffic for an app sets its name and categories. Other accounts' traffic still counts toward the app, but cannot rename it.
- Addresses on your own machine or network (
localhost,127.0.0.1, private IPs,*.local) are never shown publicly. - Requests from browsers send
Refereron their own, so a web app is counted even without the headers. AddX-Titleto choose its name. - The ranking counts requests that were answered, by tokens, over the last 24 hours, 7 days or 30 days. It refreshes every few minutes.
Categories: cli-agent, ide-extension, cloud-agent, programming-app,
native-app-builder, creative-writing, video-gen, image-gen,
audio-gen, writing-assistant, general-chat, personal-agent, legal,
roleplay, game. Unknown ones are ignored. An app keeps up to 10.
The ranking is also an API: GET https://uberllm.dev/api/v1/public/apps?period=day|week|month&category=<slug>.
Connect UberLLM (OAuth PKCE)
Your app gets a key on the user's own account, named after your app. The user can set a spending limit for it while allowing it, and can turn it off any time on their keys page.
1. Make a code verifier and challenge
The verifier is a random string you keep (43 to 128 characters). The challenge is its SHA-256, base64url-encoded without padding.
function base64url(bytes) {
return btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const codeVerifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const codeChallenge = base64url(
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)),
);
sessionStorage.setItem('uberllm_verifier', codeVerifier);
2. Send the user to UberLLM
https://uberllm.dev/auth?callback_url=https://myapp.com/uberllm/callback&code_challenge=<CHALLENGE>&code_challenge_method=S256
| Parameter | |
|---|---|
callback_url |
Where the user comes back to. Must be https://; http://localhost (any port) is allowed for development. Leave it out to show the code on screen for the user to paste into your app. |
code_challenge |
From step 1. Required. |
code_challenge_method |
S256. Required. plain is not accepted. |
key_label |
Optional name for the key (default: your app's name). |
state |
Optional. Sent back unchanged with the code, so you can check the reply belongs to the request you started. |
The user signs in (or signs up), sees "Your app wants to use your UberLLM credit", and chooses Allow or Cancel. On Allow they are sent to:
https://myapp.com/uberllm/callback?code=<CODE>&state=<STATE>
On Cancel nothing is sent anywhere.
3. Trade the code for a key
const code = new URLSearchParams(location.search).get('code');
const res = await fetch('https://uberllm.dev/api/v1/auth/keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
code_verifier: sessionStorage.getItem('uberllm_verifier'),
code_challenge_method: 'S256',
}),
});
const { key } = await res.json(); // ull_… Store it like a password.
curl https://uberllm.dev/api/v1/auth/keys \
-H "Content-Type: application/json" \
-d '{"code":"<CODE>","code_verifier":"<VERIFIER>","code_challenge_method":"S256"}'
The reply is {"key": "ull_…", "key_id": "…", "name": "My App"}. Use the key
as Authorization: Bearer on https://api.uberllm.dev/v1. The endpoint
answers browser requests from any site (CORS), so it works from a web app.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_code_challenge_method |
Use S256. |
| 403 | invalid_code |
Wrong code or code_verifier, or the code was already used. |
| 403 | code_expired |
Codes work for 10 minutes. Start again from step 2. |
| 405 | method_not_allowed |
Use POST. |
| 429 | rate_limited |
Too many tries from one address; wait a minute. |
A code works once. A wrong code_verifier uses it up too, so a code that
leaked cannot be guessed against.
Differences from OpenRouter
code_challengeis required and onlyS256is accepted (OpenRouter also takesplainor no challenge). Without it, anyone who sees the code in a browser history or log could take the key.workspace_id/required_workspace_idare not used: an UberLLM account has one balance.