Integration Guide
Embed Arda Gateway with user-owned billing: connect each user with Gateway OAuth, route requests through arda/auto, and send users to the billing portal only when their own balance needs attention.
Choose your integration
Desktop app
Use device codes for Lens-style clients.
Web app
Use PKCE and approved web origins.
Server API
Use OpenAI-compatible keys server-side.
Reseller app
Add billing recovery and payout setup.
Arda Gateway SDK: user-funded embed
Recommended for apps that want users to pay for their own inference. The SDK creates PKCE OAuth URLs, exchanges the redirect code for a user-owned gw_sk_* key, and returns an authenticated Gateway client.
import {
buildGatewayInferenceSelection,
createGatewayOAuthAuthorizationUrl,
evaluateGatewayBalanceRecovery,
evaluateGatewayInferenceRecovery,
exchangeGatewayOAuthCode,
getGatewayOAuthAuthorizeInfo,
} from '@arda/gateway-sdk';
const info = await getGatewayOAuthAuthorizeInfo({
clientId: 'gw_app_...',
redirectUri: 'myapp://oauth/callback',
});
console.log(info.name, info.reseller.disclosure);
const auth = await createGatewayOAuthAuthorizationUrl({
clientId: 'gw_app_...',
redirectUri: 'myapp://oauth/callback',
});
window.location.href = auth.url;
// After your redirect URI receives ?code=...
const connected = await exchangeGatewayOAuthCode({
clientId: 'gw_app_...',
redirectUri: 'myapp://oauth/callback',
code,
codeVerifier: auth.codeVerifier,
});SDK helpers default to https://gateway.ardabot.ai. Pass baseUrl only for local, staging, or self-hosted Gateway environments.
Status and model picker preflight
Before sending a request, correctly scoped embedded apps can check the connected project balance, saved-payment state, auto-reload settings, and render a model picker with the pricing context that includes reseller markup. Approved OAuth app web origins can call Gateway directly and read balance, routing, and rate-limit metadata headers. Use getEmbeddedAppBootstrap() for one request that returns the billing decision, account state, catalog, pricing disclosures, and AutoRoute picker metadata. If you do not provide a top-up suggestion, the SDK picks the smallest supported amount that restores your configured balance floor.
const bootstrap = await connected.gateway.getEmbeddedAppBootstrap({
billing: {
minimumBalanceCents: 500,
requireSavedPaymentMethod: true,
returnUrl: 'myapp://billing/complete',
suggestedAutoReloadEnabled: true,
},
models: {
types: 'language',
usage: { inputTokens: 5_000, outputTokens: 1_000 },
},
});
if (!bootstrap.billing.ready && bootstrap.billing.action === 'reconnect') {
throw new Error(`Reconnect with scopes: ${bootstrap.billing.missingScopes.join(' ')}`);
}
if (bootstrap.recoveryInstruction.action === 'open_portal') {
const portal = await connected.gateway.createPortalSession(
bootstrap.recoveryInstruction.portalOptions
);
open(portal.url);
}
for (const option of bootstrap.modelOptions) {
console.log(option.id, option.disclosure);
}
console.log(bootstrap.status.project?.slug, bootstrap.status.autoReload?.enabled);
console.log(bootstrap.catalog.pricing_context?.reseller_markup_bps);AutoRoute + billing recovery
Use arda/auto when you want Gateway to choose from available models. Catch billing denials as a recoverable product state, not as a generic model failure.
const selectedModel = bootstrap.autoRouteOption ?? bootstrap.defaultModelOption ?? 'arda/auto';
const selection = buildGatewayInferenceSelection({
model: selectedModel,
autoRoute: bootstrap.autoRoute,
routing: { mode: 'cheap', intent: 'coding' },
});
try {
const result = await connected.gateway.createChatCompletionWithMetadata({
...selection,
messages: [{ role: 'user', content: 'Fix this failing test' }],
});
console.log(result.routing?.selectedModel);
console.log(result.balance?.balanceCents);
const balanceRecovery = evaluateGatewayBalanceRecovery(result.balance, {
topupAmountsCents: bootstrap.status.topupAmountsCents,
returnUrl: 'myapp://billing/complete',
});
const portal = await connected.gateway.createPortalSessionForRecovery(balanceRecovery);
if (portal !== null) window.location.href = portal.url;
} catch (err) {
const recovery = evaluateGatewayInferenceRecovery(err, {
topupAmountsCents: bootstrap.status.topupAmountsCents,
returnUrl: 'myapp://billing/complete',
});
const portal = await connected.gateway.createPortalSessionForRecovery(recovery);
if (portal !== null) window.location.href = portal.url;
else throw err;
}For chat UIs, stream chunks through the same SDK so billing and AutoRoute metadata stay typed.
const result = await connected.gateway.createChatCompletionStreamWithMetadata({
model: 'arda/auto',
messages: [{ role: 'user', content: 'Walk me through this stack trace' }],
routing: { mode: 'fast', intent: 'debugging' },
});
for await (const chunk of result.stream) {
const text = chunk.choices[0]?.delta.content;
if (text != null) appendToTranscript(text);
}Device and desktop apps
Shared-screen, CLI, and desktop clients can use the device authorization helpers instead of browser redirects. Polling returns a pending state until the user approves the code on the Gateway connect page.
import {
startGatewayDeviceAuthorization,
pollGatewayDeviceAuthorization,
} from '@arda/gateway-sdk';
const device = await startGatewayDeviceAuthorization({
clientId: 'gw_app_...',
});
console.log(device.userCode, device.verificationUri);
const result = await pollGatewayDeviceAuthorization({
clientId: 'gw_app_...',
deviceCode: device.deviceCode,
});OpenAI-compatible fallback
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: 'gw_sk_...',
baseURL: 'https://gateway.ardabot.ai/v1',
});
const res = await client.chat.completions.create({
model: 'arda/auto',
messages: [{ role: 'user', content: 'Hello' }],
});Anthropic SDK (TypeScript)
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({
apiKey: 'gw_sk_...',
baseURL: 'https://gateway.ardabot.ai',
});
const res = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello' }],
});Python (openai)
from openai import OpenAI
client = OpenAI(
api_key="gw_sk_...",
base_url="https://gateway.ardabot.ai/v1",
)
res = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "Hello"}],
)curl
curl https://gateway.ardabot.ai/v1/chat/completions \
-H "Authorization: Bearer gw_sk_..." \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-6",
"messages": [{"role": "user", "content": "Hello"}]
}'Sign in with Arda
Developer apps register from the Developers dashboard with an app website, description, redirect URI, and optional reseller markup. Public browser/native apps should use PKCE; confidential server apps receive a client secret once on creation. New apps are pending until Arda review approves them. Approval requires completed Stripe Express payouts.
pnpm gateway:oauth:app-review list --status=pending pnpm gateway:oauth:app-review approve --client-id=gw_app_...
Response Headers
Gateway responses expose balance, rate-limit, and AutoRoute metadata:
- X-Gateway-Balance-Cents: Remaining balance
- X-Gateway-Balance-Warning: low when the project is near top-up
- X-Arda-Selected-Model: Concrete model selected for arda/auto
- X-Arda-Routing-Class: AutoRoute task class
- X-RateLimit-Limit: Rate limit (rpm)
- X-RateLimit-Remaining: Remaining requests