# ELTEX Agent Plan API

The ELTEX Agent Plan API is OpenAI-compatible for chat completions and adds account-scoped endpoints for model discovery, usage, capacity, Burst, Reset Tickets, Agent State, RunGuard, and Agentic Wallet autonomous USDC payments.

## Base URL and authentication

```text
https://eltexlabs.com/v1
Authorization: Bearer sk-eltex-...
Content-Type: application/json
```

OAuth access tokens use the same API with endpoint-specific scopes.

Every `/v1` response includes an `X-Request-ID` header. Include it when reporting a failed request.

## Models

```http
GET /v1/models
```

```json
{
  "object": "list",
  "data": [
    { "id": "eltex/smart", "object": "model", "owned_by": "eltex" },
    { "id": "MODEL_ID_FROM_GET_MODELS", "object": "model", "owned_by": "eltex" }
  ]
}
```

The result is filtered by the authenticated account and current plan. Treat it as the source of truth for model IDs. The catalog currently reports availability, not a per-model capability matrix.

## Chat completions

```http
POST /v1/chat/completions
```

### Request schema

| Field | Type | Required | Compatibility |
|---|---|---:|---|
| `model` | string | recommended | Use `eltex/smart` or an ID from `GET /models`. If omitted, the account default applies. |
| `messages` | array | yes | OpenAI-compatible message objects. |
| `messages[].role` | string | yes | `system`, `developer`, `user`, `assistant`, or `tool`, subject to the selected model. |
| `messages[].content` | string or array | yes | Text is supported. Multimodal content parts are passed through when the selected model supports them. |
| `stream` | boolean | no | Returns SSE chunks when `true`. |
| `stream_options.include_usage` | boolean | no | Passed through; usage-chunk support depends on the selected model. |
| `temperature`, `top_p` | number | no | Passed through; support and ranges are model-dependent. |
| `max_tokens`, `max_completion_tokens` | integer | no | Passed through; use the field accepted by the selected model. |
| `stop` | string or array | no | Model-dependent. |
| `n`, `seed`, `presence_penalty`, `frequency_penalty` | number | no | Model-dependent. |
| `tools` | array | no | OpenAI-compatible function tools; model-dependent. |
| `tool_choice`, `parallel_tool_calls` | string, object, or boolean | no | Model-dependent. |
| `response_format` | object | no | `json_object` or `json_schema` only when the selected model supports structured output. |
| `eltex.routing_mode` | string | no | `smart`, `preferred`, or `force`. |
| `eltex.preferred_model` | string | no | Concrete model cap used by Preferred or Force routing. |

ELTEX forwards OpenAI-compatible generation parameters after replacing the public model with the selected internal route. A parameter accepted by one model may be rejected or ignored by another. Test the exact model and parameter set before production use.

### Multimodal input

Use OpenAI-compatible content parts:

```json
{
  "model": "eltex/smart",
  "messages": [{
    "role": "user",
    "content": [
      { "type": "text", "text": "Describe this image." },
      { "type": "image_url", "image_url": { "url": "https://example.com/image.png" } }
    ]
  }]
}
```

The selected route must support the supplied media. Elixir image and video generation uses the separate `/v1/elixir` API.

### Tool calling

```json
{
  "model": "MODEL_ID_FROM_GET_MODELS",
  "messages": [{ "role": "user", "content": "What is the weather in Bangkok?" }],
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "Get current weather",
      "parameters": {
        "type": "object",
        "properties": { "city": { "type": "string" } },
        "required": ["city"],
        "additionalProperties": false
      }
    }
  }],
  "tool_choice": "auto"
}
```

Execute returned tool calls in your application, append a `tool` message, and send the next completion request.

### Structured output

```json
{
  "model": "MODEL_ID_FROM_GET_MODELS",
  "messages": [{ "role": "user", "content": "Return a compact project risk." }],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "risk",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "level": { "type": "string", "enum": ["low", "medium", "high"] },
          "summary": { "type": "string" }
        },
        "required": ["level", "summary"],
        "additionalProperties": false
      }
    }
  }
}
```

Structured-output support is model-dependent. Validate the returned object even when strict mode is requested.

### Non-stream response

The response preserves the OpenAI-compatible completion and adds `eltex` route and billing metadata:

```json
{
  "id": "chatcmpl_...",
  "object": "chat.completion",
  "created": 1785340800,
  "model": "selected-provider-model",
  "choices": [{
    "index": 0,
    "message": { "role": "assistant", "content": "Hello!" },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 12,
    "completion_tokens": 4,
    "total_tokens": 16
  },
  "eltex": {
    "classification": "general_qa",
    "task_type": "general_qa",
    "complexity": "simple",
    "requested_model": "eltex/smart",
    "selected_model": "MODEL_ID_FROM_GET_MODELS",
    "routing_mode": "smart",
    "routing_decision": "preclassifier_smart_route",
    "preclassifier_skipped": false,
    "credits_charged": 1.25,
    "input_tokens": 12,
    "output_tokens": 4,
    "estimated_usage": false,
    "request_id": "chatcmpl_..."
  }
}
```

`classification`, `complexity`, routing decisions, and selected models are operational metadata and may evolve. Do not use them as security boundaries. `credits_charged` is the Agent Plan credit charged for the completion.

## Routing

| Mode | How to request | Behavior | Eligibility |
|---|---|---|---|
| Smart | `model: "eltex/smart"`, `eltex.routing_mode: "smart"`, or `$*` | Classifies the latest user request and selects an eligible route. | Any plan with API access |
| Preferred | Concrete `model`, `eltex.routing_mode: "preferred"`, or `$?` | Uses the preferred model as a capability/cost cap and may select an equal or lighter route. | Subject to plan model access |
| Force | Concrete model plus `eltex.routing_mode: "force"` or `$!` | Skips preclassification and calls the requested model. | Plus, Pro, and Max |

Routing markers are inspected only in the latest user message. ELTEX removes the first `$*`, `$?`, or `$!` it finds before routing. There is currently no backslash escape syntax. To send a literal marker, avoid the contiguous token in the latest user message—for example write `$ !`—or place the literal example in an earlier system/developer message.

## Streaming

Set `stream: true`. The response uses `Content-Type: text/event-stream` and OpenAI-compatible chunks:

```text
data: {"id":"chatcmpl_...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl_...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data: {"id":"chatcmpl_...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]
```

When the selected model supports it, request `stream_options: { "include_usage": true }` for a usage chunk. ELTEX records final usage after the upstream stream is consumed successfully. The non-stream `eltex` metadata object is not added to chunks.

Errors before streaming starts use the normal JSON error schema. A failure after headers or chunks have started may arrive as an upstream SSE error event or an early connection close. There is no stream-resume cursor. Treat an early close as an indeterminate completion and reconcile through usage before deciding whether to retry.

## Usage and capacity

### Usage

```http
GET /v1/usage?range=week
```

`range` is `day`, `week`, `month`, or `all`. An omitted or unrecognized value currently falls back to `all`; clients should still send one of the documented values explicitly.

```json
{
  "object": "agent_plan.usage",
  "range": "week",
  "requests": 42,
  "input_tokens": 12000,
  "output_tokens": 3400,
  "cost_usd": 0.124,
  "cost_idr": 2046,
  "internal_units": 1.24,
  "credits_used": 124,
  "burst_credits_used": 0,
  "reset_credits_used": 20
}
```

### Capacity

```http
GET /v1/capacity
```

```json
{
  "object": "agent_plan.capacity",
  "plan": "Plus",
  "period": {
    "label": "4-hour",
    "remaining_percent": 72.5,
    "used_percent": 27.5,
    "remaining_credits": 3262.5,
    "limit_credits": 4500,
    "resets_at": "2026-07-29T20:00:00.000Z"
  },
  "weekly": {
    "remaining_percent": 81.2,
    "used_percent": 18.8,
    "resets_at": "2026-08-03T00:00:00.000Z",
    "timezone": "Asia/Bangkok"
  },
  "burst": {
    "active_credits": 500,
    "total_credits": 500,
    "auto_active": true
  },
  "reset": {
    "stored_count": 2,
    "usable_count": 1,
    "active": false,
    "active_period_credits": 0
  }
}
```

Complete capacity fields:

| Object | Fields |
|---|---|
| `period` | `label`, `limit`, `base_limit`, `used`, `remaining`, `base_remaining`, `reset_ticket_remaining`, `resets_at`, `timezone`, `override_active`, `remaining_percent`, `used_percent`, `remaining_credits`, `limit_credits` |
| `weekly` | `remaining_percent`, `used_percent`, `resets_at`, `timezone` |
| `burst` | `limit`, `used`, `remaining`, `active_remaining`, `total_remaining`, `auto_active`, `active_credits`, `total_credits` |
| `reset` | `stored_count`, `usable_count`, `active`, `active_period_credits` |

The legacy numeric fields in `period` and `burst` are internal-unit
compatibility fields. New clients should display the `*_credits` and percentage
fields. Weekly raw limits are intentionally not exposed.

### Burst

```http
GET /v1/burst
```

```json
{
  "object": "agent_plan.burst",
  "auto_active": true,
  "active_credits": 500,
  "total_credits": 500,
  "used_after": ["regular plan capacity", "active Reset Ticket capacity"],
  "message": "Burst Pack is active automatically and does not require an activation call."
}
```

### Reset Ticket inventory

```http
GET /v1/reset
```

```json
{
  "object": "agent_plan.reset",
  "plan": "Plus",
  "stored_count": 2,
  "usable_count": 1,
  "active": false,
  "active_period_credits": 0,
  "tickets": [{
    "label": "Plus Reset Ticket",
    "plan_key": "plus",
    "source": "purchase",
    "usable_now": true,
    "stored_at": "2026-07-29T10:00:00.000Z"
  }]
}
```

### Activate one Reset Ticket

The API key needs the Agent refill permission (`ai.capacity:write` for OAuth).

```bash
curl -X POST https://eltexlabs.com/v1/reset/use \
  -H "Authorization: Bearer $ELTEX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reset-job-<stable-job-id>" \
  -d '{"reason":"capacity exhausted during an active user-approved job"}'
```

Create one stable key from the intended job and reuse the exact key for every retry of that same activation. A timestamp generated at retry time is unsafe because it creates a new action and can consume another ticket.

```json
{
  "object": "agent_plan.reset_activation",
  "activated": true,
  "ticket": "Plus Reset Ticket",
  "plan": "Plus",
  "period_remaining_percent": 100,
  "weekly_remaining_percent": 100,
  "period_resets_at": "2026-07-29T20:00:00.000Z",
  "weekly_resets_at": "2026-08-03T00:00:00.000Z",
  "idempotent_replay": false
}
```

## Agent State

Agent State is the paid-plan API for persisting agent identity, task progress, decisions, checkpoints, resume runs, private R2 files, and external artifact references across sessions. It uses the same base URL and Bearer authentication as Agent Plan.

OAuth tokens and scoped API keys use these permissions:

| Scope | Access |
|---|---|
| `agents:read` | List and read agents. |
| `agents:write` | Create and update agents. |
| `tasks:read` | Read tasks, events, checkpoints, and included artifact references. |
| `tasks:write` | Create tasks, update state, checkpoint/resume, and manage artifact references. |

```text
POST   /v1/agents
GET    /v1/agents
GET    /v1/agents/{agent_id}
PATCH  /v1/agents/{agent_id}

POST   /v1/tasks
GET    /v1/tasks
GET    /v1/tasks/{task_id}
POST   /v1/tasks/{task_id}/state/operations
GET    /v1/tasks/{task_id}/events
POST   /v1/tasks/{task_id}/checkpoints
GET    /v1/tasks/{task_id}/checkpoints
GET    /v1/tasks/{task_id}/checkpoints/{checkpoint_id}
POST   /v1/tasks/{task_id}/resume
POST   /v1/tasks/{task_id}/artifacts/upload
GET    /v1/tasks/{task_id}/artifacts/{artifact_id}/content
POST   /v1/tasks/{task_id}/artifacts
DELETE /v1/tasks/{task_id}/artifacts/{artifact_id}
```

Every Agent State write needs a stable `Idempotency-Key`. Agent patches and task-state operations also need the current version through `If-Match` or `expected_version`; missing and stale versions return `428 state_version_required` and `409 state_version_conflict`/`resource_version_conflict` respectively.

Agent, task, and checkpoint lists use an opaque `next_cursor`. Events use `next_after_sequence`. Agents can upload private managed files to ELTEX R2 with a raw binary request or attach external references with `https`, `gs`, or `s3` URIs. Managed downloads require the owning account's `tasks:read` credential.

Download the complete field, operation, pagination, checkpoint, resume, artifact, and error reference from `/downloads/agent-state-api.md`.

## Agent RunGuard

RunGuard uses a two-phase `preflight` → execute → `complete` protocol to enforce an immutable run policy snapshot. It supports cost reservations, model/tool/browser call limits, runtime and failure limits, deterministic repetition detection, verified Agent State/artifact progress evidence, run control, ordered events, and trust-labelled usage.

| Scope | Access |
|---|---|
| `runs:read` | Read policies, runs, and events. |
| `runs:write` | Create/control policies and runs; preflight/complete actions; report events. |
| `runtime.usage:read` | Read normalized run usage. |

```text
POST  /v1/guard-policies
GET   /v1/guard-policies
GET   /v1/guard-policies/{policy_id}
PATCH /v1/guard-policies/{policy_id}
POST  /v1/runs
GET   /v1/runs
GET   /v1/runs/{run_id}
GET   /v1/runs/{run_id}/events
POST  /v1/runs/{run_id}/events
POST  /v1/runs/{run_id}/preflight
POST  /v1/runs/{run_id}/executions/{execution_id}/complete
POST  /v1/runs/{run_id}/pause
POST  /v1/runs/{run_id}/resume
POST  /v1/runs/{run_id}/terminate
GET   /v1/runs/{run_id}/usage
```

`ALLOW` and `WARN` preflight decisions include a signed short-lived execution token bound to the exact owner/run/execution/action digest. `PAUSE` and `TERMINATE` do not. Public external-tool data remains visibly `client_reported`; only ELTEX-mediated actions can be described as hard-enforced.

Native approval creation is not implemented in this revision. Sensitive/critical risk labels are recorded but do not automatically request approval yet. See `/downloads/runguard-api.md` for the complete policy, action, progress, cost-source, event, control, and error contract.

## Agentic Wallet and x402

Agentic Wallet lets a dedicated API key request autonomous, policy-bounded USDC x402 payments and direct transfers for one bound agent identity. The owner first creates or reuses an agent in **Dashboard → Agentic Wallet → Agents & Policies**, creates a wallet-scoped key in **API Keys**, then binds both in an active versioned grant. Creating an agent alone does not authorize spending.

| Scope | Access |
|---|---|
| `wallet:read` | Read the assigned policy, Base/Solana USDC balance, and reservations. |
| `wallet:spend` | Preflight and request policy-controlled x402 payments or direct transfers. |

```text
GET  /v1/agentic-wallet/balance?agent_id={agent_id}
POST /v1/agentic-wallet/preflight
POST /v1/agentic-wallet/x402/fetch
POST /v1/agentic-wallet/transfer
```

The key must be the exact dedicated key bound to the active grant. Agentic Wallet routes reject OAuth tokens. A grant controls networks, per-transaction/daily/monthly USDC limits, approvals, expiry, pause/revoke state, and optional exact/wildcard x402 domains. An empty domain list allows all public HTTPS destinations; private, local, link-local, multicast, credential-bearing, and unresolved destinations remain blocked.

Every preflight, x402 fetch, or direct transfer needs a stable `Idempotency-Key` bound to its exact intent. Human approval is valid for ten minutes and the agent must retry the identical request with the identical key. By default, requests at or below the configured approval threshold execute autonomously; owners can enable approval for every payment. Direct transfers require `permissions.transfer=true` and a valid network address supplied in the request. `allowedRecipients` is optional: empty allows any valid address at send time, while a non-empty list restricts destinations. Base uses an owner-authorized, encrypted Thirdweb User Wallet session scoped to the grant with no ELTEX-imposed 24-hour cap; a Thirdweb JWT expiry or grant expiry remains authoritative. Solana uses the distinct project-managed allocation wallet created for that user and needs USDC plus SOL for fees. Neither path uses a shared ELTEX treasury. Stop on `spending_authorization_required`.

See `/downloads/agentic-wallet-api.md` for the complete setup, policy, request, approval, execution-state, readiness, and error contract.

## Errors, retries, and concurrency

```json
{
  "error": {
    "message": "Your current capacity is already full",
    "type": "capacity_already_full",
    "code": "capacity_already_full"
  }
}
```

| HTTP | `error.code` | Meaning | Client action |
|---:|---|---|---|
| 400 | `invalid_request_error` | Invalid chat payload or routing input. | Fix the payload; do not retry unchanged. |
| 400 | `invalid_idempotency_key` | Missing, empty, longer than 128 characters, or contains unsupported characters. | Create a stable portable key and retry. |
| 400 | `active_reset_capacity_remaining` | An active Reset Ticket still has capacity. | Continue using it; do not activate another. |
| 400 | `capacity_already_full` | Regular capacity is already full. | Stop; no refill is needed. |
| 400 | `no_matching_reset_ticket` | No stored ticket matches the active plan. | Stop and ask the user to check inventory/plan. |
| 401 | `invalid_api_key` | API key is invalid. | Replace the credential. |
| 402 | `capacity_exhausted` | No regular, active Reset, or Burst capacity remains. | Wait, or inspect Reset inventory if authorized. |
| 402 | `monthly_budget_exhausted` | Plan monthly budget was reached. | Stop until the plan budget resets or changes. |
| 403 | `forbidden` | Plan, model, OAuth scope, or Agent refill permission is missing. | Change authorization or request. |
| 403 | `active_subscription_required` | Reset activation requires an active paid subscription. | Stop and ask the user to restore subscription access. |
| 403 | `wallet_grant_missing`, `wallet_grant_inactive`, `wallet_grant_expired` | The dedicated key has no usable grant for that agent. | Stop; the owner must correct the binding or state. |
| 403 | `network_not_allowed`, `action_not_allowed`, `x402_domain_not_allowed`, `recipient_not_allowed` | The active wallet policy denies the request. `recipient_not_allowed` appears only when optional recipient restrictions are configured. | Respect the policy; do not route around it. |
| 403 | `transaction_limit_exceeded`, `daily_limit_exceeded`, `monthly_limit_exceeded` | The wallet policy limit was reached. | Stop or ask the owner to change the policy. |
| 409 | `idempotency_record_unavailable` | The stored idempotency record could not be read. | Re-check capacity before starting a new action. |
| 409 | `idempotency_key_reused` | The key was used with a different body. | Do not retry with that body/key pair. |
| 409 | `idempotency_request_in_progress` | The same activation is still processing. | Retry the same key after `Retry-After`. |
| 409 | `reset_ticket_activation_conflict` | Another request activated the ticket. | Re-check capacity and inventory. |
| 409 | `idempotency_key_conflict`, `payment_authorization_expired`, `payment_submission_in_progress`, `transfer_authorization_expired`, `transfer_submission_in_progress` | The wallet key was rebound, authorization expired, or another identical submission already claimed the execution. | Reuse keys only for an identical live logical request; wait when submission is in progress. |
| 409 | `spending_authorization_required` | Base owner authorization expired/is missing, or the user's Solana allocation is not ready. | Stop and ask the owner to complete that grant's network setup. |
| 429 | `concurrency_limit_exceeded` | Concurrent chat limit reached. | Retry after `Retry-After`. |
| 500/502/503 | `invalid_request_error` | Temporary ELTEX or selected-model failure. | Retry with bounded exponential backoff. |
| Selected-model HTTP status (commonly 4xx/5xx) | `upstream_error` | The selected model rejected or failed the request before a response/stream began. | Fix the request for 4xx; retry 429/5xx with bounded exponential backoff. |

Agent Plan chat has a per-plan concurrency limit. There is no separate documented per-minute chat quota. Elixir has separate rate limits: generation requests default to 10/minute, while read requests use the key's configured limit (normally 60/minute). A `429` response includes `Retry-After`.

Do not automatically retry non-idempotent chat requests after an indeterminate stream close unless duplicate work is acceptable.

## OAuth

OAuth covers Agent Plan, Agent State, RunGuard, and Elixir according to requested scopes. Agentic Wallet deliberately requires a dedicated API key whose ID is bound to the wallet grant; `wallet:read` and `wallet:spend` are not OAuth scopes.

Access tokens last 1 hour. Refresh tokens last 30 days, are issued only with `offline_access`, and rotate on every use. Authorization codes are single-use and last 10 minutes. Device codes last 15 minutes.

Device Flow responses include `interval` (currently 5 seconds). Poll no faster than that interval:

| Error | Meaning | Action |
|---|---|---|
| `authorization_pending` | User has not completed approval. | Continue at the advertised interval. |
| `access_denied` | User denied the request. | Stop. |
| `expired_token` | Device code expired. | Stop and start a new flow. |
| `invalid_grant` | Code/token/client mismatch or invalid grant. | Stop and restart authorization. |

ELTEX currently provides OAuth access tokens and `/oauth/userinfo`; it does not issue ID tokens and does not publish OpenID discovery or JWKS endpoints. Do not configure an OIDC verifier against ELTEX until discovery, signing keys, and ID tokens are explicitly announced.

See the OAuth section in the dashboard for client registration, scopes, PKCE, refresh, device flow, revocation, and endpoint mappings.

## Elixir operational notes

- Video `webhook_url` callbacks are currently forwarded to the media service. ELTEX does not add an ELTEX signature header. Treat callbacks as hints and confirm the final state with `GET /v1/elixir/video/generations/{id}`.
- Generated asset URLs may be provider-hosted and may expire. No minimum retention window is currently guaranteed. Copy completed assets promptly to storage you control.
- `GET /v1/elixir/generations` accepts `limit` from 1 to 100 and an opaque `cursor`. The response includes `has_more` and `next_cursor`; pass `next_cursor` unchanged to fetch the next page.

```http
GET /v1/elixir/generations?limit=50&cursor=OPAQUE_CURSOR
```
