Documentation

One public reference for the Legion SDK, OAuth, API, CLI, MCP, and platform integrations.

Legion developer documentation

This page mirrors the Developer documentation with one exception: MCP access token generation requires login.

Use the copy controls or each section's plain-text view for the exact shared documentation source.

Create an App

Via Dashboard

Via CLI

legion login
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback"
# Returns a public client_id and a one-time client_secret.
# Standard SDK, Next.js, WordPress, and Drupal integrations configure only client_id.
# They still send a registered redirect_uri during authorization and use S256 PKCE.
# Multiple redirect URIs (comma-separated):
legion create-app --name "My App" --redirect-uris "http://localhost:3000/callback,https://myapp.com/callback"
# Optional partner attribution for non-partner developers:
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --partner lovable

Via MCP (AI Agents)

Use the legion_create_oauth_app tool with name and redirect_uris parameters.

View plain text
### Via Dashboard
- Log in to the Legion dashboard at https://legion-ai.org/developer
- Or start on the public registration page at https://legion-ai.org/register and sign in only after submitting the form
- Create a new app: enter your app name and domain (e.g., https://yourapp.com).
- You'll receive a client_id. Register your domain as the redirect URI.
- The server automatically appends /callback if no path is given, so https://yourapp.com becomes https://yourapp.com/callback. You can also register a custom path (e.g., /auth/callback) and it will be used as-is.
- Use the full redirect URI (with path) in your code.
- Response format is OpenAI-compatible.

### Via CLI
legion login
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback"
# Returns a public client_id and a one-time client_secret.
# Standard SDK, Next.js, WordPress, and Drupal integrations configure only client_id.
# They still send a registered redirect_uri during authorization and use S256 PKCE.

# Multiple redirect URIs (comma-separated):
legion create-app --name "My App" --redirect-uris "http://localhost:3000/callback,https://myapp.com/callback"

# Optional partner attribution for non-partner developers:
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --partner lovable

### Via MCP (AI Agents)
Use the legion_create_oauth_app tool with name and redirect_uris parameters.

Key Concepts: OAuth App vs Railway Log Source

OAuth App = An OAuth application registration. Creating an app gives you a public client_id for standard PKCE integrations and a one-time client_secret for advanced confidential backend OAuth. OAuth apps are for building AI-powered features.

Railway Log Source = A Railway deployment connection. Connecting a log source gives you access to deployment logs for debugging. Log sources are for monitoring and log access only — they are not required for API integration.

Most developers only need an OAuth App. Railway log sources are optional and only useful if you deploy on Railway and want log access via the CLI or MCP tools.

View plain text
**OAuth App** = An OAuth application registration. Creating an app gives you a public client_id for standard PKCE integrations and a one-time client_secret for advanced confidential backend OAuth. OAuth apps are for building AI-powered features.

**Railway Log Source** = A Railway deployment connection. Connecting a log source gives you access to deployment logs for debugging. Log sources are for monitoring and log access only — they are not required for API integration.

Most developers only need an OAuth App. Railway log sources are optional and only useful if you deploy on Railway and want log access via the CLI or MCP tools.

Integration Approach

Recommended: Login at Point of Use

Prompt users to connect their Legion wallet when they first try to use AI features (e.g., send a chat message). This provides the best UX - users only authenticate when they actually need it.

Alternative: Dedicated Button

A "Connect Wallet" button in your UI is also acceptable if you prefer upfront authentication.

View plain text
### Recommended: Login at Point of Use
Prompt users to connect their Legion wallet when they first try to use AI features (e.g., send a chat message). This provides the best UX - users only authenticate when they actually need it.

### Alternative: Dedicated Button
A "Connect Wallet" button in your UI is also acceptable if you prefer upfront authentication.

OAuth Connect Flow (Manual)

Important notes:

  • Only response_type=code is supported (authorization code flow)
  • scope defaults to 'chat:completions' if omitted
  • Authorization start requires redirect_uri, and it must match a registered redirect URI.
  • Public browser/CMS clients must use S256 PKCE. The authorization request includes code_challenge and code_challenge_method=S256; the token exchange includes code_verifier.
  • redirect_uri is intentionally not sent again in the token exchange body.
  • A public-client token exchange with only grant_type, code, and client_id is invalid for PKCE-bound authorization codes and returns invalid_grant.
  • Public clients do not send client_secret; confidential server-side clients must send client_secret when exchanging a code issued without PKCE.
  • Authorization codes expire after ten minutes and can be used only once.
  • authorization_intent supports exactly: authorize, increase_cap, payment_method.
  • Legion may require the user to add a payment method during consent. If so, the hosted /connect page handles Stripe setup first, then redirects back to your callback with the authorization code.
  • If payment setup is not required during consent, the user can authorize first, receives a payment-method prompt email, and can make capped trial usage until a card is added. User-funded usage is collected through monthly billing.
// Step 1 (recommended): the SDK generates PKCE and redirects to consent.
const legion = createLegion({ clientId: 'YOUR_CLIENT_ID' });
legion.connect({ redirectUri: 'https://yourapp.com/callback' });
// Step 2: User authorizes and returns with code + state. The SDK retrieves the
// verifier associated with that state and performs the protected exchange.
const { access_token, refresh_token, expires_in } = await legion.exchangeCode(code, { state });
// access_token expires in 86400 seconds (24 hours)
// Store both tokens - use refresh_token to get new access tokens
// Step 2, manual equivalent for public clients:
// Send code_verifier. Do not send redirect_uri or client_secret.
const tokenRes = await fetch('https://auth.legion-ai.org/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code,
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: pkceVerifierFromStep1,
  }),
});
// Step 3: Make API calls with user's token
const chatRes = await fetch('https://api.legion-ai.org/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${access_token}`,
  },
  body: JSON.stringify({
    // Optional: omit model to use your app's default model from the dashboard
    model: 'gemini-2.5-flash',
    messages: [{ role: 'user', content: 'Hello!' }],
    // Optional: inject user context for personalized responses
    user_preferences: true,  // user's AI preferences (e.g. "brief answers")
    user_information: true,  // demographics (age, location, occupation)
    user_context: true,      // query patterns (top topics, intent distribution)
  }),
});
const data = await chatRes.json();
const reply = data.choices[0].message.content;

View plain text
Important notes:
- Only response_type=code is supported (authorization code flow)
- scope defaults to 'chat:completions' if omitted
- Authorization start requires redirect_uri, and it must match a registered redirect URI.
- Public browser/CMS clients must use S256 PKCE. The authorization request includes code_challenge and code_challenge_method=S256; the token exchange includes code_verifier.
- redirect_uri is intentionally not sent again in the token exchange body.
- A public-client token exchange with only grant_type, code, and client_id is invalid for PKCE-bound authorization codes and returns invalid_grant.
- Public clients do not send client_secret; confidential server-side clients must send client_secret when exchanging a code issued without PKCE.
- Authorization codes expire after ten minutes and can be used only once.
- authorization_intent supports exactly: authorize, increase_cap, payment_method.
- Legion may require the user to add a payment method during consent. If so, the hosted /connect page handles Stripe setup first, then redirects back to your callback with the authorization code.
- If payment setup is not required during consent, the user can authorize first, receives a payment-method prompt email, and can make capped trial usage until a card is added. User-funded usage is collected through monthly billing.

// Step 1 (recommended): the SDK generates PKCE and redirects to consent.
const legion = createLegion({ clientId: 'YOUR_CLIENT_ID' });
legion.connect({ redirectUri: 'https://yourapp.com/callback' });

// Step 2: User authorizes and returns with code + state. The SDK retrieves the
// verifier associated with that state and performs the protected exchange.
const { access_token, refresh_token, expires_in } = await legion.exchangeCode(code, { state });
// access_token expires in 86400 seconds (24 hours)
// Store both tokens - use refresh_token to get new access tokens

// Step 2, manual equivalent for public clients:
// Send code_verifier. Do not send redirect_uri or client_secret.
const tokenRes = await fetch('https://auth.legion-ai.org/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code,
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: pkceVerifierFromStep1,
  }),
});

// Step 3: Make API calls with user's token
const chatRes = await fetch('https://api.legion-ai.org/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${access_token}`,
  },
  body: JSON.stringify({
    // Optional: omit model to use your app's default model from the dashboard
    model: 'gemini-2.5-flash',
    messages: [{ role: 'user', content: 'Hello!' }],
    // Optional: inject user context for personalized responses
    user_preferences: true,  // user's AI preferences (e.g. "brief answers")
    user_information: true,  // demographics (age, location, occupation)
    user_context: true,      // query patterns (top topics, intent distribution)
  }),
});
const data = await chatRes.json();
const reply = data.choices[0].message.content;

---

Available Models

Default model: your app's default model if configured in the dashboard; otherwise the Google launch default gemini-2.5-flash. Owner margin: configurable markup on model costs (default 50%). Legion also adds a fixed 20% fee on model cost. You keep 100% of the owner margin. Set via the dashboard, CLI (--margin), or PATCH /v1/apps/:clientId with owner_margin_pct. Permitted models are controlled centrally. Canonical defaults live in packages/finance-core/src/index.js (DEFAULT_MODEL_CATALOG and DEFAULT_MODEL_PRICING). Runtime model activation overrides come from auth-server model_catalog, and runtime price overrides come from auth-server price_catalog.

Launch active chat models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite

Launch note: Legion is temporarily Google-only across active AI models to reduce provider billing timing risk. OpenAI, Anthropic, and xAI integrations remain in the codebase but are inactive in the launch catalog until reintroduced.

Speech Models (Audio)

Speech-to-text default: gemini-2.5-flash Text-to-speech default: gemini-2.5-flash-preview-tts

Speech-to-text: gemini-2.5-flash Text-to-speech: gemini-2.5-flash-preview-tts

Note: use gemini-2.5-flash for speech-to-text requests. Legion routes that public model id through the audio transcription endpoint context; clients should not use internal catalog aliases.

Media Generation Models

Image: gemini-3.1-flash-image-preview (Google AI Studio, default), nano-banana-2 (Google Vertex) Video generation is temporarily unavailable during launch. Video model rows and pricing remain in Legion's internal catalog for reactivation, but active model discovery does not expose video models.

Media prices are based on the concrete generation settings Legion sends for each active model (for example: Gemini image generation at 1K). Text pricing also follows vendor large-context tiers when providers charge more above a prompt-token threshold. Configure defaults via the dashboard or PATCH /v1/apps/:clientId with default_image_model.

List Models Programmatically

GET https://api.legion-ai.org/v1/models
Returns an OpenAI-compatible model list including media models. No authentication required.
GET https://api.legion-ai.org/v1/models/catalog
Returns the active Legion catalog with active public defaults plus working-model options used internally by prompt optimization and intent routing. Speech model types are included with `type: "speech_to_text"` and `type: "text_to_speech"`.

Example:

curl https://api.legion-ai.org/v1/models
// Response: { "object": "list", "data": [{ "id": "gemini-2.5-flash", "object": "model", "type": "chat", ... }, { "id": "nano-banana-2", "type": "image_generation", ... }, ...] }

View plain text
Default model: your app's default model if configured in the dashboard; otherwise the Google launch default gemini-2.5-flash.
Owner margin: configurable markup on model costs (default 50%). Legion also adds a fixed 20% fee on model cost. You keep 100% of the owner margin. Set via the dashboard, CLI (--margin), or PATCH /v1/apps/:clientId with owner_margin_pct.
Permitted models are controlled centrally. Canonical defaults live in packages/finance-core/src/index.js (DEFAULT_MODEL_CATALOG and DEFAULT_MODEL_PRICING). Runtime model activation overrides come from auth-server model_catalog, and runtime price overrides come from auth-server price_catalog.

Launch active chat models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite

Launch note: Legion is temporarily Google-only across active AI models to reduce provider billing timing risk. OpenAI, Anthropic, and xAI integrations remain in the codebase but are inactive in the launch catalog until reintroduced.

### Speech Models (Audio)

Speech-to-text default: gemini-2.5-flash
Text-to-speech default: gemini-2.5-flash-preview-tts

Speech-to-text: gemini-2.5-flash
Text-to-speech: gemini-2.5-flash-preview-tts

Note: use `gemini-2.5-flash` for speech-to-text requests. Legion routes that public model id through the audio transcription endpoint context; clients should not use internal catalog aliases.

### Media Generation Models

Image: gemini-3.1-flash-image-preview (Google AI Studio, default), nano-banana-2 (Google Vertex)
Video generation is temporarily unavailable during launch. Video model rows and pricing remain in Legion's internal catalog for reactivation, but active model discovery does not expose video models.

Media prices are based on the concrete generation settings Legion sends for each active model (for example: Gemini image generation at 1K).
Text pricing also follows vendor large-context tiers when providers charge more above a prompt-token threshold.
Configure defaults via the dashboard or PATCH /v1/apps/:clientId with default_image_model.

### List Models Programmatically
GET https://api.legion-ai.org/v1/models
Returns an OpenAI-compatible model list including media models. No authentication required.

GET https://api.legion-ai.org/v1/models/catalog
Returns the active Legion catalog with active public defaults plus working-model options used internally by prompt optimization and intent routing. Speech model types are included with `type: "speech_to_text"` and `type: "text_to_speech"`.

Example:
curl https://api.legion-ai.org/v1/models
// Response: { "object": "list", "data": [{ "id": "gemini-2.5-flash", "object": "model", "type": "chat", ... }, { "id": "nano-banana-2", "type": "image_generation", ... }, ...] }

---

Media Generation

POST https://api.legion-ai.org/v1/media/generations
Generate images. Requires a valid Bearer token.

Request body:

  • type (required): "image"
  • prompt (required): Description of the media to generate
  • model (optional): Specific model to use; falls back to app default then platform default
  • thread_messages (optional): Recent chat turns to preserve continuity for follow-up requests like "make another one like the last image"

For content-grounded image requests such as infographics, diagrams, timelines, or "make a visual from this paper", Legion first builds a structured visual brief from the recent chat thread and any retrieved app knowledge-base content, then converts that brief into the final image prompt.

Media generation is async — returns status "processing" with an id. Poll GET /v1/media/generations/{id} until status is "complete".

Video generation is temporarily unavailable during launch. Requests to POST /v1/media/generations with type "video" or to create a video infographic job return 503 with error "video_generation_unavailable". Existing video job status reads remain available for historical jobs.

Video infographic creation code is retained but disconnected at launch:

SDK usage:

const result = await legion.generateMedia("a sunset over the ocean", { type: "image" });
console.log(result.data[0].url);

Intent Classification

POST https://api.legion-ai.org/v1/media/classify
Determines whether a user message is asking for text, image generation, or media search. Video creation requests are classified as text while launch video generation is suspended. Uses gemini-2.5-flash-lite for fast, accurate classification.

Request: { "message": "draw me a cat on a skateboard" } Response: { "intent": "generate_image" } — one of "text", "generate_image", "generate_infographic", "find_image", "find_video"

Prompt Optimization & Context

All prompts are automatically refined using gemini-2.5-flash-lite before being sent to the generation model. Standard media requests use the same user context and app knowledge base (RAG) as the text path. Content-grounded visuals use the recent chat thread plus app RAG to extract only the relevant source material before the final image prompt is written. If the request clearly depends on source material ("this paper", "above", "this conversation") and no grounding content is available, the API returns 422 insufficient_source_context instead of generating a generic visual. The optimized prompt is returned as revised_prompt.


View plain text
POST https://api.legion-ai.org/v1/media/generations
Generate images. Requires a valid Bearer token.

Request body:
- type (required): "image"
- prompt (required): Description of the media to generate
- model (optional): Specific model to use; falls back to app default then platform default
- thread_messages (optional): Recent chat turns to preserve continuity for follow-up requests like "make another one like the last image"

For content-grounded image requests such as infographics, diagrams, timelines, or "make a visual from this paper", Legion first builds a structured visual brief from the recent chat thread and any retrieved app knowledge-base content, then converts that brief into the final image prompt.

Media generation is async — returns status "processing" with an id. Poll GET /v1/media/generations/{id} until status is "complete".

Video generation is temporarily unavailable during launch. Requests to POST /v1/media/generations with type "video" or to create a video infographic job return 503 with error "video_generation_unavailable". Existing video job status reads remain available for historical jobs.

Video infographic creation code is retained but disconnected at launch:
- POST https://api.legion-ai.org/v1/video-jobs
- GET https://api.legion-ai.org/v1/video-jobs/{id}
- Supports Idempotency-Key for safe client retries
- Status is durable: if the fast Redis cache expires, Legion falls back to the database record instead of treating the job as missing

SDK usage:
const result = await legion.generateMedia("a sunset over the ocean", { type: "image" });
console.log(result.data[0].url);

### Intent Classification
POST https://api.legion-ai.org/v1/media/classify
Determines whether a user message is asking for text, image generation, or media search. Video creation requests are classified as text while launch video generation is suspended. Uses gemini-2.5-flash-lite for fast, accurate classification.

Request: { "message": "draw me a cat on a skateboard" }
Response: { "intent": "generate_image" }  — one of "text", "generate_image", "generate_infographic", "find_image", "find_video"

### Prompt Optimization & Context
All prompts are automatically refined using gemini-2.5-flash-lite before being sent to the generation model. Standard media requests use the same user context and app knowledge base (RAG) as the text path. Content-grounded visuals use the recent chat thread plus app RAG to extract only the relevant source material before the final image prompt is written. If the request clearly depends on source material ("this paper", "above", "this conversation") and no grounding content is available, the API returns 422 insufficient_source_context instead of generating a generic visual. The optimized prompt is returned as revised_prompt.

---

Audio

Legion exposes speech-to-text and text-to-speech as separate API calls. Legion does not orchestrate microphone capture, speaker playback, or conversational voice mode for your platform. Your app records audio or plays audio locally, and Legion handles the authenticated AI call plus billing.

Speech-to-Text

POST https://api.legion-ai.org/v1/audio/transcriptions

OpenAI-compatible multipart upload endpoint.

Form fields:

  • file (required): audio file upload
  • model (optional): defaults to app default default_speech_to_text_model, otherwise Legion default gemini-2.5-flash
  • prompt (optional): extra transcription guidance
  • response_format (optional): json or text

Response:

  • json: { "text": "..." }
  • text: plain transcription text

Text-to-Speech

POST https://api.legion-ai.org/v1/audio/speech

OpenAI-compatible speech generation endpoint.

JSON body:

  • input (required): text to synthesize
  • model (optional): defaults to app default default_text_to_speech_model, otherwise Legion default gemini-2.5-flash-preview-tts
  • voice (optional): provider voice hint
  • response_format (optional): requested output format

Response:

  • raw audio bytes with an audio Content-Type

App-Level Speech Defaults

Each app can set speech defaults independently in the developer dashboard:

  • default_speech_to_text_model
  • default_text_to_speech_model

Legion resolves speech models in this order:

  1. Request-level model
  2. App default speech model
  3. Legion platform default

View plain text
Legion exposes speech-to-text and text-to-speech as separate API calls. Legion does not orchestrate microphone capture, speaker playback, or conversational voice mode for your platform. Your app records audio or plays audio locally, and Legion handles the authenticated AI call plus billing.

### Speech-to-Text
POST https://api.legion-ai.org/v1/audio/transcriptions

OpenAI-compatible multipart upload endpoint.

Form fields:
- file (required): audio file upload
- model (optional): defaults to app default `default_speech_to_text_model`, otherwise Legion default `gemini-2.5-flash`
- prompt (optional): extra transcription guidance
- response_format (optional): `json` or `text`

Response:
- `json`: `{ "text": "..." }`
- `text`: plain transcription text

### Text-to-Speech
POST https://api.legion-ai.org/v1/audio/speech

OpenAI-compatible speech generation endpoint.

JSON body:
- input (required): text to synthesize
- model (optional): defaults to app default `default_text_to_speech_model`, otherwise Legion default `gemini-2.5-flash-preview-tts`
- voice (optional): provider voice hint
- response_format (optional): requested output format

Response:
- raw audio bytes with an audio `Content-Type`

### App-Level Speech Defaults
Each app can set speech defaults independently in the developer dashboard:
- `default_speech_to_text_model`
- `default_text_to_speech_model`

Legion resolves speech models in this order:
1. Request-level `model`
2. App default speech model
3. Legion platform default

---

User Context Injection (ON by Default)

Every API call automatically enriches the AI request with user-specific context for personalized responses. Three flags control what is injected — all default to true. To opt out, set a flag to false explicitly in your request body.

Flag: user_preferences (default: true)

  • Source: User's AI Preferences setting
  • Injects: Freeform preference text (e.g., "I prefer brief answers with sources")
  • Opt out: set user_preferences: false

Flag: user_information (default: true)

  • Source: User's profile demographics
  • Injects: Age, sex, language, location, occupation, industry
  • Opt out: set user_information: false

Flag: user_context (default: true)

  • Source: User's query history (last 90 days, across all apps)
  • Injects: Top 5 topics, intent distribution %, primary language, avg query length, total queries
  • Opt out: set user_context: false

How it works:

  • Context is appended to the system message before forwarding to the AI provider
  • If no system message exists, one is created
  • Empty sections are skipped automatically
  • If user has no profile data, nothing is injected
  • Zero additional latency when all flags are set to false
  • Results are cached for 5 minutes

SDK Example

const response = await legion.chat(
  [{ role: 'user', content: 'Help me plan my week' }],
  {
    model: 'gemini-2.5-flash',
    user_preferences: true,
    user_information: true,
    user_context: true,
  }
);
const transcription = await legion.transcribeAudio(file, {
  response_format: 'json',
});
const speech = await legion.generateSpeech('Hello from Legion', {
  voice: 'Puck',
});

Direct API Example

body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'Help me plan my week' }],
  user_preferences: true,
  user_information: true,
  user_context: true,
})

View plain text
Every API call automatically enriches the AI request with user-specific context for personalized responses. Three flags control what is injected — all default to true. To opt out, set a flag to false explicitly in your request body.

Flag: user_preferences (default: true)
- Source: User's AI Preferences setting
- Injects: Freeform preference text (e.g., "I prefer brief answers with sources")
- Opt out: set user_preferences: false

Flag: user_information (default: true)
- Source: User's profile demographics
- Injects: Age, sex, language, location, occupation, industry
- Opt out: set user_information: false

Flag: user_context (default: true)
- Source: User's query history (last 90 days, across all apps)
- Injects: Top 5 topics, intent distribution %, primary language, avg query length, total queries
- Opt out: set user_context: false

How it works:
- Context is appended to the system message before forwarding to the AI provider
- If no system message exists, one is created
- Empty sections are skipped automatically
- If user has no profile data, nothing is injected
- Zero additional latency when all flags are set to false
- Results are cached for 5 minutes

### SDK Example
const response = await legion.chat(
  [{ role: 'user', content: 'Help me plan my week' }],
  {
    model: 'gemini-2.5-flash',
    user_preferences: true,
    user_information: true,
    user_context: true,
  }
);

const transcription = await legion.transcribeAudio(file, {
  response_format: 'json',
});

const speech = await legion.generateSpeech('Hello from Legion', {
  voice: 'Puck',
});

### Direct API Example
body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'Help me plan my week' }],
  user_preferences: true,
  user_information: true,
  user_context: true,
})

---

Platform-Funded Mode (Developer Pays)

Developers can pay for AI usage on behalf of their users by passing platform_funded: true in the request body. When enabled, the user is not charged — the developer absorbs the cost (model cost + infra allocation) at cost with no markup.

Requirements

  • The developer must have a payment method on file (Stripe).
  • If no payment method is found, the API returns 402 developer_payment_required.

Request Example

body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'Hello!' }],
  platform_funded: true,
})

Response

The _legion object in the response will include:

  • platform_funded: true
  • cost_cents: 0 (user pays nothing)
  • developer_charge_cents: total cost absorbed by the developer

Billing

Platform-funded transactions are recorded with billed=false and collected via the developer's monthly invoice. The developer_charge_cents column tracks the amount owed.


View plain text
Developers can pay for AI usage on behalf of their users by passing platform_funded: true in the request body. When enabled, the user is not charged — the developer absorbs the cost (model cost + infra allocation) at cost with no markup.

### Requirements
- The developer must have a payment method on file (Stripe).
- If no payment method is found, the API returns 402 developer_payment_required.

### Request Example
body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'Hello!' }],
  platform_funded: true,
})

### Response
The _legion object in the response will include:
- platform_funded: true
- cost_cents: 0 (user pays nothing)
- developer_charge_cents: total cost absorbed by the developer

### Billing
Platform-funded transactions are recorded with billed=false and collected via the developer's monthly invoice. The developer_charge_cents column tracks the amount owed.

---

Partner Attribution

Accredited partners can receive a share of Legion's platform margin without increasing the user's cost or reducing the app owner's margin.

Attribution is app-level and editable. When an app is created by a claimed partner account, Legion adds that partner slug to the app's partner list. Non-partner owners may optionally provide a valid partner slug when registering the app; unknown partner slugs are rejected. App owners and active app partners can add or remove accredited partners later. Runtime chat and media calls do not set or override partner attribution.

Eligibility and Accreditation

Accredited partners are approved by Legion and listed in Legion's server-side partner registry. Examples include vibe-coding platforms and AI app-building tools. Eligibility is not self-serve: partner-claim succeeds only when the signed-in account's Clerk verified primary email exactly matches an email in the approved registry entry.

Security notes:

  • Partner claim checks Clerk directly for the verified primary email.
  • Legion does not trust the synced Postgres users.email value for partner or owner claim decisions.
  • A partner slug can only be claimed by one Legion user account.
  • A partner keeps app-admin access only while its slug remains attached to the app.

Revenue Split

Legion adds a fixed 20% platform margin on model cost. Partner share is calculated from that Legion platform margin:

  • No affiliate, no partner: Legion keeps 100% of the platform margin
  • Affiliate only: affiliate receives 50%, Legion keeps 50%
  • Partner only: attached partners share 50%, Legion keeps 50%
  • Affiliate and partner: affiliate receives 50%, attached partners share 50%, Legion keeps 0% of the platform margin
  • Unaccredited or unknown partner slug: treated as no partner

The app owner's configured owner margin is separate. Partner attribution does not reduce the app owner's margin and does not increase the end user's price.

When multiple partners are attached to one app, they split the partner pool evenly. Two partners split it in half; three partners split it in thirds. Any odd cent remainder is assigned deterministically.

Partner CLI Workflow

Already-accredited partners can manage client apps from the CLI:

  1. legion login
  2. legion partner-claim
  3. legion partner-status
  4. legion create-app --name "Client Site" --redirect-uris "https://client.com/callback"
  5. legion invite-owner <client_id> --email client@example.com

Use legion partner-connect-start to start Stripe Connect onboarding for partner payouts. The first call can create a Stripe Connect Express account before returning an onboarding URL. Use legion partner-connect-status to check payout verification.

After partner-claim succeeds, new apps created by that account automatically include that partner slug. The optional --partner flag on create-app is for manual attribution by non-partner owners and is not required for claimed partner accounts.

Partner API Reference

All partner endpoints are on the auth server: https://auth.legion-ai.org.

Authentication:

  • Dashboard calls use Clerk authentication.
  • CLI calls use the Legion personal access token saved by legion login.
  • Partner claim still verifies eligibility through Clerk's verified primary email even when called with a CLI token.

POST /v1/partner/claim

  • Claims the accredited partner slug for the authenticated account.
  • Requires the authenticated user's verified Clerk primary email to appear in the partner registry.
  • Returns partner_slug, name, and category.
  • Common errors: 404 not_eligible, 409 already_claimed.

GET /v1/partner/me

  • Returns partner_slug, name, category, pending_cents, total_earned_cents, last_payout_at, connect_onboarded, connect_verified, attributed apps, and per-app earnings breakdown.
  • Common errors: 404 not_partner.

POST /v1/partner/connect/onboard

  • Starts Stripe Connect Express onboarding for partner payouts.
  • Side effect: on the first call, Legion can create a Stripe Connect account before returning the onboarding URL.
  • Returns url, account_id, and whether an existing Connect account was reused.
  • Common errors: 404 not_partner.

GET /v1/partner/connect/status

  • Checks the partner Stripe Connect account status and updates Legion's cached verification state.
  • Returns connected, verified, charges_enabled, payouts_enabled, details_submitted, and status.
  • Common errors: 404 not_partner.

GET /v1/apps/:clientId/partners

  • Lists active partner slugs for an app.
  • Available to the app owner and active app partners.

POST /v1/apps/:clientId/partners

  • Adds an accredited partner slug to an app.
  • Body: { "partner": "lovable" }
  • Idempotent for an already attached partner.
  • Common errors: 400 validation_error, 404 App not found or access denied.

DELETE /v1/apps/:clientId/partners/:partnerSlug

  • Removes a partner slug from an app.
  • Removing the final partner is allowed; after that, only the app owner can add partners back.

Partner CLI Commands

legion partner-claim
- Claim the accredited partner account for the logged-in Legion user.
legion partner-status
- Show partner balance, attributed apps, and per-app earnings.
legion partner-connect-start
- Start Stripe Connect onboarding for partner payouts. The first call may create a Stripe Connect account.
legion partner-connect-status
- Check partner payout verification status.
legion list-partners <client_id>
- List partners attached to an app.
legion add-partner <client_id> --partner lovable
- Add an accredited partner to an app.
legion remove-partner <client_id> --partner lovable
- Remove a partner from an app.
legion create-app --name "Client Site" --redirect-uris "https://client.com/callback"
- Create a client app. Claimed partner accounts are attached automatically.
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --partner lovable
- Optional manual attribution for non-partner developers. Claimed partners normally do not need --partner.

View plain text
Accredited partners can receive a share of Legion's platform margin without increasing the user's cost or reducing the app owner's margin.

Attribution is app-level and editable. When an app is created by a claimed partner account, Legion adds that partner slug to the app's partner list. Non-partner owners may optionally provide a valid partner slug when registering the app; unknown partner slugs are rejected. App owners and active app partners can add or remove accredited partners later. Runtime chat and media calls do not set or override partner attribution.

### Eligibility and Accreditation

Accredited partners are approved by Legion and listed in Legion's server-side partner registry. Examples include vibe-coding platforms and AI app-building tools. Eligibility is not self-serve: partner-claim succeeds only when the signed-in account's Clerk verified primary email exactly matches an email in the approved registry entry.

Security notes:
- Partner claim checks Clerk directly for the verified primary email.
- Legion does not trust the synced Postgres users.email value for partner or owner claim decisions.
- A partner slug can only be claimed by one Legion user account.
- A partner keeps app-admin access only while its slug remains attached to the app.

### Revenue Split

Legion adds a fixed 20% platform margin on model cost. Partner share is calculated from that Legion platform margin:

- No affiliate, no partner: Legion keeps 100% of the platform margin
- Affiliate only: affiliate receives 50%, Legion keeps 50%
- Partner only: attached partners share 50%, Legion keeps 50%
- Affiliate and partner: affiliate receives 50%, attached partners share 50%, Legion keeps 0% of the platform margin
- Unaccredited or unknown partner slug: treated as no partner

The app owner's configured owner margin is separate. Partner attribution does not reduce the app owner's margin and does not increase the end user's price.

When multiple partners are attached to one app, they split the partner pool evenly. Two partners split it in half; three partners split it in thirds. Any odd cent remainder is assigned deterministically.

### Partner CLI Workflow

Already-accredited partners can manage client apps from the CLI:

1. legion login
2. legion partner-claim
3. legion partner-status
4. legion create-app --name "Client Site" --redirect-uris "https://client.com/callback"
5. legion invite-owner <client_id> --email client@example.com

Use legion partner-connect-start to start Stripe Connect onboarding for partner payouts. The first call can create a Stripe Connect Express account before returning an onboarding URL. Use legion partner-connect-status to check payout verification.

After partner-claim succeeds, new apps created by that account automatically include that partner slug. The optional --partner flag on create-app is for manual attribution by non-partner owners and is not required for claimed partner accounts.

### Partner API Reference

All partner endpoints are on the auth server: https://auth.legion-ai.org.

Authentication:
- Dashboard calls use Clerk authentication.
- CLI calls use the Legion personal access token saved by legion login.
- Partner claim still verifies eligibility through Clerk's verified primary email even when called with a CLI token.

POST /v1/partner/claim
- Claims the accredited partner slug for the authenticated account.
- Requires the authenticated user's verified Clerk primary email to appear in the partner registry.
- Returns partner_slug, name, and category.
- Common errors: 404 not_eligible, 409 already_claimed.

GET /v1/partner/me
- Returns partner_slug, name, category, pending_cents, total_earned_cents, last_payout_at, connect_onboarded, connect_verified, attributed apps, and per-app earnings breakdown.
- Common errors: 404 not_partner.

POST /v1/partner/connect/onboard
- Starts Stripe Connect Express onboarding for partner payouts.
- Side effect: on the first call, Legion can create a Stripe Connect account before returning the onboarding URL.
- Returns url, account_id, and whether an existing Connect account was reused.
- Common errors: 404 not_partner.

GET /v1/partner/connect/status
- Checks the partner Stripe Connect account status and updates Legion's cached verification state.
- Returns connected, verified, charges_enabled, payouts_enabled, details_submitted, and status.
- Common errors: 404 not_partner.

GET /v1/apps/:clientId/partners
- Lists active partner slugs for an app.
- Available to the app owner and active app partners.

POST /v1/apps/:clientId/partners
- Adds an accredited partner slug to an app.
- Body: { "partner": "lovable" }
- Idempotent for an already attached partner.
- Common errors: 400 validation_error, 404 App not found or access denied.

DELETE /v1/apps/:clientId/partners/:partnerSlug
- Removes a partner slug from an app.
- Removing the final partner is allowed; after that, only the app owner can add partners back.

### Partner CLI Commands

legion partner-claim
- Claim the accredited partner account for the logged-in Legion user.

legion partner-status
- Show partner balance, attributed apps, and per-app earnings.

legion partner-connect-start
- Start Stripe Connect onboarding for partner payouts. The first call may create a Stripe Connect account.

legion partner-connect-status
- Check partner payout verification status.

legion list-partners <client_id>
- List partners attached to an app.

legion add-partner <client_id> --partner lovable
- Add an accredited partner to an app.

legion remove-partner <client_id> --partner lovable
- Remove a partner from an app.

legion create-app --name "Client Site" --redirect-uris "https://client.com/callback"
- Create a client app. Claimed partner accounts are attached automatically.

legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --partner lovable
- Optional manual attribution for non-partner developers. Claimed partners normally do not need --partner.

---

Delegated Ownership

Agency or partner-managed apps can assign a separate client owner for payouts. The developer keeps app management access, while the owner only gets read-only earnings and Stripe Connect setup for owner-margin payouts.

Developers invite an owner from the app's Admin area. Legion emails a claim link to the owner. The owner must sign in with the same verified email address, claim the website, then use My websites to view earnings and set up Stripe Connect.

Partners and developers can also send or cancel owner invites from the CLI:

legion invite-owner <client_id> --email client@example.com
legion cancel-owner-invite <client_id>

Client owners should use the emailed web claim page and My websites payout flow; the CLI does not expose client-owner claim or payout commands.

After an owner is assigned, the developer can no longer attach their own Stripe Connect account for that app's owner payouts. Reassignment is handled manually by Legion support.

Delegated Ownership API Reference

POST /v1/apps/:clientId/owner-invite

  • Invites a client owner by email for an app managed by the authenticated developer or partner.
  • Authentication: Clerk or Legion personal access token.
  • Body: { "email": "client@example.com" }
  • Sends an email containing a claim link to https://legion-ai.org/claim-ownership?token=...
  • Returns invite metadata, email_sent, and email_status.
  • Common errors: 400 validation_error, 404 App not found or access denied, 409 invite_pending, 409 owner_already_assigned.

DELETE /v1/apps/:clientId/owner-invite

  • Cancels the active pending unclaimed owner invite for an app.
  • Authentication: Clerk or Legion personal access token.
  • Returns { "cancelled": true } when an invite was cancelled, or false when no active invite existed.

POST /v1/owner-claims/accept

  • Used by the web claim page, not by the CLI.
  • The invitee must sign in with the same verified Clerk primary email that received the invite.
  • Body: { "token": "claim-token-from-email" }
  • Common errors: 403 email_mismatch, 404 invalid_or_expired, 409 owner_already_assigned.

GET /v1/owned-apps

  • Powers the owner's My websites page.
  • Returns read-only app earnings and payout status for apps where the caller is the delegated owner.

POST /v1/owned-apps/:clientId/connect/onboard

  • Starts Stripe Connect onboarding for an app owner payout account.
  • Used by the My websites web flow.

GET /v1/owned-apps/:clientId/connect/status

  • Checks owner payout verification for an owned app.

Delegated Ownership Boundaries

Client owners receive only payout visibility and payout onboarding for owner-margin earnings. They do not receive app keys, client secrets, redirect URI controls, model settings, knowledge base controls, logs, deletion, rotation, or developer/operator access.

Partners and developers keep app management access after assigning an owner. Owner reassignment and support recovery are manual Legion support operations.

Owner Invite CLI Commands

legion invite-owner <client_id> --email client@example.com
- Sends the owner claim email for an app you manage.
legion cancel-owner-invite <client_id>
- Cancels the active pending claim email for an app you manage.

The CLI reports common owner invite conflicts with actionable messages:

  • invite_pending: cancel the existing invite before sending another.
  • owner_already_assigned: the app already has a delegated owner; contact Legion support for reassignment.

View plain text
Agency or partner-managed apps can assign a separate client owner for payouts. The developer keeps app management access, while the owner only gets read-only earnings and Stripe Connect setup for owner-margin payouts.

Developers invite an owner from the app's Admin area. Legion emails a claim link to the owner. The owner must sign in with the same verified email address, claim the website, then use My websites to view earnings and set up Stripe Connect.

Partners and developers can also send or cancel owner invites from the CLI:

legion invite-owner <client_id> --email client@example.com
legion cancel-owner-invite <client_id>

Client owners should use the emailed web claim page and My websites payout flow; the CLI does not expose client-owner claim or payout commands.

After an owner is assigned, the developer can no longer attach their own Stripe Connect account for that app's owner payouts. Reassignment is handled manually by Legion support.

### Delegated Ownership API Reference

POST /v1/apps/:clientId/owner-invite
- Invites a client owner by email for an app managed by the authenticated developer or partner.
- Authentication: Clerk or Legion personal access token.
- Body: { "email": "client@example.com" }
- Sends an email containing a claim link to https://legion-ai.org/claim-ownership?token=...
- Returns invite metadata, email_sent, and email_status.
- Common errors: 400 validation_error, 404 App not found or access denied, 409 invite_pending, 409 owner_already_assigned.

DELETE /v1/apps/:clientId/owner-invite
- Cancels the active pending unclaimed owner invite for an app.
- Authentication: Clerk or Legion personal access token.
- Returns { "cancelled": true } when an invite was cancelled, or false when no active invite existed.

POST /v1/owner-claims/accept
- Used by the web claim page, not by the CLI.
- The invitee must sign in with the same verified Clerk primary email that received the invite.
- Body: { "token": "claim-token-from-email" }
- Common errors: 403 email_mismatch, 404 invalid_or_expired, 409 owner_already_assigned.

GET /v1/owned-apps
- Powers the owner's My websites page.
- Returns read-only app earnings and payout status for apps where the caller is the delegated owner.

POST /v1/owned-apps/:clientId/connect/onboard
- Starts Stripe Connect onboarding for an app owner payout account.
- Used by the My websites web flow.

GET /v1/owned-apps/:clientId/connect/status
- Checks owner payout verification for an owned app.

### Delegated Ownership Boundaries

Client owners receive only payout visibility and payout onboarding for owner-margin earnings. They do not receive app keys, client secrets, redirect URI controls, model settings, knowledge base controls, logs, deletion, rotation, or developer/operator access.

Partners and developers keep app management access after assigning an owner. Owner reassignment and support recovery are manual Legion support operations.

### Owner Invite CLI Commands

legion invite-owner <client_id> --email client@example.com
- Sends the owner claim email for an app you manage.

legion cancel-owner-invite <client_id>
- Cancels the active pending claim email for an app you manage.

The CLI reports common owner invite conflicts with actionable messages:
- invite_pending: cancel the existing invite before sending another.
- owner_already_assigned: the app already has a delegated owner; contact Legion support for reassignment.

---

App Knowledge Base (RAG Context)

Give your app domain-specific knowledge by uploading files. Content is automatically chunked, embedded, and retrieved at query time via hybrid search (semantic + keyword with Reciprocal Rank Fusion).

Supported File Types

  • PDF (.pdf)
  • Word (.docx)
  • PowerPoint (.pptx)
  • Video (.mp4) — embedded as single unit
  • Audio (.mp3) — embedded as single unit

Upload Flow

  1. Go to your app's detail page in the developer dashboard
  2. Drag and drop files into the Knowledge Base section
  3. Wait for status to change from "processing" to "ready"
  4. Each file gets a context_id (e.g., ctx_abc123...) that you can copy

Using context_ids in API Calls

By default, all app context is searched for every API call. To limit search to specific files, pass context_ids:

SDK Example

const response = await legion.chat(
  [{ role: 'user', content: 'What does our return policy say?' }],
  {
    model: 'gemini-2.5-flash',
    context_ids: ['ctx_abc123def456', 'ctx_789xyz'],
  }
);

Direct API Example

body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'What does our return policy say?' }],
  context_ids: ['ctx_abc123def456'],
})

How It Works

  • Files are chunked (~1000 tokens with overlap) and embedded using Gemini Embedding 2
  • At query time, the user's message is embedded and used for hybrid search (pgvector cosine + tsvector keyword)
  • Top 5 matching chunks are injected into the system message as "[App Knowledge Base]"
  • Results are cached for 60 seconds
  • If no context exists for the app, the search is skipped entirely (zero latency impact)

Programmatic Upload (via API)

POST /v1/apps/:clientId/context/upload-url — get presigned upload URL POST /v1/apps/:clientId/context/confirm — confirm upload, trigger processing GET /v1/apps/:clientId/context — list all context items GET /v1/apps/:clientId/context/:contextId — get item status DELETE /v1/apps/:clientId/context/:contextId — delete item and chunks


View plain text
Give your app domain-specific knowledge by uploading files. Content is automatically chunked, embedded, and retrieved at query time via hybrid search (semantic + keyword with Reciprocal Rank Fusion).

### Supported File Types
- PDF (.pdf)
- Word (.docx)
- PowerPoint (.pptx)
- Video (.mp4) — embedded as single unit
- Audio (.mp3) — embedded as single unit

### Upload Flow
1. Go to your app's detail page in the developer dashboard
2. Drag and drop files into the Knowledge Base section
3. Wait for status to change from "processing" to "ready"
4. Each file gets a context_id (e.g., ctx_abc123...) that you can copy

### Using context_ids in API Calls
By default, all app context is searched for every API call. To limit search to specific files, pass context_ids:

#### SDK Example
const response = await legion.chat(
  [{ role: 'user', content: 'What does our return policy say?' }],
  {
    model: 'gemini-2.5-flash',
    context_ids: ['ctx_abc123def456', 'ctx_789xyz'],
  }
);

#### Direct API Example
body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'What does our return policy say?' }],
  context_ids: ['ctx_abc123def456'],
})

### How It Works
- Files are chunked (~1000 tokens with overlap) and embedded using Gemini Embedding 2
- At query time, the user's message is embedded and used for hybrid search (pgvector cosine + tsvector keyword)
- Top 5 matching chunks are injected into the system message as "[App Knowledge Base]"
- Results are cached for 60 seconds
- If no context exists for the app, the search is skipped entirely (zero latency impact)

### Programmatic Upload (via API)
POST /v1/apps/:clientId/context/upload-url — get presigned upload URL
POST /v1/apps/:clientId/context/confirm — confirm upload, trigger processing
GET  /v1/apps/:clientId/context — list all context items
GET  /v1/apps/:clientId/context/:contextId — get item status
DELETE /v1/apps/:clientId/context/:contextId — delete item and chunks

---

SDK Quick Start

Install the SDK

npm install @legionai/sdk

Basic Usage

import { createLegion } from '@legionai/sdk';
const legion = createLegion({
  clientId: 'YOUR_CLIENT_ID',
  // Optional: callback when tokens are auto-refreshed (persist new tokens)
  onTokenRefresh: (tokens) => saveTokens(tokens),
});
// Connect user's wallet (redirectUri is passed here, not in createLegion)
legion.connect({ redirectUri: 'https://yourapp.com/callback' });
// After user returns to your callback, exchange code for tokens
const state = new URLSearchParams(window.location.search).get('state');
const tokens = await legion.exchangeCode(code, { state });
// tokens = { access_token, refresh_token, token_type, expires_in }
// Make API calls (uses your app default model if set; otherwise gemini-2.5-flash during launch)
const response = await legion.chat([
  { role: 'user', content: 'Hello!' }
]);
// Specify a different active launch model
const response2 = await legion.chat(
  [{ role: 'user', content: 'Hello!' }],
  { model: 'gemini-2.5-pro' }
);

View plain text
### Install the SDK
npm install @legionai/sdk

### Basic Usage
import { createLegion } from '@legionai/sdk';

const legion = createLegion({
  clientId: 'YOUR_CLIENT_ID',
  // Optional: callback when tokens are auto-refreshed (persist new tokens)
  onTokenRefresh: (tokens) => saveTokens(tokens),
});

// Connect user's wallet (redirectUri is passed here, not in createLegion)
legion.connect({ redirectUri: 'https://yourapp.com/callback' });

// After user returns to your callback, exchange code for tokens
const state = new URLSearchParams(window.location.search).get('state');
const tokens = await legion.exchangeCode(code, { state });
// tokens = { access_token, refresh_token, token_type, expires_in }

// Make API calls (uses your app default model if set; otherwise gemini-2.5-flash during launch)
const response = await legion.chat([
  { role: 'user', content: 'Hello!' }
]);

// Specify a different active launch model
const response2 = await legion.chat(
  [{ role: 'user', content: 'Hello!' }],
  { model: 'gemini-2.5-pro' }
);

---

Next.js Quick Start (@legionai/next)

Drop-in OAuth integration for Next.js App Router apps. Handles the full connect flow, stores tokens in httpOnly cookies, and provides React hooks for connection state.

Install

npm install @legionai/next @legionai/sdk

1. Route Handler — app/api/auth/[...legion]/route.js

import { createLegionHandler } from '@legionai/next';
export const { GET, POST } = createLegionHandler({
  clientId: process.env.LEGION_CLIENT_ID,
  afterCallback: '/dashboard', // where to redirect after OAuth
});
// @legionai/next always uses S256 PKCE. Do not add a client secret here.
// The generated callback URL must still be registered as a redirect URI.

2. Layout — app/layout.js

import { LegionProvider } from '@legionai/next';
export default function Layout({ children }) {
  return <html><body><LegionProvider>{children}</LegionProvider></body></html>;
}

3. Connect Button — any client component

'use client';

import { useLegion } from '@legionai/next';
export function ConnectButton() {
  const { connected, loading, connect, disconnect } = useLegion();
  if (loading) return null;
  return (
    <button onClick={connected ? disconnect : connect}>
      {connected ? 'Disconnect' : 'Connect Legion'}
    </button>
  );
}

4. Custom API Route — app/api/chat/route.js

import { getLegionSession } from '@legionai/next';
export async function POST(request) {
  const session = getLegionSession(request);
  if (!session) return Response.json({ error: 'Not connected' }, { status: 401 });
  const { messages } = await request.json();
  const res = await fetch('https://api.legion-ai.org/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${session.accessToken}`,
    },
    body: JSON.stringify({ model: 'gemini-2.5-flash', messages }),
  });
  return Response.json(await res.json());
}

Handler Config Options

  • clientId (required): Your Legion app client ID
  • afterCallback (default: '/'): Redirect path after successful OAuth
  • basePath (default: '/api/auth/legion'): Route prefix for the catch-all handler
  • cookiePrefix (default: 'legion'): Prefix for cookie names
  • scope (default: 'chat:completions'): OAuth scope
  • authUrl (optional): Override the Legion auth server URL
  • consentUrl (optional): Override the hosted Legion consent URL

How It Works

The catch-all route handles four endpoints automatically:

  • GET /api/auth/legion/connect — Redirects to Legion consent page
  • GET /api/auth/legion/callback — Exchanges code for tokens, sets cookies
  • GET /api/auth/legion/status — Returns { connected: true/false }
  • POST /api/auth/legion/disconnect — Clears cookies

Tokens are stored in httpOnly cookies (never exposed to client JavaScript). The React hook only reflects connection state — it does not access tokens directly.


View plain text
Drop-in OAuth integration for Next.js App Router apps. Handles the full connect flow, stores tokens in httpOnly cookies, and provides React hooks for connection state.

### Install
npm install @legionai/next @legionai/sdk

### 1. Route Handler — app/api/auth/[...legion]/route.js
import { createLegionHandler } from '@legionai/next';

export const { GET, POST } = createLegionHandler({
  clientId: process.env.LEGION_CLIENT_ID,
  afterCallback: '/dashboard', // where to redirect after OAuth
});

// @legionai/next always uses S256 PKCE. Do not add a client secret here.
// The generated callback URL must still be registered as a redirect URI.

### 2. Layout — app/layout.js
import { LegionProvider } from '@legionai/next';

export default function Layout({ children }) {
  return <html><body><LegionProvider>{children}</LegionProvider></body></html>;
}

### 3. Connect Button — any client component
'use client';
import { useLegion } from '@legionai/next';

export function ConnectButton() {
  const { connected, loading, connect, disconnect } = useLegion();
  if (loading) return null;
  return (
    <button onClick={connected ? disconnect : connect}>
      {connected ? 'Disconnect' : 'Connect Legion'}
    </button>
  );
}

### 4. Custom API Route — app/api/chat/route.js
import { getLegionSession } from '@legionai/next';

export async function POST(request) {
  const session = getLegionSession(request);
  if (!session) return Response.json({ error: 'Not connected' }, { status: 401 });

  const { messages } = await request.json();
  const res = await fetch('https://api.legion-ai.org/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${session.accessToken}`,
    },
    body: JSON.stringify({ model: 'gemini-2.5-flash', messages }),
  });
  return Response.json(await res.json());
}

### Handler Config Options
- clientId (required): Your Legion app client ID
- afterCallback (default: '/'): Redirect path after successful OAuth
- basePath (default: '/api/auth/legion'): Route prefix for the catch-all handler
- cookiePrefix (default: 'legion'): Prefix for cookie names
- scope (default: 'chat:completions'): OAuth scope
- authUrl (optional): Override the Legion auth server URL
- consentUrl (optional): Override the hosted Legion consent URL

### How It Works
The catch-all route handles four endpoints automatically:
- GET /api/auth/legion/connect — Redirects to Legion consent page
- GET /api/auth/legion/callback — Exchanges code for tokens, sets cookies
- GET /api/auth/legion/status — Returns { connected: true/false }
- POST /api/auth/legion/disconnect — Clears cookies

Tokens are stored in httpOnly cookies (never exposed to client JavaScript). The React hook only reflects connection state — it does not access tokens directly.

---

Authentication

Token Lifecycle

  • Access tokens expire after 24 hours (86400 seconds)
  • The SDK automatically refreshes tokens on 401 errors using the refresh token
  • When tokens are refreshed, the onTokenRefresh callback is called so you can persist the new tokens
  • If refresh fails, the user must re-authorize via connect()
  • Refresh tokens are single-use — each refresh call returns a new refresh token (rotation)

Manual Token Refresh (Without SDK)

If you're not using the SDK, refresh tokens manually:

const res = await fetch('https://auth.legion-ai.org/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'refresh_token',
    refresh_token: 'YOUR_REFRESH_TOKEN',
  }),
});
const { access_token, refresh_token, expires_in } = await res.json();
// IMPORTANT: Save the new refresh_token — it replaces the old one (single-use rotation)
// client_id is not required for refresh requests

Restoring Sessions

// Load saved tokens (both access and refresh tokens)
legion.setTokens({
  accessToken: saved.access_token,
  refreshToken: saved.refresh_token,
});

View plain text
### Token Lifecycle
- Access tokens expire after 24 hours (86400 seconds)
- The SDK automatically refreshes tokens on 401 errors using the refresh token
- When tokens are refreshed, the onTokenRefresh callback is called so you can persist the new tokens
- If refresh fails, the user must re-authorize via connect()
- Refresh tokens are single-use — each refresh call returns a new refresh token (rotation)

### Manual Token Refresh (Without SDK)
If you're not using the SDK, refresh tokens manually:

const res = await fetch('https://auth.legion-ai.org/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'refresh_token',
    refresh_token: 'YOUR_REFRESH_TOKEN',
  }),
});
const { access_token, refresh_token, expires_in } = await res.json();
// IMPORTANT: Save the new refresh_token — it replaces the old one (single-use rotation)
// client_id is not required for refresh requests

### Restoring Sessions
// Load saved tokens (both access and refresh tokens)
legion.setTokens({
  accessToken: saved.access_token,
  refreshToken: saved.refresh_token,
});

---

Error Handling

StatusCodeAction
401invalid_tokenToken expired. SDK auto-refreshes; if that fails, call connect()
403connection_inactiveUser deactivated. Call connect() to reauthorize
402limit_exceededCall connect({ authorizationIntent: 'increase_cap' })
402payment_requiredCall connect({ authorizationIntent: 'payment_method' })

Recommended Pattern

For any authentication or connection error, call connect() to restart the OAuth flow:

try {
  const response = await legion.chat(messages);
} catch (error) {
  if (error.requiresReauth() || error.isDeactivated()) {
    // Reconnect - this handles reauth and reactivates deactivated connections
    legion.connect({ redirectUri: 'https://yourapp.com/callback' });
  } else if (error.isCapError()) {
    legion.connect({
      redirectUri: 'https://yourapp.com/callback',
      authorizationIntent: 'increase_cap',
    });
  } else if (error.isPaymentMethodError()) {
    legion.connect({
      redirectUri: 'https://yourapp.com/callback',
      authorizationIntent: 'payment_method',
    });
  }
}

View plain text
Status | Code | Action
401 | invalid_token | Token expired. SDK auto-refreshes; if that fails, call connect()
403 | connection_inactive | User deactivated. Call connect() to reauthorize
402 | limit_exceeded | Call connect({ authorizationIntent: 'increase_cap' })
402 | payment_required | Call connect({ authorizationIntent: 'payment_method' })

### Recommended Pattern
For any authentication or connection error, call connect() to restart the OAuth flow:

try {
  const response = await legion.chat(messages);
} catch (error) {
  if (error.requiresReauth() || error.isDeactivated()) {
    // Reconnect - this handles reauth and reactivates deactivated connections
    legion.connect({ redirectUri: 'https://yourapp.com/callback' });
  } else if (error.isCapError()) {
    legion.connect({
      redirectUri: 'https://yourapp.com/callback',
      authorizationIntent: 'increase_cap',
    });
  } else if (error.isPaymentMethodError()) {
    legion.connect({
      redirectUri: 'https://yourapp.com/callback',
      authorizationIntent: 'payment_method',
    });
  }
}

---

Connection States

Active: Normal operation. Make API calls freely. Inactive: User deactivated. Call connect() - reauthorization automatically reactivates. Revoked: Permanently disconnected. Call connect() to start fresh.


View plain text
Active: Normal operation. Make API calls freely.
Inactive: User deactivated. Call connect() - reauthorization automatically reactivates.
Revoked: Permanently disconnected. Call connect() to start fresh.

---

API Reference

createLegion(config)

legion.connect(options)

  • Start OAuth flow (redirects user to consent page).
  • Options: redirectUri, state, scope (default: 'chat:completions'), popup, authorizationIntent (authorize, increase_cap, payment_method)

legion.exchangeCode(code, { state })

  • Exchange authorization code for tokens.
  • Public clients retrieve the PKCE verifier associated with the returned state and send it as code_verifier.
  • Token exchange sends grant_type, code, client_id, and code_verifier. It does not send redirect_uri or client_secret.
  • Sending only grant_type, code, and client_id is not sufficient for public-client codes created with PKCE.
  • Advanced backend clients may instead configure clientSecret and exchange a code issued without PKCE.
  • Returns: { access_token, refresh_token, token_type, expires_in }

legion.setTokens({ accessToken, refreshToken })

  • Set tokens directly (e.g., from storage).

legion.chat(messages, options)

  • Make chat completion request.
  • Default model: your app default if set, otherwise gemini-2.5-flash during launch. Auto-refreshes token on 401.
  • Options: model, temperature, max_tokens, autoRefresh (default: true), user_preferences, user_information, user_context, context_ids, platform_funded

legion.refreshAccessToken()

  • Manually refresh access token using refresh token.
  • Calls onTokenRefresh callback with new tokens.

legion.isConnected()

  • Returns true if SDK has an access token.

legion.disconnect()

  • Clear all tokens from SDK.

View plain text
createLegion(config)
- Create a Legion SDK instance.
- Config: clientId, authUrl (default: https://auth.legion-ai.org), connectUrl (default: https://legion-ai.org/connect), apiUrl (default: https://api.legion-ai.org), onTokenRefresh

legion.connect(options)
- Start OAuth flow (redirects user to consent page).
- Options: redirectUri, state, scope (default: 'chat:completions'), popup, authorizationIntent (authorize, increase_cap, payment_method)

legion.exchangeCode(code, { state })
- Exchange authorization code for tokens.
- Public clients retrieve the PKCE verifier associated with the returned state and send it as code_verifier.
- Token exchange sends grant_type, code, client_id, and code_verifier. It does not send redirect_uri or client_secret.
- Sending only grant_type, code, and client_id is not sufficient for public-client codes created with PKCE.
- Advanced backend clients may instead configure clientSecret and exchange a code issued without PKCE.
- Returns: { access_token, refresh_token, token_type, expires_in }

legion.setTokens({ accessToken, refreshToken })
- Set tokens directly (e.g., from storage).

legion.chat(messages, options)
- Make chat completion request.
- Default model: your app default if set, otherwise gemini-2.5-flash during launch. Auto-refreshes token on 401.
- Options: model, temperature, max_tokens, autoRefresh (default: true), user_preferences, user_information, user_context, context_ids, platform_funded

legion.refreshAccessToken()
- Manually refresh access token using refresh token.
- Calls onTokenRefresh callback with new tokens.

legion.isConnected()
- Returns true if SDK has an access token.

legion.disconnect()
- Clear all tokens from SDK.

---

LegionError

All SDK errors are instances of LegionError with these properties:

  • error.message: Human-readable message
  • error.code: Error code (e.g., 'invalid_token')
  • error.status: HTTP status code
  • error.action: Financial remediation action when supplied
  • error.currentSpendCents / error.currentCapCents: Server-authoritative cap context for limit_exceeded
  • error.metadata: Raw structured API error payload

Helper Methods

  • error.requiresReauth() - Returns true if user needs to reconnect
  • error.isDeactivated() - Returns true if user deactivated the connection
  • error.isBillingError() - Backward-compatible check for any HTTP 402
  • error.isCapError() - Returns true when increase_cap authorization is required
  • error.isPaymentMethodError() - Returns true when payment_method authorization is required

View plain text
All SDK errors are instances of LegionError with these properties:
- error.message: Human-readable message
- error.code: Error code (e.g., 'invalid_token')
- error.status: HTTP status code
- error.action: Financial remediation action when supplied
- error.currentSpendCents / error.currentCapCents: Server-authoritative cap context for limit_exceeded
- error.metadata: Raw structured API error payload

Helper Methods
- error.requiresReauth() - Returns true if user needs to reconnect
- error.isDeactivated() - Returns true if user deactivated the connection
- error.isBillingError() - Backward-compatible check for any HTTP 402
- error.isCapError() - Returns true when increase_cap authorization is required
- error.isPaymentMethodError() - Returns true when payment_method authorization is required

---

AI Agent Quick Start

End-to-end flow for AI agents integrating a new app with Legion:

1. Setup

npm install -g @legionai/cli
legion login
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --margin 50

Accredited partners creating apps for clients should first run:

legion partner-claim

2. Save Credentials

The create-app output includes a public client_id and a one-time client_secret. Standard SDK, Next.js, WordPress, and Drupal integrations configure only client_id, then use a registered redirect URI plus S256 PKCE during authorization. Save client_secret only when building an advanced confidential backend OAuth flow.

3. Integrate

npm install @legionai/sdk
Then use createLegion({ clientId }) in your app. See "SDK Quick Start" above for full usage.

4. What Happens by Default on Every API Call

When your app makes a POST /v1/chat/completions call, Legion automatically:

  • Injects user context: user_preferences, user_information, and user_context all default to true. The user's AI preferences, demographics, and query patterns are added to the system message.
  • Searches app knowledge base (RAG): If you've uploaded files (PDF, DOCX, PPTX, video, audio) to your app's knowledge base via the dashboard, every call automatically searches this content. Top 5 matching chunks are injected as [App Knowledge Base] in the system message. If no files are uploaded, this is skipped (zero latency).
  • Applies owner margin: Your configured markup on model costs (default 50%). Legion separately adds a fixed 20% fee on model cost. You keep 100% of the owner margin.
  • Applies partner attribution: if your app was registered with an accredited partner slug, Legion attributes the partner share automatically.
  • Routes to the active launch provider: all responses are OpenAI-compatible; during launch the active provider catalog is Google-only.

5. How to Control Defaults

  • Opt out of user context: set user_preferences: false, user_information: false, or user_context: false in the request body
  • Limit RAG search to specific files: pass context_ids: ['ctx_abc123'] in the request body
  • Change margin: legion update-app --client-id <id> --margin <pct>
  • Change default model: configure in the dashboard, or pass model in each request
  • Invite a non-technical client owner for payouts: legion invite-owner <client_id> --email client@example.com

6. View Full Docs

Run "legion docs" to print the complete documentation in your terminal.

7. (Optional) Connect Railway Log Source

legion connect-source --name "my-api" --project-id <id> --environment-id <id> --project-token <token>
View plain text
End-to-end flow for AI agents integrating a new app with Legion:

### 1. Setup
npm install -g @legionai/cli
legion login
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --margin 50

Accredited partners creating apps for clients should first run:
legion partner-claim

### 2. Save Credentials
The create-app output includes a public client_id and a one-time client_secret.
Standard SDK, Next.js, WordPress, and Drupal integrations configure only client_id, then use a registered redirect URI plus S256 PKCE during authorization.
Save client_secret only when building an advanced confidential backend OAuth flow.

### 3. Integrate
npm install @legionai/sdk
Then use createLegion({ clientId }) in your app. See "SDK Quick Start" above for full usage.

### 4. What Happens by Default on Every API Call
When your app makes a POST /v1/chat/completions call, Legion automatically:
- Injects user context: user_preferences, user_information, and user_context all default to true. The user's AI preferences, demographics, and query patterns are added to the system message.
- Searches app knowledge base (RAG): If you've uploaded files (PDF, DOCX, PPTX, video, audio) to your app's knowledge base via the dashboard, every call automatically searches this content. Top 5 matching chunks are injected as [App Knowledge Base] in the system message. If no files are uploaded, this is skipped (zero latency).
- Applies owner margin: Your configured markup on model costs (default 50%). Legion separately adds a fixed 20% fee on model cost. You keep 100% of the owner margin.
- Applies partner attribution: if your app was registered with an accredited partner slug, Legion attributes the partner share automatically.
- Routes to the active launch provider: all responses are OpenAI-compatible; during launch the active provider catalog is Google-only.

### 5. How to Control Defaults
- Opt out of user context: set user_preferences: false, user_information: false, or user_context: false in the request body
- Limit RAG search to specific files: pass context_ids: ['ctx_abc123'] in the request body
- Change margin: legion update-app --client-id <id> --margin <pct>
- Change default model: configure in the dashboard, or pass model in each request
- Invite a non-technical client owner for payouts: legion invite-owner <client_id> --email client@example.com

### 6. View Full Docs
Run "legion docs" to print the complete documentation in your terminal.

### 7. (Optional) Connect Railway Log Source
legion connect-source --name "my-api" --project-id <id> --environment-id <id> --project-token <token>

MCP Server (AI Agents)

Manage apps and access deployment logs from AI agents like Claude Code, Cursor, or Windsurf.

Transport: stdio (the MCP server communicates over stdin/stdout)

Quick Setup

  1. Install CLI: npm install -g @legionai/cli
  2. Install MCP server: npm install -g @legionai/mcp
  3. Login: legion login (opens browser, saves token to ~/.legion/config.json)
  4. Add the config below to your editor's MCP settings file
  5. Restart your editor / reload MCP servers

Installation

npm install -g @legionai/cli    # CLI commands (login, app management, log sources)
npm install -g @legionai/mcp    # MCP server (for AI agent integration)

Configuration (Login Required)

Generate your MCP access token in the logged-in dashboard: Developer > Documentation > MCP Server > Get MCP Config

Claude Code (~/.config/claude/mcp.json)

{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

Cursor (~/.cursor/mcp.json)

{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

Windsurf (~/.windsurf/mcp.json)

{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

Available Tools

OAuth App Management:

  • legion_list_oauth_apps: List all OAuth apps on your account
  • legion_get_oauth_app: Get details of an OAuth app by client ID
  • legion_create_oauth_app: Create a new OAuth app
  • legion_update_oauth_app: Update OAuth app name or redirect URIs
  • legion_delete_oauth_app: Delete an OAuth app (revokes all connections)
  • legion_rotate_app_secret: Rotate an OAuth app's client secret (new secret shown once)
  • legion_get_oauth_app_stats: Get usage stats (users, tokens, earnings)
  • legion_get_oauth_app_transactions: Get transaction history (prompts, costs)

API Documentation:

  • legion_get_api_docs: Get full API docs (request format, models, errors)

Deployment Logs (Railway):

  • legion_list_railway_sources: List connected Railway log sources
  • legion_connect_railway_source: Connect a Railway deployment as a log source
  • legion_get_deployment_logs: Fetch deployment logs with filters (level, limit)
  • legion_get_deployment_errors: Fetch error-level deployment logs only
  • legion_disconnect_railway_source: Remove a Railway log source connection

CLI Equivalents (install @legionai/cli)

All MCP tools have CLI counterparts. Examples:

  legion list-apps
  legion create-app --name "My App" --redirect-uris "https://myapp.com/callback"
  legion get-app lgn_abc123
  legion rotate-secret lgn_abc123
  legion app-stats lgn_abc123
  legion partner-claim
  legion partner-status
  legion partner-connect-start
  legion partner-connect-status
  legion invite-owner lgn_abc123 --email client@example.com
  legion cancel-owner-invite lgn_abc123
  legion list-sources
  legion get-logs --source-id conn_xyz
  legion get-errors --source-id conn_xyz

Add --verbose or --debug to any command for HTTP request/response details:

  legion list-apps --verbose

Run "legion --help" for the full command reference.

Example Prompts

  • "List my OAuth apps and their stats"
  • "Create a new OAuth app called my-saas with redirect URI https://myapp.com/callback"
  • "Check my deployment logs for errors"
  • "Connect my Railway deployment for debugging"

View plain text
Manage apps and access deployment logs from AI agents like Claude Code, Cursor, or Windsurf.

Transport: stdio (the MCP server communicates over stdin/stdout)

### Quick Setup
1. Install CLI: npm install -g @legionai/cli
2. Install MCP server: npm install -g @legionai/mcp
3. Login: legion login (opens browser, saves token to ~/.legion/config.json)
4. Add the config below to your editor's MCP settings file
5. Restart your editor / reload MCP servers

### Installation
npm install -g @legionai/cli    # CLI commands (login, app management, log sources)
npm install -g @legionai/mcp    # MCP server (for AI agent integration)

### Configuration (Login Required)
Generate your MCP access token in the logged-in dashboard:
Developer > Documentation > MCP Server > Get MCP Config

#### Claude Code (~/.config/claude/mcp.json)
{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

#### Cursor (~/.cursor/mcp.json)
{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

#### Windsurf (~/.windsurf/mcp.json)
{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

### Available Tools

OAuth App Management:
- legion_list_oauth_apps: List all OAuth apps on your account
- legion_get_oauth_app: Get details of an OAuth app by client ID
- legion_create_oauth_app: Create a new OAuth app
- legion_update_oauth_app: Update OAuth app name or redirect URIs
- legion_delete_oauth_app: Delete an OAuth app (revokes all connections)
- legion_rotate_app_secret: Rotate an OAuth app's client secret (new secret shown once)
- legion_get_oauth_app_stats: Get usage stats (users, tokens, earnings)
- legion_get_oauth_app_transactions: Get transaction history (prompts, costs)

API Documentation:
- legion_get_api_docs: Get full API docs (request format, models, errors)

Deployment Logs (Railway):
- legion_list_railway_sources: List connected Railway log sources
- legion_connect_railway_source: Connect a Railway deployment as a log source
- legion_get_deployment_logs: Fetch deployment logs with filters (level, limit)
- legion_get_deployment_errors: Fetch error-level deployment logs only
- legion_disconnect_railway_source: Remove a Railway log source connection

### CLI Equivalents (install @legionai/cli)
All MCP tools have CLI counterparts. Examples:
  legion list-apps
  legion create-app --name "My App" --redirect-uris "https://myapp.com/callback"
  legion get-app lgn_abc123
  legion rotate-secret lgn_abc123
  legion app-stats lgn_abc123
  legion partner-claim
  legion partner-status
  legion partner-connect-start
  legion partner-connect-status
  legion invite-owner lgn_abc123 --email client@example.com
  legion cancel-owner-invite lgn_abc123
  legion list-sources
  legion get-logs --source-id conn_xyz
  legion get-errors --source-id conn_xyz

Add --verbose or --debug to any command for HTTP request/response details:
  legion list-apps --verbose

Run "legion --help" for the full command reference.

### Example Prompts
- "List my OAuth apps and their stats"
- "Create a new OAuth app called my-saas with redirect URI https://myapp.com/callback"
- "Check my deployment logs for errors"
- "Connect my Railway deployment for debugging"

---

CMS Integrations (WordPress & Drupal)

Add AI chat to your WordPress or Drupal site with zero code. Site visitors authorize through Legion before chatting, and you earn your full configured owner margin.

How It Works

  1. Install the Legion plugin on your site
  2. Enter your Client ID in the plugin settings
  3. Site visitors choose Connect to AI before chatting
  4. You earn your full configured owner margin on every API call

Download Plugins

WordPress plugin details, installation instructions, source-build directions, and external-service disclosures are available at https://legion-ai.org/wordpress.

WordPress Plugin

Installation

  1. Install "Legion AI Connect" from the WordPress.org plugin directory
  2. In WordPress Admin: Plugins > Add New and search for Legion AI Connect
  3. Install and activate "Legion AI Connect"

Configuration

  1. Go to Settings > Legion AI
  2. Enter your Client ID (get it from legion-ai.org/developer)
  3. Copy the displayed Redirect URI
  4. Add this Redirect URI to your Legion app settings
  5. Save changes

Always register the exact URI displayed by WordPress. Sites using plain permalinks use /?legion_callback=1; other sites use /legion-callback.

Usage

Add the chat widget using the shortcode: [legion_chat]

Shortcode options:

  • position: bottom-right, bottom-left, button, inline (default: bottom-right)
  • theme: dark, light, auto (default: dark)
  • placeholder: Custom input placeholder text
  • greeting: Custom welcome message
  • accent_color: Hex color for accent elements
  • trigger_text: Text shown for article-button mode
  • context_ids: Comma or space-separated context IDs searched for this widget only
  • context_text: Inline context sent with each request for this widget only; not added to persistent Legion knowledge
  • context_label: Label for inline context (default: Page context)

Example: [legion_chat position="inline" theme="light" placeholder="Ask me anything..."]

Article-button example: [legion_chat position="button" trigger_text="Chat with this article"]

Article-specific context example: [legion_chat position="inline" context_ids="ctx_abc123" context_text="Paste article context here"]

For longer pasted context or text containing quotation marks, put the context between opening and closing shortcode tags: [legion_chat position="inline"]Paste article context here[/legion_chat]

Drupal Module

Installation

  1. Download legion-drupal.zip from the link above
  2. Extract to /modules/contrib/legion_integration
  3. Enable: drush en legion_integration

Configuration

  1. Go to Admin > Configuration > Services > Legion AI
  2. Enter your Client ID
  3. Copy the Redirect URI and add to your Legion app
  4. Save configuration

Usage

  1. Go to Structure > Block Layout
  2. Place the "Legion AI Chat" block in your desired region
  3. Configure block settings (position, theme, placeholder, greeting, accent color, article-button text)

Drupal content can also embed the widget with the built-in Legion AI chat text filter: [legion_chat position="button" trigger_text="Chat with this article"]

Enable the "Legion AI chat shortcode" filter on trusted text formats before using shortcode embeds in content.

Creating a CMS App

When creating your app in the Legion dashboard:

  1. Select "WordPress" or "Drupal" as the app type
  2. WordPress normally suggests /legion-callback; sites using plain permalinks display /?legion_callback=1 instead
  3. Enter the exact Redirect URI displayed on the plugin settings page

Widget Features

  • Automatic session management (tokens stored in localStorage)
  • Same-tab OAuth flow that returns visitors to the originating page
  • Automatic same-tab reauthorization and return when a saved connection is revoked or expires
  • Automatic same-tab increase-cap and payment-method remediation with explicit user approval
  • Animated thinking indicator while an AI response is pending
  • Safe Markdown formatting for assistant responses
  • Light/dark/auto theme support
  • Floating, inline, and compact article-button display modes
  • Responsive design (works on mobile)
  • Auto error handling (silent token refresh, automatic reauthorization, billing alerts)
  • Zero backend required — public client OAuth, no server-side code
  • No default public-facing credit link in the CMS plugins; marketplace front-end credits remain opt-in

Security Notes

  • Uses Public Client OAuth flow (S256 PKCE, no client_secret in PHP)
  • Tokens stored in browser localStorage
  • WordPress and Drupal OAuth state combine a server-verifiable CMS nonce with a per-tab cryptographically random value
  • CMS callbacks verify the nonce before accepting authorization input; the browser verifies the complete state value before exchanging codes
  • Revoked or expired saved connections clear stale tokens and use a same-tab PKCE redirect back to the originating page
  • Strict redirect URI matching enforced
  • CORS enabled for cross-origin requests

View plain text
Add AI chat to your WordPress or Drupal site with zero code. Site visitors authorize through Legion before chatting, and you earn your full configured owner margin.

### How It Works
1. Install the Legion plugin on your site
2. Enter your Client ID in the plugin settings
3. Site visitors choose Connect to AI before chatting
4. You earn your full configured owner margin on every API call

### Download Plugins
- WordPress: https://wordpress.org/plugins/legion-ai-connect/ (requires WordPress 6.3+ / PHP 7.4+)
- Drupal: https://legion-ai.org/downloads/legion-drupal.zip (requires Drupal 10+ / PHP 8.1+)

WordPress plugin details, installation instructions, source-build directions, and external-service disclosures are available at https://legion-ai.org/wordpress.

### WordPress Plugin

#### Installation
1. Install "Legion AI Connect" from the WordPress.org plugin directory
2. In WordPress Admin: Plugins > Add New and search for Legion AI Connect
3. Install and activate "Legion AI Connect"

#### Configuration
1. Go to Settings > Legion AI
2. Enter your Client ID (get it from legion-ai.org/developer)
3. Copy the displayed Redirect URI
4. Add this Redirect URI to your Legion app settings
5. Save changes

Always register the exact URI displayed by WordPress. Sites using plain permalinks use `/?legion_callback=1`; other sites use `/legion-callback`.

#### Usage
Add the chat widget using the shortcode:
`[legion_chat]`

Shortcode options:
- position: bottom-right, bottom-left, button, inline (default: bottom-right)
- theme: dark, light, auto (default: dark)
- placeholder: Custom input placeholder text
- greeting: Custom welcome message
- accent_color: Hex color for accent elements
- trigger_text: Text shown for article-button mode
- context_ids: Comma or space-separated context IDs searched for this widget only
- context_text: Inline context sent with each request for this widget only; not added to persistent Legion knowledge
- context_label: Label for inline context (default: Page context)

Example:
`[legion_chat position="inline" theme="light" placeholder="Ask me anything..."]`

Article-button example:
`[legion_chat position="button" trigger_text="Chat with this article"]`

Article-specific context example:
`[legion_chat position="inline" context_ids="ctx_abc123" context_text="Paste article context here"]`

For longer pasted context or text containing quotation marks, put the context between opening and closing shortcode tags:
`[legion_chat position="inline"]Paste article context here[/legion_chat]`

### Drupal Module

#### Installation
1. Download legion-drupal.zip from the link above
2. Extract to /modules/contrib/legion_integration
3. Enable: drush en legion_integration

#### Configuration
1. Go to Admin > Configuration > Services > Legion AI
2. Enter your Client ID
3. Copy the Redirect URI and add to your Legion app
4. Save configuration

#### Usage
1. Go to Structure > Block Layout
2. Place the "Legion AI Chat" block in your desired region
3. Configure block settings (position, theme, placeholder, greeting, accent color, article-button text)

Drupal content can also embed the widget with the built-in Legion AI chat text filter:
`[legion_chat position="button" trigger_text="Chat with this article"]`

Enable the "Legion AI chat shortcode" filter on trusted text formats before using shortcode embeds in content.

### Creating a CMS App

When creating your app in the Legion dashboard:
1. Select "WordPress" or "Drupal" as the app type
2. WordPress normally suggests /legion-callback; sites using plain permalinks display /?legion_callback=1 instead
3. Enter the exact Redirect URI displayed on the plugin settings page

### Widget Features
- Automatic session management (tokens stored in localStorage)
- Same-tab OAuth flow that returns visitors to the originating page
- Automatic same-tab reauthorization and return when a saved connection is revoked or expires
- Automatic same-tab increase-cap and payment-method remediation with explicit user approval
- Animated thinking indicator while an AI response is pending
- Safe Markdown formatting for assistant responses
- Light/dark/auto theme support
- Floating, inline, and compact article-button display modes
- Responsive design (works on mobile)
- Auto error handling (silent token refresh, automatic reauthorization, billing alerts)
- Zero backend required — public client OAuth, no server-side code
- No default public-facing credit link in the CMS plugins; marketplace front-end credits remain opt-in

### Security Notes
- Uses Public Client OAuth flow (S256 PKCE, no client_secret in PHP)
- Tokens stored in browser localStorage
- WordPress and Drupal OAuth state combine a server-verifiable CMS nonce with a per-tab cryptographically random value
- CMS callbacks verify the nonce before accepting authorization input; the browser verifies the complete state value before exchanging codes
- Revoked or expired saved connections clear stale tokens and use a same-tab PKCE redirect back to the originating page
- Strict redirect URI matching enforced
- CORS enabled for cross-origin requests

---

Chat AI (Hosted Chat Apps)

Create a hosted chat app on Legion — no SDK integration needed. Users chat directly at https://legion-ai.org/chat/:appId and authorize it through Legion's standard consent screen on first use.

Creating a Chat App

  1. Go to https://legion-ai.org/developer and click "Create a Chat AI"
  2. Or use the public registration page at https://legion-ai.org/register-chat
  3. Configure a system prompt in the app detail view
  4. Share the chat link: https://legion-ai.org/chat/YOUR_APP_ID

Chat apps use app_type='chat' and appear with a purple "Chat" badge in the developer dashboard.

Chat API Endpoints

All endpoints are under /v1/chat/:appId on the auth server (https://auth.legion-ai.org).

GET /v1/chat/:appId/info

  • Public (no auth required)
  • Returns chat app metadata: name, description, whether a system prompt is configured

POST /v1/chat/:appId/connect

  • Requires Clerk authentication
  • Returns an access token for users with an active app connection
  • Returns a hosted consent URL when the user needs to authorize or re-authorize the chat app

POST /v1/chat/:appId/conversations

  • Requires auth
  • Creates a new conversation, returns conversation ID

GET /v1/chat/:appId/conversations

  • Requires auth
  • Lists the user's conversations for this chat app

GET /v1/chat/:appId/conversations/:id/messages

  • Requires auth
  • Returns messages for a conversation

POST /v1/chat/:appId/conversations/:id/messages

  • Requires auth
  • Saves messages (user + assistant) to a conversation
View plain text
Create a hosted chat app on Legion — no SDK integration needed. Users chat directly at https://legion-ai.org/chat/:appId and authorize it through Legion's standard consent screen on first use.

### Creating a Chat App
1. Go to https://legion-ai.org/developer and click "Create a Chat AI"
2. Or use the public registration page at https://legion-ai.org/register-chat
3. Configure a system prompt in the app detail view
4. Share the chat link: https://legion-ai.org/chat/YOUR_APP_ID

Chat apps use app_type='chat' and appear with a purple "Chat" badge in the developer dashboard.

### Chat API Endpoints

All endpoints are under /v1/chat/:appId on the auth server (https://auth.legion-ai.org).

GET /v1/chat/:appId/info
- Public (no auth required)
- Returns chat app metadata: name, description, whether a system prompt is configured

POST /v1/chat/:appId/connect
- Requires Clerk authentication
- Returns an access token for users with an active app connection
- Returns a hosted consent URL when the user needs to authorize or re-authorize the chat app

POST /v1/chat/:appId/conversations
- Requires auth
- Creates a new conversation, returns conversation ID

GET /v1/chat/:appId/conversations
- Requires auth
- Lists the user's conversations for this chat app

GET /v1/chat/:appId/conversations/:id/messages
- Requires auth
- Returns messages for a conversation

POST /v1/chat/:appId/conversations/:id/messages
- Requires auth
- Saves messages (user + assistant) to a conversation