Wuraw
Open App

Documentation

One endpoint, every model

If your code already speaks the OpenAI chat-completions shape, it already speaks to Wuraw. Change the base URL, drop the key, and let the wallet answer the 402.

v1 · stable400+ models402 is not an error

01

Quickstart

Two requests. The first is unpaid and comes back as a quote. The second carries the signature and streams.

The base URL is the API host for your deployment — written here as api.<your-domain>. Free-tier models need nothing else: no key, no account, no header. Metered models answer 402 Payment Required with machine-readable terms, and your wallet signs them.

bash
# 1 · call a free-tier model. Nothing to sign, nothing to sign up for.
curl https://api.<your-domain>/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "wuraw/dolphin-8b",
    "messages": [{"role": "user", "content": "Say hi in nine words."}]
  }'

# 2 · call a metered model with no payment attached — a quote comes back.
curl -i https://api.<your-domain>/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "claude-sonnet-5", "messages": [{"role":"user","content":"Hi"}]}'

HTTP/1.1 402 Payment Required
Content-Type: application/json
{
  "accepts": [{
    "scheme":  "exact",
    "network": "robinhood-chain",
    "asset":   "USDG",
    "maxAmountRequired": "0.0182",
    "payTo":   "0x…",
    "resource": "/v1/chat/completions",
    "maxTimeoutSeconds": 60
  }]
}

Repeating the request with the signed payment header attached returns 200 and the usual streaming body. Doing that by hand is instructive exactly once; after that, use the SDK, which owns the 402 loop, the signature and the retry.

typescript
import { Wuraw } from "@wuraw-protocol/sdk";

const router = new Wuraw({
  baseUrl: "https://api.<your-domain>/v1",
  wallet,                            // any EIP-3009 capable signer
  budget: { perRequestMax: "0.05", perDayMax: "10.00" },
});

const stream = await router.chat.completions.create({
  model: "claude-sonnet-5",
  messages: [{ role: "user", content: "Explain HTTP 402 in two sentences." }],
  stream: true,
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

// Settlement metadata rides along with the finished response.
console.log(stream.settlement);
// { asset: "USDG", amount: "0.0182", chainId: 4663, txHash: "0x…" }

Install it from npm as @wuraw-protocol/sdk. It ships types, so a wrong model alias or a malformed budget is a compile error rather than a 400.

02

Endpoint contract

OpenAI-compatible where it counts, and explicit about where it is not.

Method · PathPurposePayment
POST /v1/chat/completionsChat and completion, streaming or buffered. The workhorse.Per request
POST /v1/embeddingsVector embeddings. A batched input is one payment, not one per item.Per request
POST /v1/images/generationsImage models, priced per image rather than per token.Per image
GET /v1/modelsThe catalog: every model, its providers, context window and rate card.Free
GET /v1/models/{id}One model, with per-provider health and current queue depth.Free
GET /v1/settlementSettled transfers for a payer address, newest first.Free

Request and response bodies follow the OpenAI chat-completions shape: model, messages, temperature, max_tokens, tools, stream. Streaming is server-sent events with the same delta envelope. Any client library that lets you override the base URL works unmodified.

Two deliberate differences. First, there is no Authorization header, because there is no key and no account — identity, where it exists at all, is the payer address recovered from a signature. Second, every response carries a routing object naming the provider that served it, the resolved model, the observed overhead and the settlement reference. Log it; it is what makes an incident reconstructable.

json
{
  "id": "cmpl_8Jd2…",
  "object": "chat.completion",
  "model": "claude-sonnet-5",
  "choices": [{ "index": 0, "message": { "role": "assistant", "content": "…" } }],
  "usage": { "prompt_tokens": 412, "completion_tokens": 288, "total_tokens": 700 },
  "routing": {
    "tier": "surplus",
    "provider": "surplus-eu-2",
    "resolved_model": "claude-sonnet-5",
    "policy": "quality",
    "attempts": 1,
    "overhead_ms": 11
  },
  "settlement": {
    "asset": "USDG",
    "amount": "0.0182",
    "chain_id": 4663,
    "tx_hash": "0x…",
    "facilitator": "fac-02"
  }
}

03

Paying with x402

Payment happens inside the request cycle. There is no checkout, no session and no server-side balance.

01

Request

Your client calls the router with no key and no account attached.

02

402

The router answers 402 Payment Required with the price, asset, network and payee.

03

Authorize

The wallet signs a gasless EIP-3009 transfer authorization and re-sends.

04

Verify

A facilitator validates the signed payload off-chain in a single round trip.

05

Settle

The transfer lands onchain, the tx hash comes back, and the response streams.

The authorization is an EIP-3009 transferWithAuthorization signature: a message committing to a sender, a recipient, an exact amount, a validity window and a random nonce. It is not a transaction. The payer holds no gas and broadcasts nothing — the facilitator submits the transfer and pays gas on Robinhood Chain, then hands back the transaction hash.

json
// what the wallet signs — an authorization, not a transaction
{
  "from":        "0x…payer",
  "to":          "0x…payee",
  "value":       "18200",           // USDG has 6 decimals → $0.0182
  "validAfter":  1785312000,
  "validBefore": 1785312060,        // a 60-second window
  "nonce":       "0x…random"
}

A note on header names

We deliberately do not name the payment header in prose. The community specification and the current vendor documentation disagree on it, and the rename looks like a version boundary rather than a typo. Read the header name out of the 402 challenge your client receives and echo the scheme it asks for. What is stable across both versions — and what you should code against — is the EIP-3009 authorization payload itself.

A verified authorization is reusable within its validity window for the exact amount it names. In practice the SDK signs a fresh one per request, because the price depends on token count and the window is short by design. Nothing is pre-authorized, and no allowance is left standing after the window closes.

If an upstream fails after settlement has landed — a genuine 5xx from the provider, not a refusal — the router issues a credit note against the payer address, redeemable on the next request. It cannot reverse a settled onchain transfer, and we would rather say that plainly than imply a refund path that does not exist.

04

Agent budgets

A spend cap enforced by the signer, not by the counterparty being paid.

Give each agent its own signer. A cap attached to that signer is checked before a request is signed, which means an over-budget call is never sent and no server has to be trusted to decline it. That is the practical difference between a budget and a balance: a balance is discovered by exhausting it.

typescript
const research = new Wuraw({
  baseUrl: "https://api.<your-domain>/v1",
  wallet:  researchAgentSigner,     // one key per agent, funded deliberately
  budget: {
    perRequestMax: "0.08",          // refuse to sign above this
    perDayMax:     "5.00",          // rolling 24h, tracked locally
    asset:         "USDG",
    onExceeded:    "reject",        // "reject" | "downgrade" | "queue"
  },
});

try {
  await research.chat.completions.create({ model, messages });
} catch (err) {
  if (err instanceof BudgetExceeded) {
    // err.spent, err.remaining, err.window
    return degradeGracefully(err);
  }
  throw err;
}
onExceededBehaviourFits
rejectThrows BudgetExceeded locally. Nothing is signed and nothing is sent.Anything customer-facing, where a wrong answer is worse than no answer.
downgradeRe-routes to the cheapest model in the same alias family and continues.Monitoring, classification, background enrichment.
queueDefers the call to the next budget window and returns a handle.Batch work with no interactive caller waiting on it.

Attribution comes free. Because every paid call settles onchain against the agent’s own address, the spend record is the settlement itself rather than a log line the router generates and could get wrong. Filter GET /v1/settlement by payer address, or read it straight off Robinhood Chain, and you have an independently verifiable ledger for that agent.

The cost of that property is publicity: amounts, timestamps and addresses are public and permanent. Prompts and completions are never written onchain, but the rhythm and rough size of an agent’s workload are visible to anyone who knows its address. Rotate signers per period if that matters to you.

05

Routing policies

A request names a model. The router picks which upstream serves it.

policyOptimizes forBehaviour
costLowest settled priceCheapest healthy provider for the resolved model. Ties break toward the lower observed P99.
latencyTime to first tokenLowest recent TTFT, measured continuously per provider and region rather than read from a static table.
qualityReference behaviourPrefers the model author’s own first-party endpoint over partner-operated ones. The default.

Fallback routing. Supply an ordered fallback array and a failed attempt walks it. Only idempotent failures are retried — connection errors, 5xx, timeouts and upstream 429s. A 400 from the provider is your bug and comes back unchanged rather than being replayed against three more providers at your expense. Every attempt is listed in the response’s routing.attempts.

Provider preference. Pin, prefer or exclude upstreams per request. Exclusion is a hard constraint: if the only remaining candidate is excluded, the call fails rather than silently violating it. Preference is soft and yields to health.

typescript
await router.chat.completions.create({
  model: "wuraw/reasoner",
  messages,
  routing: {
    policy:   "latency",
    fallback: ["claude-haiku-4-5", "gpt-5.6-luna"],
    providers: {
      prefer:  ["surplus"],         // soft — yields to health
      exclude: ["dolphin"],         // hard — never serve this from the free tier
    },
    maxAttempts: 3,
  },
});

Health is observed rather than declared. A provider that returns errors or slips its latency band is de-weighted within seconds and probed back in gradually. This is the mechanism behind the availability argument: two independent providers with automatic failover cut expected downtime from roughly 41 hours a year to about 12 minutes.

06

Model aliases

Name a role in your code and resolve it to a vendor SKU at route time.

Writing a provider’s model identifier into your application couples your call sites to somebody else’s deprecation calendar. An alias — anything in the wuraw/ namespace — is a name you own that resolves at route time, so a retirement becomes a reviewable config diff instead of a search across the repository.

json
{
  "wuraw/reasoner": {
    "primary":  "claude-opus-5",
    "fallback": ["gpt-5.6-sol", "gemini-3-pro"],
    "policy":   "quality",
    "pin":      "2026-07-24"
  },
  "wuraw/summarizer": {
    "primary":  "claude-haiku-4-5",
    "fallback": ["gpt-5.6-luna", "deepseek-v4-flash"],
    "policy":   "cost"
  }
}

Pin an alias when reproducibility matters more than availability. A pinned alias stops re-resolving, so last Tuesday’s output can be traced to the exact model that produced it; an unpinned one follows the catalog. Either way the resolved model is echoed in routing.resolved_model on every response, so you are never guessing after the fact. Names beginning with wuraw/ are reserved for aliases and will never collide with an upstream identifier.

07

Status codes

402 is a quote, not a failure. Treat it as control flow.

CodeMeaningWhat to do
200Completion returned. Payment settled if the tier is metered.Read routing and settlement off the body and log both.
400Malformed request — unknown field, bad message shape, bad tool schema.Fix the request. Never retried across providers.
402Payment required. The body carries price, asset, network and payee.Sign the EIP-3009 authorization and repeat the request.
403Payment verified, resource refused — an exclusion left no candidate provider.Relax the exclusion or choose another model.
404No such model or alias in the catalog.Check GET /v1/models; the alias may have been retired.
409Authorization nonce already used, or its validity window has closed.Sign a fresh authorization. Do not replay the old one.
413Prompt exceeds the resolved model’s context window.Trim the context, or route to a larger-context model.
429Rate limited by your own tier, not by the upstream.Back off exponentially with jitter; Retry-After is authoritative.
502Every provider for this model failed after fallback.Retry later or widen the fallback list. A credit note is issued if payment had settled.
503Settlement path unavailable — facilitator or chain degraded.Free-tier models keep serving; metered calls are queued or refused.
504Upstream exceeded the request deadline.Retried automatically up to maxAttempts before you ever see it.

There is no 401. The router holds no credentials to reject, so authentication failure is not a state it can reach. Anything you would have handled as a 401 elsewhere is a 402 here — the difference being that a 402 tells you the price instead of telling you to go and get a key.

08

Rate limits

Limits follow the tier, and on metered tiers they follow settled volume rather than an account plan.

TierRequests / minConcurrencyBasis
Dolphin (free)202Per payer address, or per IP where no address is present. Never metered, never billed.
Surplus (metered)60032Per payer address, raised automatically as settled volume accumulates.
AntSeed (metered)60032Shares the payer’s Surplus allowance, so fallback traffic does not double-count.
Catalog reads1208GET /v1/models and GET /v1/settlement. Cache-friendly and cheap to serve.

Upstream providers impose their own limits underneath ours, and those change without notice — that is the normal condition of this market, not an exception. When an upstream returns a 429 the router treats it as a health signal and shifts traffic rather than passing the failure through. You only see a 429 from us when your own tier limit is the binding one, and that response always carries Retry-After.

Every 429 and every fallback decision is visible on the status page, per provider, with the observed windows. If a limit is wrong for your workload, the 19 settlement networks we accept usually mean there is a funding path that fits — ask before engineering around it.