BASIS
Documentation

API reference

The Basis API is OpenAI-compatible and built for agents. Point any OpenAI client at Basis by changing two values — the base_url and the api_key. Nothing else in your code has to change.

Introduction

Basis speaks the OpenAI Chat Completions protocol. The request shape, the streaming wire format, the model list, and the error envelope all match what OpenAI clients expect, so any framework that targets an OpenAI-compatible endpoint works against Basis unmodified. Point your existing client at https://basis.watch/api/v1 and keep your code.

Behind the compatible surface, Basis is a worker-powered, Base-native inference network: a hosted upstream or — when worker routing is enabled — contributor GPU workers serve jobs, accounting is deterministic and denominated in $BASIS credits, and every settled job produces a verifiable inference receipt.

This page documents the programmatic API at /api/v1 for agents and apps. There is also a separate signed-in web console at /inference (and on the home page) — a chat surface that requires sign-in (Privy login or an sk-basis- key) and applies stricter, identity-keyed limits. Both share the same accounting and write the same receipts; this reference is the API.

Inference is live in production via a hosted upstream proxy, and the public API is a free metered demo (credits are not required by default). On a deploy with no backend configured (local/preview) chat completions return a structured runtime_pending (503) instead — the contract is real, the backend is honestly absent. $BASIS is live on Base; the payment router is configured and can quote ETH/WETH/USDC → $BASIS, with live deposits crediting once a swap is proven on-chain. live

Base URL

All API routes are served under a single base URL. Set it as your client's base_url (or baseURL) and the SDK appends the standard OpenAI paths such as /chat/completions and /models.

Base URL

text
base_url:  https://basis.watch/api/v1

Authentication

Basis has two ways to authenticate. A person signs in to the dashboard with Privy (wallet or email); an agent or app calls the API with an API key. Both are documented in full on the authentication page.

For API access, authenticate with a bearer token in the Authorization header, exactly as you would with OpenAI. Basis keys are prefixed sk-basis-.

http
Authorization: Bearer sk-basis-...

Create a key

Mint a key in the dashboard, or programmatically with POST /api/user/api-keys authenticated by a Privy access token. The raw sk-basis-... value is shown once — only a peppered hash is stored — so copy it immediately. Keys can be listed (by prefix) and revoked.

bash
# Mint an API key with a Privy access token (the dashboard does this for you).
# The raw sk-basis-... value is returned ONCE — store it immediately.
curl -X POST https://basis.watch/api/user/api-keys \
  -H "Authorization: Bearer <PRIVY_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"label":"my-agent"}'

Then send the key on a request:

bash
# Call inference with the API key in the Authorization header.
curl https://basis.watch/api/v1/chat/completions \
  -H "Authorization: Bearer sk-basis-..." \
  -H "Content-Type: application/json" \
  -d '{"model":"basis-default","messages":[{"role":"user","content":"Ready when funded."}]}'

When auth is required

Enforcement is controlled by BASIS_AUTH_REQUIRED (default false). Today the public API is open — an anonymous call still works and returns the same runtime_pending response while the backend is absent. When BASIS_AUTH_REQUIRED is true, /api/v1/chat/completions and the user routes require a Privy token or an API key. Key issuance is configured today (mint one in the dashboard or via POST /api/user/api-keys), and a key is accepted but not required on the public API until auth is enforced — so you can send sk-basis-... now and it is honored.

Auth error shapes

Authentication failures carry a structured { error: { code } }. Branch on error.code.

StatusCodeWhen
401auth_requiredAuthentication is required and no Privy token or API key was supplied.
401invalid_privy_tokenA Privy access token was supplied but failed JWKS verification.
403wallet_not_linkedAn authenticated user acted for a wallet not linked to their account.
503privy_config_pendingLogin was requested before Privy is configured. The auth surface is live; Privy is honestly pending.
keys configured

API-key issuance is configured ( BASIS_API_KEY_PEPPER is set) — a key is shown once and stored only as a peppered hash. The public API is open by default ( BASIS_AUTH_REQUIRED=false), so a key is accepted but not required today; it becomes mandatory when auth is enforced. See authentication.

Models

List the models the network publishes with GET /api/v1/models. The response is an OpenAI-shaped { object: "list", data: [...] } payload. Each entry carries an available flag that reflects whether a runtime backend is configured — not just whether the model is published.

ModelContextMultiplierStatus
basis-small16,3840.50xsoon
basis-default32,7681.00xsoon
basis-large131,0723.00xsoon

The multiplier scales deterministic per-token accounting (1.00x is the baseline). Context is the maximum token window. Currently available is true for all models, reflecting the configured runtime backend.

List models

bash
curl -s https://basis.watch/api/v1/models

Pricing

Inference is metered in credits — Basis's usage-accounting unit — with deterministic per-token accounting computed in integer base units, no floating point anywhere in the money path. A heavier model costs more per token via its multiplier (see the model table). Credits convert to $BASIS at the active pricing epoch's BASIS-per-credit rate.

Credits are deterministic inside a quote, reservation, pricing epoch, and receipt, but the network does not promise that one credit maps to the same amount of $BASIS forever. A new pricing epoch applies prospectively to new quotes and reservations only — there is no retroactive repricing: accepted jobs are never silently repriced and existing receipts are never rewritten. When a job is reserved, the BASIS-per-credit rate is locked into a hashed snapshot the receipt preserves, and the worker reward is drawn from that same snapshot.

deterministic

See Credits for the exact per-token formula, base units, the credit ledger, and the dynamic pricing epoch model.

Pricing API

The dynamic pricing system is fully introspectable. Read the active epoch and the resolved rate, list the versioned epoch table, and request a deterministic, epoch-locked quote for a job before you run it. Every raw amount is an integer base-unit string.

Pricing status

GET /api/pricing returns the active epoch, the resolved basisPerCreditRaw, the per-model credit price table, the multipliers, the honest price source, and any warnings. Until a live price source is configured it reports placeholder — never a fabricated live price.

bash
curl -s https://basis.watch/api/pricing

basis.pricing response

json
{
  "object": "basis.pricing",
  "status": "placeholder",
  "mode": "placeholder",
  "basisPerCreditRaw": "1000000000000000",
  "priceSource": "placeholder",
  "activeEpoch": {
    "epochId": "epoch-placeholder-001",
    "status": "active",
    "source": "placeholder",
    "basisPerCreditRaw": "1000000000000000",
    "creditTargetUsdRaw": "10000",
    "basisUsdPriceRaw": null,
    "maxEpochChangeBps": 2500
  },
  "models": [ /* per-model credit prices */ ],
  "multipliers": { "utilizationMultiplierBps": 10000, "supplyMultiplierBps": 10000,
    "demandMultiplierBps": 10000, "providerFloorMultiplierBps": 10000 },
  "warnings": ["Pricing is placeholder until BASIS token, liquidity, and a price source are configured."]
}

Epoch list

GET /api/pricing/epochs returns the versioned epoch table, newest first, as { object: "list", data: [...] }. Listing an epoch never reprices an accepted job — new epochs apply prospectively only.

bash
curl -s https://basis.watch/api/pricing/epochs

Quote a job

POST /api/pricing/quote with a model, prompt_tokens_estimate, and output_tokens_estimate returns a basis.pricing_quote: usage credits, the locked BASIS-per-credit rate, the BASIS required, the protocol fee and worker pool, and a frozen snapshot + its snapshotHash (what a reservation locks and a receipt preserves). Re-quoting the same model + tokens under the same active epoch yields identical raw amounts. Status is quotable / pending (the rate fell back to the epoch anchor) / unavailable (model not priced).

bash
curl https://basis.watch/api/pricing/quote \
  -H "Content-Type: application/json" \
  -d '{"model":"basis-default","prompt_tokens_estimate":1000,"output_tokens_estimate":500}'

basis.pricing_quote response

json
{
  "object": "basis.pricing_quote",
  "quoteId": "quote_...",
  "pricingEpochId": "epoch-placeholder-001",
  "modelId": "basis-default",
  "usageCreditsRaw": "3000000",
  "basisPerCreditRaw": "1000000000000000",
  "basisRequiredRaw": "3000000000000000",
  "protocolFeeRaw": "300000000000000",
  "workerPoolRaw": "2700000000000000",
  "priceSource": "placeholder",
  "snapshot": { /* the frozen, hashed PricingSnapshot */ },
  "snapshotHash": "<sha256-hex>",
  "createdAt": "2026-06-21T00:00:00.000Z",
  "expiresAt": "2026-06-21T00:01:00.000Z",
  "status": "quotable",
  "warnings": []
}

Refresh (operator)

POST /api/internal/pricing/refresh re-reads the price source and re-derives BASIS-per-credit now, returning what the network would price at. It is secret-protected (INTERNAL_API_SECRET / ADMIN_SECRET, or the dedicated BASIS_PRICING_REFRESH_SECRET) and never echoes the secret. It never signs or writes on-chain and never reprices an accepted job.

bash
# Operator-only. Secret-protected (INTERNAL_API_SECRET / ADMIN_SECRET,
# or the dedicated BASIS_PRICING_REFRESH_SECRET). The secret is never echoed.
curl -X POST https://basis.watch/api/internal/pricing/refresh \
  -H "x-internal-secret: <INTERNAL_API_SECRET>"

Pricing on /models

GET /api/v1/models carries the dynamic pricing status: pricing_epoch_id, price_source, basis_per_credit_raw, and a per-model pricing block denominated in credits.

/api/v1/models pricing fields

json
{
  "object": "list",
  "pricing_epoch_id": "epoch-placeholder-001",
  "pricing_status": "placeholder",
  "price_source": "placeholder",
  "basis_per_credit_raw": "1000000000000000",
  "pricing_warnings": ["Pricing is placeholder until ... configured."],
  "data": [
    {
      "id": "basis-default", "object": "model", "available": false, "status": "planned",
      "pricing": {
        "unit": "credit", "pricing_type": "per_token",
        "prompt_credits_per_token_raw": "1000",
        "output_credits_per_token_raw": "4000",
        "model_multiplier_bps": 10000,
        "basis_per_credit_raw": "1000000000000000",
        "price_source": "placeholder",
        "pricing_epoch_id": "epoch-placeholder-001"
      }
    }
  ]
}

Pricing on a payment quote

POST /api/payments/quote attaches a pricing block showing the epoch the required BASIS was priced under — the epoch id, the locked BASIS-per-credit rate, the usage credits, and the resulting BASIS required. The executable swap still uses the provider's fresh market quote; only the required BASIS amount is dynamic. It is honest null on any non-quotable path.

payment quote pricing block

json
{
  "quote_id": "q_...",
  "status": "quotable",
  /* ...the executable swap fields... */
  "pricing": {
    "pricing_epoch_id": "epoch-placeholder-001",
    "basis_per_credit_raw": "1000000000000000",
    "usage_credits_raw": "100000000",
    "basis_required_raw": "100000000000000000",
    "price_source": "placeholder"
  }
}

Chat completions

POST /api/v1/chat/completions is the core endpoint. Send a model and a messages array of { role, content } objects; set stream: true for token-by-token Server-Sent Events. The request and response shapes are OpenAI-compatible.

curl

bash
curl https://basis.watch/api/v1/chat/completions \
  -H "Authorization: Bearer sk-basis-..." \
  -H "Content-Type: application/json" \
  -d '{"model":"basis-default","messages":[{"role":"user","content":"Summarize this for an agent."}],"stream":true}'

Python (OpenAI SDK)

python
from openai import OpenAI

client = OpenAI(base_url="https://basis.watch/api/v1", api_key="sk-basis-...")

stream = client.chat.completions.create(
    model="basis-default",
    messages=[{"role": "user", "content": "Write a one paragraph summary."}],
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="")

Node (OpenAI SDK)

typescript
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://basis.watch/api/v1", apiKey: "sk-basis-..." });

const stream = await client.chat.completions.create({
  model: "basis-default",
  messages: [{ role: "user", content: "Write a one paragraph summary." }],
  stream: true,
});
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
  • model — a published model id (defaults to basis-default when omitted).
  • messages — a non-empty array of { role, content }. An empty or missing array is rejected with 400 invalid_messages.
  • stream — optional boolean; when true the response is an SSE stream (see below).

Streaming

With "stream": true, the response is text/event-stream. Each event is a data: line carrying a chat.completion.chunk object, separated by a blank line. The stream terminates with a literal data: [DONE] sentinel — identical to OpenAI, so SDK streaming loops work unchanged.

SSE wire format

text
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"basis-default","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"basis-default","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"basis-default","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

Python streaming loop

python
from openai import OpenAI

client = OpenAI(base_url="https://basis.watch/api/v1", api_key="sk-basis-...")

stream = client.chat.completions.create(
    model="basis-default",
    messages=[{"role": "user", "content": "Stream a short answer."}],
    stream=True,
)
for chunk in stream:
    piece = chunk.choices[0].delta.content
    if piece:
        print(piece, end="", flush=True)
print()  # the stream ends with a [DONE] sentinel the SDK consumes for you

Balance

Read a wallet's $BASIS credit balance with GET /api/v1/balance. Credits are the unit inference is metered in; the response also reports the raw $BASIS balance and the lifetime deposited/spent totals. There are two ways to address it:

  • Authenticated owner view — call with an sk-basis- key or a Privy token whose identity carries a linked wallet, and it returns that wallet's balance with no ?address= needed.
  • Public address view ?address=0x… reads any wallet's balance (read-only, anyone can read any wallet). A malformed address returns 400 invalid_address; neither path present returns 401.

Read a balance

bash
curl "https://basis.watch/api/v1/balance?address=0x0000000000000000000000000000000000000000"

Response

json
{
  "object": "balance",
  "credits": "0",
  "basis_balance_raw": "0",
  "basis_token_address": "",
  "usd_notional": null,
  "total_deposited_basis_raw": "0",
  "total_spent_basis_raw": "0",
  "status": "ok"
}
  • credits — spendable credits as an integer string.
  • basis_balance_raw — the same balance in $BASIS base units (18 decimals).
  • basis_token_address — the $BASIS contract address, or "" while the token is pending.
  • usd_notional null unless a price snapshot is available; never fabricated.
  • total_deposited_basis_raw / total_spent_basis_raw — lifetime totals in base units.
  • status — reflects whether deposits/credits are configured or pending.

Credits & payments

Inference is paid for in $BASIS credits. There are two ways to fund a balance, and they share one deterministic core: a credit's $BASIS value is set by the active pricing epoch and locked at reservation — deterministic within a quote and a receipt, but not a constant the network promises to hold for all time.

  • 1

    Pay with $BASIS directly

    If you already hold $BASIS, deposit it to fund credits — a flat, deterministic conversion with no market quote involved.

  • 2

    Route ETH / WETH / USDC into $BASIS

    If you hold ETH, WETH, or USDC on Base, request a payment quote that routes your token into $BASIS at payment time. The sell-token → $BASIS leg uses a fresh market quote with a TTL and a slippage floor. The user/agent wallet signs and submits the transaction — Basis never signs or custodies.

The payment routes are live as a contract; the router is configured in this deployment. $BASIS is live on Base — until the payment router and a quote provider are configured, payment routes return a structured pending response (basis_token_pending / quote_provider_pending, 503). live

The exact credit ↔ $BASIS unit, the quote TTL, slippage, and how failed jobs release reservations live under Credits.

Pay with $BASIS

If you already hold $BASIS, you fund credits by depositing it. The credit ↔ $BASIS conversion uses the active pricing epoch's BASIS-per-credit rate (basisPerCreditRaw) — oracle-free and deterministic to the last base unit within an epoch. The rate is versioned: a new epoch can change it for new conversions, always within the capped epoch-to-epoch bound, and never rewrites a locked snapshot.

deterministic

Reading your funded balance is the Balance endpoint. The deposit-to-credit conversion and the reserve → debit → release lifecycle are documented under Credits. Deposits are enabled once the $BASIS token and the credit vault are configured.

Route ETH / WETH / USDC → BASIS

A payer holding ETH, WETH, or USDC on Base can route into $BASIS at payment time. The flow is four read/prepare routes, and the user or agent wallet signs the swap — Basis never signs a transaction and never custodies funds.

1. Supported tokens

GET /api/payments/supported-tokens returns the chain id, the $BASIS output token (with its pending/live status), the accepted sell tokens, the router status, and which quote providers are configured.

bash
curl -s https://basis.watch/api/payments/supported-tokens

Response

json
{
  "chain_id": 8453,
  "basis_token": { "address": "", "symbol": "BASIS", "status": "pending" },
  "sell_tokens": [
    { "symbol": "ETH",  "address": "0xEeee...EEeE", "decimals": 18, "label": "Ether" },
    { "symbol": "WETH", "address": "0x4200...0006", "decimals": 18, "label": "Wrapped Ether" },
    { "symbol": "USDC", "address": "0x8335...2913", "decimals": 6,  "label": "USD Coin" }
  ],
  "router_status": "pending",
  "providers": { "zeroX": false, "uniswap": false, "aerodrome": false }
}

2. Request a quote

POST /api/payments/quote with a sell_token, your payer_wallet, and either a credit_amount or an explicit basis_amount_raw. Optionally pass slippage_bps (default 100) and a job_id. The response is a PaymentQuote whose to / calldata / value the wallet signs; min_basis_out_raw encodes the slippage floor, and the quote expires_at a short TTL.

bash
curl https://basis.watch/api/payments/quote \
  -H "Content-Type: application/json" \
  -d '{"sell_token":"USDC","payer_wallet":"0x0000000000000000000000000000000000000000","credit_amount":"100","slippage_bps":100}'

PaymentQuote (status: quotable)

json
{
  "quote_id": "q_...",
  "status": "quotable",
  "sell_token": "USDC",
  "sell_amount_raw": "...",
  "buy_token_symbol": "BASIS",
  "buy_amount_raw": "100000000000000000",
  "min_basis_out_raw": "99000000000000000",
  "slippage_bps": 100,
  "to": "0x... (router/aggregator)",
  "calldata": "0x...",
  "value": "0",
  "approval_target": "0x... (spender to approve for ERC-20 sells)",
  "expires_at": "2026-06-20T00:01:30Z",
  "warnings": []
}
  • quotable (200) — an executable quote was produced.
  • unsupported_token (422) — the sell token is not ETH/WETH/USDC.
  • basis_token_pending / quote_provider_pending / router_disabled (503) — the token, a provider, or the router is not yet configured.
  • slippage_too_high / invalid_request (400) — the requested slippage exceeds the ceiling, or the body is malformed.

3. Prepare

POST /api/payments/prepare with the quote_id returns the executable transaction fields to submit, or a { status: "pending" | "expired" } if the quote is not (or no longer) executable. A stale quote is never honored.

bash
curl https://basis.watch/api/payments/prepare \
  -H "Content-Type: application/json" \
  -d '{"quote_id":"q_..."}'

4. Confirm

After the wallet submits the swap (and deposit), call POST /api/payments/confirm with the quote_id and the swap_tx_hash and/or deposit_tx_hash. Confirm always requires authentication and a linked payer wallet — it credits a wallet, so anonymous callers are rejected and an unlinked payer returns 403 wallet_not_linked.

Crediting is gated on on-chain verification. In the default strict mode (when the router is enabled) Basis proves the tx on Base — correct chain, token, receiver, output at or above the slippage floor, enough confirmations, quote unexpired and unreplayed — before any credit; a tx hash is never trusted on its own. Until the tx is provable, confirm returns payment_verification_pending (200, credited: false) and the quote is not consumed, so you can retry once it lands. The quote is single-use: replaying a confirmed quote returns 409 duplicate_payment.

bash
curl https://basis.watch/api/payments/confirm \
  -H "Content-Type: application/json" \
  -d '{"quote_id":"q_...","swap_tx_hash":"0x...","deposit_tx_hash":"0x..."}'

Response — confirmed (verified on Base)

json
// strict mode (default when the router is enabled): the swap/deposit tx is
// PROVEN on Base before any credit. credited is a boolean, not an amount.
{
  "status": "confirmed",
  "credited": true,
  "verified": true,
  "credited_basis_raw": "100000000000000000",
  "confirmations": 12
}

Response — not yet provable

json
{
  "status": "payment_verification_pending",
  "credited": false,
  "detail": "a confirmed tx hash is required"
}

Agent auto-top-up

An agent can keep itself funded without a human in the loop: check the balance, and if it is low, request a quote, have its own wallet sign to / calldata / value, confirm the deposit, then run inference. Basis prepares the transaction; the agent signs and submits it.

Auto-top-up loop (Python)

python
import httpx
from openai import OpenAI

BASE = "https://basis.watch"
WALLET = "0xYourWallet"

def credits() -> int:
    r = httpx.get(f"{BASE}/api/v1/balance", params={"address": WALLET}).json()
    return int(r["credits"])

# 1. Check balance. If low, request a quote to route USDC -> BASIS.
if credits() < 10:
    quote = httpx.post(f"{BASE}/api/payments/quote", json={
        "sell_token": "USDC",
        "payer_wallet": WALLET,
        "credit_amount": "100",
        "slippage_bps": 100,
    }).json()
    # 2. The USER/AGENT wallet signs quote["to"] / ["calldata"] / ["value"].
    #    Basis NEVER signs or custodies. Submit the tx with your own signer.
    swap_tx = sign_and_send(quote["to"], quote["calldata"], quote["value"])  # your signer
    # 3. Confirm the deposit. confirm requires auth + a linked wallet; strict mode
    #    proves the tx on Base before crediting. While it is not yet provable you
    #    get payment_verification_pending (credited:false) — retry once it lands.
    httpx.post(f"{BASE}/api/payments/confirm", json={
        "quote_id": quote["quote_id"],
        "swap_tx_hash": swap_tx,
    }, headers={"Authorization": "Bearer sk-basis-..."})

# 4. Run inference once credits are sufficient.
client = OpenAI(base_url=f"{BASE}/api/v1", api_key="sk-basis-...")
print(client.chat.completions.create(
    model="basis-default",
    messages=[{"role": "user", "content": "Ready when funded."}],
).choices[0].message.content)
non-custodial

The signer is yours. Basis returns calldata for your wallet to sign — it never holds the agent's key and never executes the swap. The payment router is configured and can quote ETH/WETH/USDC → $BASIS today; live deposits credit once you submit a proven on-chain swap. When credits run out mid-flight, chat completions return 402 insufficient_basis_credits (see Errors) — the signal to top up.

Runtime status

Current runtime (derived from the single source of truth):Inference Live

In production, inference is live via a hosted upstream proxy, and the public API is a free metered demo (credits are not required). On a deploy with no inference backend configured (local/preview), chat completions instead return 503 with a structured runtime_pending error in the OpenAI error shape — the contract working as designed: the API surface is live, the backend is honestly absent. Detect it by the error.type / error.code rather than retrying blindly.

503 runtime_pending body

json
{
  "error": {
    "message": "Basis inference runtime is not yet configured. The OpenAI-compatible API contract is live; a worker backend is pending.",
    "type": "runtime_pending",
    "code": "runtime_pending"
  }
}

Check the network's configured-vs-pending state programmatically. GET /api/launch-status carries the token, contract, payment-router, persistence, auth, and backend-topology posture — each reported as configured or pending, never a promise.

bash
curl -s https://basis.watch/api/launch-status

Network & treasury data

Two more read-only, no-auth endpoints expose aggregate network state. GET /api/data (and /api/network/data) return privacy-safe network aggregates with a treasury block; GET /api/treasury returns the fee-stream, draft-allocation, and draft-burn surface. All counts are honest — zeros until traffic flows — and fee/treasury/burn fields stay pending until a fee source and the relevant config exist; nothing here ever fabricates a live metric.

bash
curl -s https://basis.watch/api/data
curl -s https://basis.watch/api/treasury

Inference upstream

When an operator configures a hosted OpenAI-compatible backend, the route flips from runtime_pending to a live proxy — no code change, no client change. The behavior is honest about which state it is in:

StateChat completionsModels
Pending503 runtime_pending (OpenAI error shape). No fake tokens are ever returned.available: false, backend.route: "pending"
Configured (proxy)Streams real tokens from the upstream; output tokens are counted server-side (authoritative).available: true, backend.route: "upstream_proxy"

Model mapping

You always request a published id (basis-default, basis-small, basis-large). Internally the gateway maps that to the operator's upstream model name; the upstream model id and the upstream key are never returned in any response, error, or /api/v1/models body.

Streaming & failure semantics

Streaming uses the same OpenAI SSE wire format as Streaming (data: {chunk} data: [DONE]). A stream that never reaches [DONE] — an upstream error, a network drop, a timeout, or a truncated upstream — meters the job as failed and creates no payable worker reward. A non-streaming upstream error returns 502 upstream_error / 502 upstream_unreachable rather than a 200 with fabricated content.

Gateway-served (proxied) jobs are metered, not paid: they record a verifiable receipt with workerRewardRaw: "0" because a hosted gateway is not a contributor GPU. Only worker-routed jobs — jobs the orchestrator places on a contributor GPU with a valid EVM reward address — can create a worker reward, and only for completed, verified work. Whether worker routing is active is reported by GET /api/launch-status (runtime.runtime: proxy for the hosted upstream vs worker for the contributor mesh) — not assumed from this page.

Receipts

Every settled job produces a verifiable inference receipt: a canonical-JSON, SHA-256-hashed accounting record. Look one up by its hash. The response includes the receipt and a verified boolean — Basis re-derives the hash server-side so you can confirm integrity.

Look up a receipt

bash
curl -s https://basis.watch/api/inference/receipts/<receipt_hash>

Response

json
{
  "receipt": {
    "receiptVersion": 2,
    "jobId": "...",
    "modelId": "basis-default",
    "promptTokens": 128,
    "outputTokens": 96,
    "totalChargedRaw": "...",
    "workerRewardRaw": "...",
    "protocolFeeRaw": "...",
    "pricing": {
      "pricingEpochId": "epoch-placeholder-001",
      "pricingSnapshotHash": "<sha256-hex>",
      "usageCreditsRaw": "...",
      "basisPerCreditRaw": "1000000000000000",
      "basisRequiredRaw": "...",
      "priceSource": "placeholder",
      "utilizationMultiplierBps": 10000,
      "supplyMultiplierBps": 10000,
      "demandMultiplierBps": 10000,
      "providerFloorMultiplierBps": 10000
    },
    "tokenSymbol": "BASIS",
    "tokenAddress": "",
    "chainId": 8453,
    "receiptHash": "<receipt_hash>",
    "status": "completed"
  },
  "verified": true
}

List recent receipts

GET /api/inference/receipts returns the most recent receipts (newest first), with an optional ?limit= (default 50, max 200). The response also reports the store's honest persistence posture — an in-process store is non-durable and resets, so counts read zero until traffic flows and the response says so.

bash
curl -s https://basis.watch/api/inference/receipts?limit=20

Response

json
{
  "receipts": [ /* InferenceReceipt[] */ ],
  "count": 0,
  "persistence": { "status": "postgres_configured", "durable": true, "note": "…" }
}
verifiable

See Inference receipts for every field, the hashing scheme, and how to re-derive the hash yourself. An unknown hash returns 404 not_found.

Errors

Errors use the OpenAI error envelope: { error: { message, type, code } }. Branch on type and code, not on the message string.

StatusTypeCodeWhen
400invalid_request_errorinvalid_bodyThe request body is not valid JSON.
400invalid_request_errorinvalid_messages`messages` is missing, not an array, or empty.
400invalid_request_errortoo_many_messagesThe request carries more than the per-request message cap (BASIS_INFERENCE_MAX_MESSAGES, default 64).
400invalid_request_errorprompt_too_largeThe combined prompt text exceeds the per-request character cap (BASIS_INFERENCE_MAX_PROMPT_CHARS, default 200000).
400invalid_request_errorinvalid_max_tokens`max_tokens` is not a positive integer (or is absurdly large). The output cap is enforced regardless (BASIS_INFERENCE_MAX_OUTPUT_TOKENS, default 2048) so a single request can never generate unbounded billable output.
413invalid_request_errorrequest_too_largeThe request body exceeds the byte cap (BASIS_INFERENCE_MAX_BODY_BYTES, default 262144) — rejected before it is parsed.
401auth_requiredauth_requiredAuthentication is required (BASIS_AUTH_REQUIRED=true) and no Privy token or API key was supplied — including an unknown or revoked sk-basis- key, which resolves to no identity. While the public API is open, a key is accepted but not required.
401invalid_auth_tokeninvalid_privy_tokenA Privy access token was supplied but failed JWKS verification (bad signature, wrong issuer/audience, or expired).
402insufficient_basis_creditsinsufficient_basis_creditsThe caller's $BASIS credit balance cannot cover the request. Top up with $BASIS, or route ETH/WETH/USDC into $BASIS via the payment routes.
404invalid_request_errormodel_not_foundThe `model` field is not one of the published Basis models (basis-small / basis-default / basis-large).
409invalid_request_errorduplicate_paymentA payment confirm was replayed — the same `quote_id` was already consumed. Confirmation is idempotent; the quote is single-use.
422invalid_request_errorunsupported_payment_tokenA payment `sell_token` is not one of the supported inputs (ETH, WETH, USDC).
429rate_limit_errorrate_limitedToo many requests in a window — a best-effort, per-instance limit with low anonymous caps and higher caps for an authenticated identity or API key. Honor the `retry-after` header and back off. Disable with BASIS_RATE_LIMIT_ENABLED=false.
502api_errorupstream_errorThe configured upstream inference backend returned a non-2xx response (non-streaming). A streaming job that never reaches [DONE] is instead metered failed and the stream ends gracefully.
502api_errorupstream_unreachableThe upstream backend was unreachable or the request timed out (BASIS_INFERENCE_UPSTREAM_TIMEOUT_MS); the job is metered failed and no content is fabricated.
503runtime_pendingruntime_pendingNo inference backend is configured. The API contract is live; the worker backend is pending.
503runtime_pendinginference_disabledThe operator kill switch is on (BASIS_INFERENCE_DISABLED=true). The OpenAI-compatible contract stays live, but no upstream is dialed and zero spend is incurred; it clears when the switch is removed.
503basis_token_pendingbasis_token_pendingA payment was requested before the $BASIS token address is configured. The payment surface is live; the token is pending.
503quote_provider_pendingquote_provider_pendingA payment quote was requested before a swap-quote provider (0x / Uniswap / Aerodrome) is configured.

402 insufficient_basis_credits body

json
{
  "error": {
    "message": "Insufficient $BASIS credits for this request. Top up by depositing $BASIS, or route ETH/WETH/USDC into $BASIS via /api/payments/quote.",
    "type": "insufficient_basis_credits",
    "code": "insufficient_basis_credits"
  }
}

Not every non-success is an error. payment_verification_pending is returned by /api/payments/confirm with HTTP 200 and credited: false when a payment is not yet provable on-chain — it is a retry signal, not a failure (see Confirm).

Payment routes can sell 8453-chain ETH, WETH, and USDC into $BASIS; the router is configured in this deployment.

Building agents

Because Basis is OpenAI-compatible, you point an agent framework at it the same way you point the raw SDK: set the base URL, the API key, and a model. Every framework below needs the same three values.

The three values

python
# Any OpenAI-compatible framework needs the same three values:
base_url = "https://basis.watch/api/v1"
api_key  = "sk-basis-..."   # accepted; required only when BASIS_AUTH_REQUIRED=true
model    = "basis-default"

OpenAI Agents SDK (Python)

python
from agents import Agent, Runner, OpenAIChatCompletionsModel
from openai import AsyncOpenAI

client = AsyncOpenAI(base_url="https://basis.watch/api/v1", api_key="sk-basis-...")

agent = Agent(
    name="Researcher",
    instructions="You are a concise research assistant.",
    model=OpenAIChatCompletionsModel(model="basis-default", openai_client=client),
)
result = Runner.run_sync(agent, "Summarize the Base agent economy in two sentences.")
print(result.final_output)

LangChain (Python)

python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    base_url="https://basis.watch/api/v1",
    api_key="sk-basis-...",
    model="basis-default",
    streaming=True,
)
for chunk in llm.stream("Write a one sentence summary for an agent."):
    print(chunk.content, end="", flush=True)

Vercel AI SDK (TypeScript)

typescript
import { createOpenAI } from "@ai-sdk/openai";
import { streamText } from "ai";

const basis = createOpenAI({
  baseURL: "https://basis.watch/api/v1",
  apiKey: "sk-basis-...",
});

const { textStream } = await streamText({
  model: basis("basis-default"),
  prompt: "Write a one paragraph summary for an agent.",
});
for await (const delta of textStream) process.stdout.write(delta);