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