@blockrun/clawrouter 0.12.282 → 0.12.283

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,11 +9,11 @@ Agents can only sign transactions.<br><br>
9
9
  <strong>ClawRouter is the only LLM router that lets agents operate independently.</strong><br><br>
10
10
  <em>Pay however you like: <strong>credit card</strong> — sign up at <a href="https://user.blockrun.ai"><strong>user.blockrun.ai</strong></a>, top up, mint an API key<br>
11
11
  or <strong>USDC</strong> on Solana or Base (x402 wallet, no account at all).<br>
12
- Or neither — <!-- br:models.free -->6<!-- /br:models.free --> models are free, with no signup, no key and no wallet.</em></p>
12
+ Or neither — <!-- br:models.free -->7<!-- /br:models.free --> models are free, with no signup, no key and no wallet.</em></p>
13
13
 
14
14
  <br>
15
15
 
16
- <img src="https://img.shields.io/badge/🆓_6_Free_Models-success?style=for-the-badge" alt="6 free models">&nbsp;
16
+ <img src="https://img.shields.io/badge/🆓_7_Free_Models-success?style=for-the-badge" alt="7 free models">&nbsp;
17
17
  <img src="https://img.shields.io/badge/🤖_Agent--Native-black?style=for-the-badge" alt="Agent native">&nbsp;
18
18
  <img src="https://img.shields.io/badge/💳_Card_or_USDC-blue?style=for-the-badge" alt="Card or USDC">&nbsp;
19
19
  <img src="https://img.shields.io/badge/⚡_Local_Routing-yellow?style=for-the-badge" alt="Local routing">&nbsp;
@@ -36,7 +36,7 @@ Or neither — <!-- br:models.free -->6<!-- /br:models.free --> models are free,
36
36
 
37
37
  </div>
38
38
 
39
- > **ClawRouter** is an open-source smart LLM router that reduces AI API costs by up to <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->%. It analyzes each request across <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions --> dimensions and routes to the cheapest capable model in under 1ms, entirely locally. It is the only LLM router that an autonomous agent can pay for by itself — a wallet signature is the account, and USDC micropayments over the x402 protocol are the billing — and it takes an ordinary credit card too: sign up at [user.blockrun.ai](https://user.blockrun.ai), top up, and run the same router on an API key. <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models from OpenAI, Anthropic, Google, xAI, DeepSeek, and more. MIT licensed.
39
+ > **ClawRouter** is an open-source smart LLM router that reduces AI API costs by up to <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->%. It analyzes each request across <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions --> dimensions and routes to the cheapest capable model in under 1ms, entirely locally. It is the only LLM router that an autonomous agent can pay for by itself — a wallet signature is the account, and USDC micropayments over the x402 protocol are the billing — and it takes an ordinary credit card too: sign up at [user.blockrun.ai](https://user.blockrun.ai), top up, and run the same router on an API key. <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models from OpenAI, Anthropic, Google, xAI, DeepSeek, and more. MIT licensed.
40
40
 
41
41
  ---
42
42
 
@@ -48,7 +48,7 @@ Not a seat, not a subscription, not a monthly minimum you keep paying while the
48
48
 
49
49
  | | |
50
50
  | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | **Every LLM** | <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> chat models across 9 providers, behind one endpoint and one balance |
51
+ | **Every LLM** | <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> chat models across 9 providers, behind one endpoint and one balance |
52
52
  | **Every tool** | <!-- br:models.image -->12<!-- /br:models.image --> image models, <!-- br:models.video -->8<!-- /br:models.video --> video models, music, <!-- br:models.speech -->5<!-- /br:models.speech --> voices, img2img editing, and outbound phone calls that come back as transcripts |
53
53
  | **Every data source** | web, news and neural search; prediction markets; live crypto and equity quotes; on-chain SQL over 100M+ labeled wallets; DEX routing; RPC across <!-- br:chains.rpc -->40<!-- /br:chains.rpc --> chains |
54
54
  | **Best value per dollar** | <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions -->-dimension local routing sends each request to the cheapest model that can actually do it — <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% below pinning Claude Opus 5 on the same traffic, computed from a published workload mix rather than estimated |
@@ -65,7 +65,7 @@ Every other LLM router was built for **human developers** — create an account,
65
65
 
66
66
  ClawRouter is built for the agent-first world:
67
67
 
68
- - **Starts at $0** — <!-- br:models.free -->6<!-- /br:models.free --> open-weight models are free forever (incl. a 1M-context reasoner and two sub-second coders). Image turns route to paid models — see the free-tier note below.
68
+ - **Starts at $0** — <!-- br:models.free -->7<!-- /br:models.free --> open-weight models are free forever (incl. a 1M-context reasoner and two sub-second coders). Image turns route to paid models — see the free-tier note below.
69
69
  - **No accounts needed** — a wallet is generated locally, no signup
70
70
  - **No API keys needed** — your wallet signature IS authentication
71
71
  - **No model selection** — <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions -->-dimension scoring + constraint-first ranking ([router-core](https://github.com/BlockRunAI/router-core)) picks the right model automatically
@@ -76,7 +76,7 @@ This is the stack that lets agents operate autonomously: **x402 + USDC + local r
76
76
 
77
77
  ### …and if you are a human, not an agent
78
78
 
79
- Every "no X needed" above is a capability, not a restriction. **ClawRouter takes a credit card too.** Sign up at **[user.blockrun.ai](https://user.blockrun.ai)**, top up, mint an API key, and run `clawrouter login brk_live_…`. Same router, same <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models, same local routing — calls go to `api.blockrun.ai` on a bearer token and bill your account credit. No crypto, no gas, no seed phrase.
79
+ Every "no X needed" above is a capability, not a restriction. **ClawRouter takes a credit card too.** Sign up at **[user.blockrun.ai](https://user.blockrun.ai)**, top up, mint an API key, and run `clawrouter login brk_live_…`. Same router, same <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, same local routing — calls go to `api.blockrun.ai` on a bearer token and bill your account credit. No crypto, no gas, no seed phrase.
80
80
 
81
81
  | You are | Pay with | Setup |
82
82
  | ------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------- |
@@ -92,8 +92,8 @@ Full walkthrough: [Paying with a credit card](#paying-with-a-credit-card-api-key
92
92
 
93
93
  | | OpenRouter | LiteLLM | Martian | Portkey | **ClawRouter** |
94
94
  | ---------------- | ----------------- | ---------------- | ----------------- | ----------------- | ---------------------------------------------------------------------- |
95
- | **Models** | 200+ | 100+ | Smart routing | Gateway | **<!-- br:models.chatVisible -->82<!-- /br:models.chatVisible -->** |
96
- | **Free tier** | Rate-limited | BYO keys | No | No | **<!-- br:models.free -->6<!-- /br:models.free --> models, no signup** |
95
+ | **Models** | 200+ | 100+ | Smart routing | Gateway | **<!-- br:models.chatVisible -->88<!-- /br:models.chatVisible -->** |
96
+ | **Free tier** | Rate-limited | BYO keys | No | No | **<!-- br:models.free -->7<!-- /br:models.free --> models, no signup** |
97
97
  | **Routing** | Manual selection | Manual selection | Smart (closed) | Observability | **Smart (open source)** |
98
98
  | **Auth** | Account + API key | Your API keys | Account + API key | Account + API key | **Wallet signature _or_ API key** |
99
99
  | **Payment** | Credit card | BYO keys | Credit card | $49-499/mo | **USDC per-request _or_ card credit** |
@@ -109,7 +109,7 @@ Full walkthrough: [Paying with a credit card](#paying-with-a-credit-card-api-key
109
109
 
110
110
  ## Quick Start
111
111
 
112
- > **No wallet? <!-- br:models.free -->6<!-- /br:models.free --> models work free out of the box.** Install, run, and pin `free/nemotron-3.5-lightning` (or any of the <!-- br:models.free -->6<!-- /br:models.free -->) — no crypto, no signup, no balance required. Add a credit card or USDC later when you want paid models.
112
+ > **No wallet? <!-- br:models.free -->7<!-- /br:models.free --> models work free out of the box.** Install, run, and pin `free/nemotron-3.5-lightning` (or any of the <!-- br:models.free -->7<!-- /br:models.free -->) — no crypto, no signup, no balance required. Add a credit card or USDC later when you want paid models.
113
113
 
114
114
  **Two ways to pay for the paid ones — pick one, the rest of ClawRouter is identical:**
115
115
 
@@ -127,7 +127,7 @@ is all it takes to switch and `clawrouter logout` is all it takes to switch back
127
127
 
128
128
  > **Where do I sign up?** [**user.blockrun.ai**](https://user.blockrun.ai) — sign in with
129
129
  > Google, add credit with a card under **Credits**, mint a key under **API Keys**. That
130
- > one key covers all <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> chat models
130
+ > one key covers all <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> chat models
131
131
  > plus images, video, speech and the live-data APIs. No wallet, no crypto, no seed phrase.
132
132
 
133
133
  ### Desktop control plane (macOS)
@@ -171,7 +171,7 @@ openclaw gateway restart
171
171
 
172
172
  > **Using Claude Code?** Check out [BRCC](https://github.com/BlockRunAI/brcc) — it's purpose-built for Claude Code with the same smart routing and x402 payments.
173
173
  >
174
- > **Using NousResearch Hermes?** See [ClawRouter-Hermes](https://github.com/BlockRunAI/ClawRouter-Hermes) — a Python plugin that wires Hermes into the ClawRouter proxy. Same wallet, same <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models, same x402 USDC settlement on Solana & Base.
174
+ > **Using NousResearch Hermes?** See [ClawRouter-Hermes](https://github.com/BlockRunAI/ClawRouter-Hermes) — a Python plugin that wires Hermes into the ClawRouter proxy. Same wallet, same <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, same x402 USDC settlement on Solana & Base.
175
175
 
176
176
  No OpenClaw required. ClawRouter runs as a local proxy on port 8402.
177
177
 
@@ -182,7 +182,7 @@ npx @blockrun/clawrouter
182
182
  ```
183
183
 
184
184
  **2. Fund your wallet** — optional, skip for free tier
185
- Your wallet address is printed on first run. For paid models, send a few USDC on Solana (the default chain) or Base — $5 covers thousands of requests. Prefer a card? [Sign up at user.blockrun.ai](https://user.blockrun.ai) and run `clawrouter login brk_live_…` instead. To stay at $0, pin any of the <!-- br:models.free -->6<!-- /br:models.free --> free models (e.g. `free/nemotron-3.5-lightning`) or use `/model free` inside OpenClaw.
185
+ Your wallet address is printed on first run. For paid models, send a few USDC on Solana (the default chain) or Base — $5 covers thousands of requests. Prefer a card? [Sign up at user.blockrun.ai](https://user.blockrun.ai) and run `clawrouter login brk_live_…` instead. To stay at $0, pin any of the <!-- br:models.free -->7<!-- /br:models.free --> free models (e.g. `free/nemotron-3.5-lightning`) or use `/model free` inside OpenClaw.
186
186
 
187
187
  **3. Point your client at `http://localhost:8402`**
188
188
 
@@ -485,7 +485,7 @@ No Surf account, no API key — settles directly to Surf's Base treasury in USDC
485
485
 
486
486
  ## Models & Pricing
487
487
 
488
- <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models across 9 providers, one wallet. **<!-- br:models.free -->6<!-- /br:models.free --> models are $0 — paid models start at fractions of a cent.**
488
+ <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models across 9 providers, one wallet. **<!-- br:models.free -->7<!-- /br:models.free --> models are $0 — paid models start at fractions of a cent.**
489
489
 
490
490
  > **💡 "Cost per request"** = estimated cost for a typical chat message (~500 input + 500 output tokens). Paid requests also carry a flat **$0.001/tx settlement fee** (covers on-chain gas; already included in the price the gateway quotes). Free models never pay it.
491
491
 
@@ -585,7 +585,7 @@ No Surf account, no API key — settles directly to Surf's Base treasury in USDC
585
585
  | openai/gpt-5.4-pro | $30.00 | $180.00 | $0.1050 | 1M | reasoning, tools |
586
586
  | openai/gpt-5.5-pro | $30.00 | $180.00 | $0.1050 | 1M | reasoning, vision, tools |
587
587
 
588
- > **Free tier:** <!-- br:models.free -->6<!-- /br:models.free --> open-weight models cost nothing — `/model free` pins the free default (`nemotron-3.5-lightning`, with the other five as fallbacks), `/model eco` opens on it, or pick one directly (e.g., `/model lightning` for 1M-context reasoning, `/model north-mini` or `/model laguna` for fast coding).
588
+ > **Free tier:** <!-- br:models.free -->7<!-- /br:models.free --> open-weight models cost nothing — `/model free` pins the free default (`nemotron-3.5-lightning`, with the other five as fallbacks), `/model eco` opens on it, or pick one directly (e.g., `/model lightning` for 1M-context reasoning, `/model north-mini` or `/model laguna` for fast coding).
589
589
  >
590
590
  > **No free vision.** Two free models are catalogued as vision-capable and neither reliably is: on a 64×64 solid-colour probe `nemotron-3-nano-omni` was right 1 of 4 times on Base and answered "white" for red on Solana, and `llama-3.2-11b-vision` replied "I'm unable to see the image" 3 of 3 while answering text fine. Both return HTTP 200, so a wrong answer arrives with no error to branch on. ClawRouter therefore does not flag them for vision, and requests carrying an `image_url` route to a paid vision model instead.
591
591
  > **Best value:** `gpt-5-nano` and `gemini-2.5-flash-lite` deliver strong results at ~$0.0003/request.
@@ -728,7 +728,7 @@ chains, the response cache and the local spend controls.
728
728
  - **Balance lives server-side.** There is no local balance to check: a call that
729
729
  outruns your credit comes back as HTTP `402 insufficient_quota`, naming the
730
730
  top-up page. Free models stay free and need no credit at all — all
731
- <!-- br:models.free -->6<!-- /br:models.free --> are served on the API-key gateway too.
731
+ <!-- br:models.free -->7<!-- /br:models.free --> are served on the API-key gateway too.
732
732
  - **One key covers nearly everything.** Verified live against `api.blockrun.ai`
733
733
  on 2026-09-05: chat completions (`/v1/chat/completions`), the Anthropic-shaped
734
734
  `/v1/messages`, `/v1/models`, image generation, speech, video, and the partner
@@ -881,7 +881,7 @@ npm test
881
881
 
882
882
  **The LLM router built for autonomous agents**
883
883
 
884
- You're here. <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models, local smart routing, and two ways to pay — a credit-card-funded API key, or x402 USDC from a wallet, which is what lets an agent operate independently.
884
+ You're here. <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, local smart routing, and two ways to pay — a credit-card-funded API key, or x402 USDC from a wallet, which is what lets an agent operate independently.
885
885
 
886
886
  `curl -fsSL https://blockrun.ai/ClawRouter-update | bash`
887
887
 
@@ -892,7 +892,7 @@ You're here. <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> mod
892
892
 
893
893
  **BlockRun for Claude Code**
894
894
 
895
- Run Claude Code with <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models, no rate limits, no Anthropic account, no phone verification. Pay per request with USDC — your wallet is your identity.
895
+ Run Claude Code with <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, no rate limits, no Anthropic account, no phone verification. Pay per request with USDC — your wallet is your identity.
896
896
 
897
897
  `curl -fsSL https://blockrun.ai/brcc-install | bash`
898
898
 
@@ -905,7 +905,7 @@ Run Claude Code with <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible
905
905
 
906
906
  **ClawRouter for NousResearch Hermes**
907
907
 
908
- Python plugin that wraps the ClawRouter proxy for `hermes-agent`. Same <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models, same x402 USDC payments on Solana & Base, native Hermes ergonomics.
908
+ Python plugin that wraps the ClawRouter proxy for `hermes-agent`. Same <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, same x402 USDC payments on Solana & Base, native Hermes ergonomics.
909
909
 
910
910
  `pip install hermes-plugin-clawrouter`
911
911
 
@@ -949,7 +949,7 @@ Python plugin that wraps the ClawRouter proxy for `hermes-agent`. Same <!-- br:m
949
949
 
950
950
  ### What is BlockRun?
951
951
 
952
- BlockRun lets agents pay for the outcome — every LLM, tool and data source, best value per dollar. One endpoint and one balance covering <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> chat models plus image, video, music and speech generation, search, market data and multi-chain RPC, billed per call with USDC over [x402](https://x402.org) or a credit-card-funded API key — no seat, no subscription, no minimum. ClawRouter is the open-source local router agents use to reach it.
952
+ BlockRun lets agents pay for the outcome — every LLM, tool and data source, best value per dollar. One endpoint and one balance covering <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> chat models plus image, video, music and speech generation, search, market data and multi-chain RPC, billed per call with USDC over [x402](https://x402.org) or a credit-card-funded API key — no seat, no subscription, no minimum. ClawRouter is the open-source local router agents use to reach it.
953
953
 
954
954
  ### What is ClawRouter?
955
955
 
@@ -977,7 +977,7 @@ No. A wallet is the default because it is what lets an _agent_ pay for itself,
977
977
  but you can sign up at **[user.blockrun.ai](https://user.blockrun.ai)**, top up
978
978
  with a credit card, mint an API key and run `clawrouter login brk_live_…`. Calls
979
979
  then go to `api.blockrun.ai` on a bearer token and bill your account credit — no
980
- crypto, no gas, no seed phrase. And the <!-- br:models.free -->6<!-- /br:models.free --> free models need neither: no
980
+ crypto, no gas, no seed phrase. And the <!-- br:models.free -->7<!-- /br:models.free --> free models need neither: no
981
981
  wallet, no key, no signup. See [Paying with a credit card](#paying-with-a-credit-card-api-key).
982
982
 
983
983
  ### Is ClawRouter free?
package/dist/cli.js CHANGED
@@ -32315,6 +32315,11 @@ function getSharedSpendControl() {
32315
32315
  host.__clawrouterSharedSpendControl ??= new SpendControl();
32316
32316
  return host.__clawrouterSharedSpendControl;
32317
32317
  }
32318
+ function parseQuotedAmountMicros(selected) {
32319
+ const raw = selected.amount ?? selected.maxAmountRequired;
32320
+ if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) return void 0;
32321
+ return BigInt(raw);
32322
+ }
32318
32323
  function parseQuotedAmountUsd(selected) {
32319
32324
  const raw = selected.amount ?? selected.maxAmountRequired;
32320
32325
  if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
@@ -33080,6 +33085,16 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
33080
33085
  return est === void 0 ? void 0 : Number(est);
33081
33086
  };
33082
33087
  const needMicros = estimateMicros();
33088
+ const notifyDeclinedPayment = () => {
33089
+ if (!options?.onPaymentDeclined) return;
33090
+ try {
33091
+ options.onPaymentDeclined();
33092
+ } catch (error) {
33093
+ console.error(
33094
+ `[ClawRouter] onPaymentDeclined callback failed: ${error instanceof Error ? error.message : String(error)}`
33095
+ );
33096
+ }
33097
+ };
33083
33098
  const cached = !options?.skipPreAuth ? cache2.get(cacheKey2) : void 0;
33084
33099
  const preAuthCovers = cached !== void 0 && Date.now() - cached.cachedAt < ttlMs && cached.coverMicros !== void 0 && needMicros !== void 0 && needMicros <= cached.coverMicros;
33085
33100
  if (preAuthCovers) {
@@ -33098,6 +33113,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
33098
33113
  return response2;
33099
33114
  }
33100
33115
  paymentInFlight = false;
33116
+ notifyDeclinedPayment();
33101
33117
  cache2.delete(cacheKey2);
33102
33118
  } catch (err) {
33103
33119
  cache2.delete(cacheKey2);
@@ -33142,6 +33158,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
33142
33158
  clonedRequest.headers.set(key3, value);
33143
33159
  }
33144
33160
  const paidResponse = await baseFetch(clonedRequest);
33161
+ if (paidResponse.status === 402) notifyDeclinedPayment();
33145
33162
  notifyAcceptedPayment(paidResponse, payload);
33146
33163
  return paidResponse;
33147
33164
  };
@@ -64789,6 +64806,132 @@ var init_api_key = __esm({
64789
64806
  }
64790
64807
  });
64791
64808
 
64809
+ // src/session-spend.ts
64810
+ function parseMicros(raw) {
64811
+ if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
64812
+ return BigInt(raw);
64813
+ }
64814
+ function usdToMicros(usd2) {
64815
+ return BigInt(Math.round(usd2 * 1e6));
64816
+ }
64817
+ function microsToUsd(micros) {
64818
+ return Number(micros) / 1e6;
64819
+ }
64820
+ var SessionSpend, CostCapError;
64821
+ var init_session_spend = __esm({
64822
+ "src/session-spend.ts"() {
64823
+ "use strict";
64824
+ init_spend_control();
64825
+ SessionSpend = class _SessionSpend {
64826
+ constructor(store, sessionId, capMicros, holdMicros) {
64827
+ this.store = store;
64828
+ this.sessionId = sessionId;
64829
+ this.capMicros = capMicros;
64830
+ this.heldMicros = holdMicros;
64831
+ if (holdMicros > 0n) store.adjustSessionHold(sessionId, holdMicros);
64832
+ }
64833
+ store;
64834
+ sessionId;
64835
+ capMicros;
64836
+ /** Budget this request still holds against the session (strict mode only). */
64837
+ heldMicros;
64838
+ /** Sum of payments signed by this request that still count. */
64839
+ signedMicros = 0n;
64840
+ /** The most recent signature not yet settled or voided — the one a decline undoes. */
64841
+ lastSignedMicros = 0n;
64842
+ released = false;
64843
+ /**
64844
+ * Strict mode: admit the request only if the session's spend, plus what its
64845
+ * other in-flight requests hold, plus this request's estimate fits the cap —
64846
+ * and hold the estimate in the same step.
64847
+ */
64848
+ static admit(store, sessionId, capMicros, estimateMicros) {
64849
+ const spentMicros = store.getSessionCostMicros(sessionId);
64850
+ const heldMicros = store.getSessionHoldMicros(sessionId);
64851
+ if (spentMicros + heldMicros + estimateMicros > capMicros) {
64852
+ return { admitted: false, spentMicros, heldMicros, estimateMicros };
64853
+ }
64854
+ return {
64855
+ admitted: true,
64856
+ spend: new _SessionSpend(store, sessionId, capMicros, estimateMicros)
64857
+ };
64858
+ }
64859
+ /** Graceful mode: track signed spend, enforce nothing. */
64860
+ static track(store, sessionId) {
64861
+ return new _SessionSpend(store, sessionId, void 0, 0n);
64862
+ }
64863
+ get isStrict() {
64864
+ return this.capMicros !== void 0;
64865
+ }
64866
+ /**
64867
+ * Before a payment is created: may this request sign `quoteMicros`?
64868
+ * Spend already recorded (including this request's own earlier signatures)
64869
+ * plus what OTHER in-flight requests hold, plus the quote, must fit the cap.
64870
+ * Always true in graceful mode.
64871
+ */
64872
+ coverPayment(quoteMicros) {
64873
+ if (this.capMicros === void 0) return true;
64874
+ const spent = this.store.getSessionCostMicros(this.sessionId);
64875
+ const heldByOthers = this.store.getSessionHoldMicros(this.sessionId) - this.heldMicros;
64876
+ if (spent + heldByOthers + quoteMicros > this.capMicros) return false;
64877
+ if (quoteMicros > this.heldMicros) {
64878
+ this.store.adjustSessionHold(this.sessionId, quoteMicros - this.heldMicros);
64879
+ this.heldMicros = quoteMicros;
64880
+ }
64881
+ return true;
64882
+ }
64883
+ /** A payment was signed: it is spend now, and replaces part of the hold. */
64884
+ recordSigned(amountMicros) {
64885
+ if (amountMicros <= 0n) return;
64886
+ this.signedMicros += amountMicros;
64887
+ this.lastSignedMicros = amountMicros;
64888
+ this.store.addSessionCost(this.sessionId, amountMicros);
64889
+ this.shrinkHold(amountMicros);
64890
+ }
64891
+ /**
64892
+ * The gateway answered the payment just signed with a 402: nothing settled.
64893
+ * Payments within one request are signed and sent one at a time, so the
64894
+ * decline always belongs to the latest signature. Undone at most once.
64895
+ */
64896
+ voidLastSigned() {
64897
+ const undo = this.lastSignedMicros;
64898
+ this.lastSignedMicros = 0n;
64899
+ if (undo <= 0n) return;
64900
+ this.signedMicros -= undo;
64901
+ this.store.addSessionCost(this.sessionId, -undo);
64902
+ }
64903
+ /**
64904
+ * An attempt succeeded. When nothing was signed for this request (the API
64905
+ * key rail), the local estimate is the only record of the spend. When a
64906
+ * payment was signed, it is already recorded at its real amount.
64907
+ */
64908
+ recordSuccess(estimateMicros) {
64909
+ if (this.signedMicros > 0n || estimateMicros <= 0n) return;
64910
+ this.store.addSessionCost(this.sessionId, estimateMicros);
64911
+ this.shrinkHold(estimateMicros);
64912
+ }
64913
+ /** The request is over: return whatever it still holds. Idempotent. */
64914
+ release() {
64915
+ if (this.released) return;
64916
+ this.released = true;
64917
+ this.shrinkHold(this.heldMicros);
64918
+ }
64919
+ shrinkHold(byMicros) {
64920
+ const drop = byMicros < this.heldMicros ? byMicros : this.heldMicros;
64921
+ if (drop <= 0n) return;
64922
+ this.heldMicros -= drop;
64923
+ this.store.adjustSessionHold(this.sessionId, -drop);
64924
+ }
64925
+ };
64926
+ CostCapError = class extends SpendPolicyError {
64927
+ constructor(reason) {
64928
+ super(reason);
64929
+ this.name = "CostCapError";
64930
+ }
64931
+ };
64932
+ }
64933
+ });
64934
+
64792
64935
  // src/compression/types.ts
64793
64936
  var DEFAULT_COMPRESSION_CONFIG;
64794
64937
  var init_types2 = __esm({
@@ -65636,6 +65779,9 @@ var init_session = __esm({
65636
65779
  };
65637
65780
  SessionStore = class {
65638
65781
  sessions = /* @__PURE__ */ new Map();
65782
+ // Kept apart from `sessions`: a hold belongs to requests still running, so it
65783
+ // must not vanish when the idle-session sweep drops an entry mid-request.
65784
+ sessionHolds = /* @__PURE__ */ new Map();
65639
65785
  config;
65640
65786
  cleanupInterval = null;
65641
65787
  constructor(config = {}) {
@@ -65811,14 +65957,34 @@ var init_session = __esm({
65811
65957
  this.sessions.set(sessionId, entry);
65812
65958
  }
65813
65959
  entry.sessionCostMicros += additionalMicros;
65960
+ if (entry.sessionCostMicros < 0n) entry.sessionCostMicros = 0n;
65814
65961
  }
65815
65962
  /**
65816
65963
  * Get the total accumulated cost for a session in USD.
65817
65964
  */
65818
65965
  getSessionCostUsd(sessionId) {
65819
- const entry = this.sessions.get(sessionId);
65820
- if (!entry) return 0;
65821
- return Number(entry.sessionCostMicros) / 1e6;
65966
+ return Number(this.getSessionCostMicros(sessionId)) / 1e6;
65967
+ }
65968
+ /**
65969
+ * Get the total accumulated cost for a session in USDC micros.
65970
+ */
65971
+ getSessionCostMicros(sessionId) {
65972
+ return this.sessions.get(sessionId)?.sessionCostMicros ?? 0n;
65973
+ }
65974
+ /**
65975
+ * Budget held by a session's in-flight requests (USDC micros) — admitted
65976
+ * against the strict cap but not yet spent. See session-spend.ts.
65977
+ */
65978
+ getSessionHoldMicros(sessionId) {
65979
+ return this.sessionHolds.get(sessionId) ?? 0n;
65980
+ }
65981
+ /**
65982
+ * Grow (positive) or release (negative) a session's in-flight hold.
65983
+ */
65984
+ adjustSessionHold(sessionId, deltaMicros) {
65985
+ const next = (this.sessionHolds.get(sessionId) ?? 0n) + deltaMicros;
65986
+ if (next > 0n) this.sessionHolds.set(sessionId, next);
65987
+ else this.sessionHolds.delete(sessionId);
65822
65988
  }
65823
65989
  /**
65824
65990
  * Stop the cleanup interval.
@@ -96521,6 +96687,19 @@ async function startProxy(options) {
96521
96687
  }
96522
96688
  const account = walletKey ? privateKeyToAccount(walletKey) : void 0;
96523
96689
  const x402 = authMode === "wallet" ? new x402Client() : void 0;
96690
+ x402?.onBeforePaymentCreation(async (context) => {
96691
+ const spend = paymentStore.getStore()?.sessionSpend;
96692
+ if (!spend?.isStrict) return;
96693
+ const quote = parseQuotedAmountMicros(context.selectedRequirements);
96694
+ if (quote === void 0) {
96695
+ throw new CostCapError("ClawRouter cost cap: unreadable payment amount in the 402 quote");
96696
+ }
96697
+ if (!spend.coverPayment(quote)) {
96698
+ throw new CostCapError(
96699
+ `ClawRouter cost cap exceeded: signing $${microsToUsd(quote).toFixed(6)} would push this session past its maxCostPerRun limit`
96700
+ );
96701
+ }
96702
+ });
96524
96703
  if (x402 && account) {
96525
96704
  const evmPublicClient = createPublicClient({ chain: base, transport: http() });
96526
96705
  const evmSigner = toClientEvmSigner(account, evmPublicClient);
@@ -96563,6 +96742,11 @@ async function startProxy(options) {
96563
96742
  if (store) store.amountUsd = amountUsd;
96564
96743
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
96565
96744
  });
96745
+ x402?.onAfterPaymentCreation(async (context) => {
96746
+ const spend = paymentStore.getStore()?.sessionSpend;
96747
+ const amount = parseQuotedAmountMicros(context.selectedRequirements);
96748
+ if (spend && amount !== void 0) spend.recordSigned(amount);
96749
+ });
96566
96750
  const payFetch = authMode === "api-key" ? withSpendPolicy(
96567
96751
  createApiKeyFetch(apiKey, fetch, apiBase),
96568
96752
  options.spendControl ?? getSharedSpendControl(),
@@ -96581,7 +96765,10 @@ async function startProxy(options) {
96581
96765
  // Only the wallet rail settles per call. The API-key rail is billed
96582
96766
  // server-side against account credit, so there is no per-request
96583
96767
  // settlement for an observer to report.
96584
- onPayment: options.onPayment
96768
+ onPayment: options.onPayment,
96769
+ // A 402 answer to a signed payment means the gateway declined it and
96770
+ // nothing settled — take it back off the session's budget.
96771
+ onPaymentDeclined: () => paymentStore.getStore()?.sessionSpend?.voidLastSigned()
96585
96772
  });
96586
96773
  let balanceMonitor;
96587
96774
  if (options._balanceMonitorOverride) {
@@ -97865,6 +98052,17 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
97865
98052
  return { success: true, response };
97866
98053
  } catch (err) {
97867
98054
  const errorMsg = err instanceof Error ? err.message : String(err);
98055
+ if (err instanceof CostCapError) {
98056
+ return {
98057
+ success: false,
98058
+ errorBody: JSON.stringify({
98059
+ error: { message: errorMsg, type: "cost_cap_exceeded", code: "cost_cap_exceeded" }
98060
+ }),
98061
+ errorStatus: 429,
98062
+ isProviderError: false,
98063
+ errorCategory: "payment_error"
98064
+ };
98065
+ }
97868
98066
  if (err instanceof SpendPolicyError) {
97869
98067
  return {
97870
98068
  success: false,
@@ -98944,14 +99142,23 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98944
99142
  }
98945
99143
  }
98946
99144
  }
99145
+ let sessionSpend;
98947
99146
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
98948
- const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
98949
99147
  const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
98950
- const thisReqEstUsd = thisReqEstStr ? Number(thisReqEstStr) / 1e6 : 0;
98951
- const projectedCostUsd = runCostUsd + thisReqEstUsd;
98952
- if (projectedCostUsd > options.maxCostPerRunUsd) {
99148
+ const admission = SessionSpend.admit(
99149
+ sessionStore,
99150
+ effectiveSessionId,
99151
+ usdToMicros(options.maxCostPerRunUsd),
99152
+ parseMicros(thisReqEstStr) ?? 0n
99153
+ );
99154
+ if (!admission.admitted) {
99155
+ const spentUsd = microsToUsd(admission.spentMicros);
99156
+ const heldUsd = microsToUsd(admission.heldMicros);
99157
+ const estUsd = microsToUsd(admission.estimateMicros);
99158
+ const projectedCostUsd = spentUsd + heldUsd + estUsd;
99159
+ const breakdown = `spent $${spentUsd.toFixed(4)}${heldUsd > 0 ? ` + in flight $${heldUsd.toFixed(4)}` : ""} + est $${estUsd.toFixed(4)}`;
98953
99160
  console.log(
98954
- `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) > $${options.maxCostPerRunUsd} limit`
99161
+ `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (${breakdown}) > $${options.maxCostPerRunUsd} limit`
98955
99162
  );
98956
99163
  res.writeHead(429, {
98957
99164
  "Content-Type": "application/json",
@@ -98960,7 +99167,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98960
99167
  res.end(
98961
99168
  JSON.stringify({
98962
99169
  error: {
98963
- message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) would exceed limit $${options.maxCostPerRunUsd}`,
99170
+ message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (${breakdown}) would exceed limit $${options.maxCostPerRunUsd}`,
98964
99171
  type: "cost_cap_exceeded",
98965
99172
  code: "cost_cap_exceeded"
98966
99173
  }
@@ -98969,7 +99176,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98969
99176
  deduplicator.removeInflight(dedupKey);
98970
99177
  return;
98971
99178
  }
99179
+ sessionSpend = admission.spend;
99180
+ } else if (options.maxCostPerRunUsd && effectiveSessionId) {
99181
+ sessionSpend = SessionSpend.track(sessionStore, effectiveSessionId);
98972
99182
  }
99183
+ const requestStore = paymentStore.getStore();
99184
+ if (requestStore) requestStore.sessionSpend = sessionSpend;
98973
99185
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
98974
99186
  const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
98975
99187
  const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
@@ -99365,11 +99577,9 @@ data: [DONE]
99365
99577
  actualModelUsed = tryModel;
99366
99578
  recordOutcome(tryModel, requestKind, true);
99367
99579
  console.log(`[ClawRouter] Success with model: ${tryModel}`);
99368
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99369
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99370
- if (costEst) {
99371
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99372
- }
99580
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99581
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99582
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99373
99583
  }
99374
99584
  break;
99375
99585
  }
@@ -99443,11 +99653,9 @@ data: [DONE]
99443
99653
  upstream = retryResult.response;
99444
99654
  actualModelUsed = tryModel;
99445
99655
  console.log(`[ClawRouter] Explicit-pin retry succeeded for: ${tryModel}`);
99446
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99447
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99448
- if (costEst) {
99449
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99450
- }
99656
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99657
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99658
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99451
99659
  }
99452
99660
  break;
99453
99661
  }
@@ -99514,11 +99722,9 @@ data: [DONE]
99514
99722
  upstream = retryResult.response;
99515
99723
  actualModelUsed = tryModel;
99516
99724
  console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
99517
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99518
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99519
- if (costEst) {
99520
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99521
- }
99725
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99726
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99727
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99522
99728
  }
99523
99729
  break;
99524
99730
  }
@@ -99612,6 +99818,7 @@ data: [DONE]
99612
99818
  );
99613
99819
  const rawErrBody = lastError?.body || structuredMessage;
99614
99820
  const errStatus = lastError?.status || 502;
99821
+ const costCapRefusal = errStatus === 429 && rawErrBody.includes('"cost_cap_exceeded"');
99615
99822
  const transformedErr = transformPaymentError(rawErrBody);
99616
99823
  requestHadError = true;
99617
99824
  if (headersSentEarly) {
@@ -99648,7 +99855,8 @@ data: [DONE]
99648
99855
  "Content-Type": "application/json",
99649
99856
  "x-context-used-kb": String(originalContextSizeKB),
99650
99857
  "x-context-limit-kb": String(CONTEXT_LIMIT_KB),
99651
- ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
99858
+ ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {},
99859
+ ...costCapRefusal ? { "X-ClawRouter-Cost-Cap-Exceeded": "1" } : {}
99652
99860
  });
99653
99861
  res.end(transformedErr);
99654
99862
  deduplicator.complete(dedupKey, {
@@ -100082,6 +100290,8 @@ data: [DONE]
100082
100290
  throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
100083
100291
  }
100084
100292
  throw err;
100293
+ } finally {
100294
+ sessionSpend?.release();
100085
100295
  }
100086
100296
  const logModel = routingDecision?.model ?? modelId;
100087
100297
  if (logModel) {
@@ -100168,6 +100378,7 @@ var init_proxy = __esm({
100168
100378
  init_auth();
100169
100379
  init_api_key();
100170
100380
  init_spend_control();
100381
+ init_session_spend();
100171
100382
  init_compression();
100172
100383
  init_errors5();
100173
100384
  init_version4();
@@ -123261,7 +123472,7 @@ var init_client5 = __esm({
123261
123472
  function resolveSpendControl(deps) {
123262
123473
  return deps?.spendControl ?? getSharedSpendControl();
123263
123474
  }
123264
- function usdToMicros(usd2) {
123475
+ function usdToMicros2(usd2) {
123265
123476
  return String(Math.ceil(usd2 * 1e6));
123266
123477
  }
123267
123478
  async function signUnderSpendPolicy(deps, quote, action, sign4) {
@@ -123527,7 +123738,7 @@ Re-call with confirm:true to sign and submit.`,
123527
123738
  payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
123528
123739
  network: `eip155:${POLYGON_CHAIN_ID}`,
123529
123740
  asset: PUSD_COLLATERAL,
123530
- amount: usdToMicros(notional)
123741
+ amount: usdToMicros2(notional)
123531
123742
  };
123532
123743
  let r2;
123533
123744
  try {
package/dist/index.d.ts CHANGED
@@ -966,6 +966,7 @@ declare const DEFAULT_SESSION_CONFIG: SessionConfig;
966
966
  */
967
967
  declare class SessionStore {
968
968
  private sessions;
969
+ private sessionHolds;
969
970
  private config;
970
971
  private cleanupInterval;
971
972
  constructor(config?: Partial<SessionConfig>);
@@ -1036,6 +1037,19 @@ declare class SessionStore {
1036
1037
  * Get the total accumulated cost for a session in USD.
1037
1038
  */
1038
1039
  getSessionCostUsd(sessionId: string): number;
1040
+ /**
1041
+ * Get the total accumulated cost for a session in USDC micros.
1042
+ */
1043
+ getSessionCostMicros(sessionId: string): bigint;
1044
+ /**
1045
+ * Budget held by a session's in-flight requests (USDC micros) — admitted
1046
+ * against the strict cap but not yet spent. See session-spend.ts.
1047
+ */
1048
+ getSessionHoldMicros(sessionId: string): bigint;
1049
+ /**
1050
+ * Grow (positive) or release (negative) a session's in-flight hold.
1051
+ */
1052
+ adjustSessionHold(sessionId: string, deltaMicros: bigint): void;
1039
1053
  /**
1040
1054
  * Stop the cleanup interval.
1041
1055
  */
package/dist/index.js CHANGED
@@ -34678,6 +34678,11 @@ function getSharedSpendControl() {
34678
34678
  host.__clawrouterSharedSpendControl ??= new SpendControl();
34679
34679
  return host.__clawrouterSharedSpendControl;
34680
34680
  }
34681
+ function parseQuotedAmountMicros(selected) {
34682
+ const raw = selected.amount ?? selected.maxAmountRequired;
34683
+ if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) return void 0;
34684
+ return BigInt(raw);
34685
+ }
34681
34686
  function parseQuotedAmountUsd(selected) {
34682
34687
  const raw = selected.amount ?? selected.maxAmountRequired;
34683
34688
  if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
@@ -35443,6 +35448,16 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35443
35448
  return est === void 0 ? void 0 : Number(est);
35444
35449
  };
35445
35450
  const needMicros = estimateMicros();
35451
+ const notifyDeclinedPayment = () => {
35452
+ if (!options?.onPaymentDeclined) return;
35453
+ try {
35454
+ options.onPaymentDeclined();
35455
+ } catch (error) {
35456
+ console.error(
35457
+ `[ClawRouter] onPaymentDeclined callback failed: ${error instanceof Error ? error.message : String(error)}`
35458
+ );
35459
+ }
35460
+ };
35446
35461
  const cached = !options?.skipPreAuth ? cache2.get(cacheKey2) : void 0;
35447
35462
  const preAuthCovers = cached !== void 0 && Date.now() - cached.cachedAt < ttlMs && cached.coverMicros !== void 0 && needMicros !== void 0 && needMicros <= cached.coverMicros;
35448
35463
  if (preAuthCovers) {
@@ -35461,6 +35476,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35461
35476
  return response2;
35462
35477
  }
35463
35478
  paymentInFlight = false;
35479
+ notifyDeclinedPayment();
35464
35480
  cache2.delete(cacheKey2);
35465
35481
  } catch (err) {
35466
35482
  cache2.delete(cacheKey2);
@@ -35505,6 +35521,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35505
35521
  clonedRequest.headers.set(key3, value);
35506
35522
  }
35507
35523
  const paidResponse = await baseFetch(clonedRequest);
35524
+ if (paidResponse.status === 402) notifyDeclinedPayment();
35508
35525
  notifyAcceptedPayment(paidResponse, payload);
35509
35526
  return paidResponse;
35510
35527
  };
@@ -64542,6 +64559,132 @@ var init_api_key = __esm({
64542
64559
  }
64543
64560
  });
64544
64561
 
64562
+ // src/session-spend.ts
64563
+ function parseMicros(raw) {
64564
+ if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
64565
+ return BigInt(raw);
64566
+ }
64567
+ function usdToMicros(usd) {
64568
+ return BigInt(Math.round(usd * 1e6));
64569
+ }
64570
+ function microsToUsd(micros) {
64571
+ return Number(micros) / 1e6;
64572
+ }
64573
+ var SessionSpend, CostCapError;
64574
+ var init_session_spend = __esm({
64575
+ "src/session-spend.ts"() {
64576
+ "use strict";
64577
+ init_spend_control();
64578
+ SessionSpend = class _SessionSpend {
64579
+ constructor(store, sessionId, capMicros, holdMicros) {
64580
+ this.store = store;
64581
+ this.sessionId = sessionId;
64582
+ this.capMicros = capMicros;
64583
+ this.heldMicros = holdMicros;
64584
+ if (holdMicros > 0n) store.adjustSessionHold(sessionId, holdMicros);
64585
+ }
64586
+ store;
64587
+ sessionId;
64588
+ capMicros;
64589
+ /** Budget this request still holds against the session (strict mode only). */
64590
+ heldMicros;
64591
+ /** Sum of payments signed by this request that still count. */
64592
+ signedMicros = 0n;
64593
+ /** The most recent signature not yet settled or voided — the one a decline undoes. */
64594
+ lastSignedMicros = 0n;
64595
+ released = false;
64596
+ /**
64597
+ * Strict mode: admit the request only if the session's spend, plus what its
64598
+ * other in-flight requests hold, plus this request's estimate fits the cap —
64599
+ * and hold the estimate in the same step.
64600
+ */
64601
+ static admit(store, sessionId, capMicros, estimateMicros) {
64602
+ const spentMicros = store.getSessionCostMicros(sessionId);
64603
+ const heldMicros = store.getSessionHoldMicros(sessionId);
64604
+ if (spentMicros + heldMicros + estimateMicros > capMicros) {
64605
+ return { admitted: false, spentMicros, heldMicros, estimateMicros };
64606
+ }
64607
+ return {
64608
+ admitted: true,
64609
+ spend: new _SessionSpend(store, sessionId, capMicros, estimateMicros)
64610
+ };
64611
+ }
64612
+ /** Graceful mode: track signed spend, enforce nothing. */
64613
+ static track(store, sessionId) {
64614
+ return new _SessionSpend(store, sessionId, void 0, 0n);
64615
+ }
64616
+ get isStrict() {
64617
+ return this.capMicros !== void 0;
64618
+ }
64619
+ /**
64620
+ * Before a payment is created: may this request sign `quoteMicros`?
64621
+ * Spend already recorded (including this request's own earlier signatures)
64622
+ * plus what OTHER in-flight requests hold, plus the quote, must fit the cap.
64623
+ * Always true in graceful mode.
64624
+ */
64625
+ coverPayment(quoteMicros) {
64626
+ if (this.capMicros === void 0) return true;
64627
+ const spent = this.store.getSessionCostMicros(this.sessionId);
64628
+ const heldByOthers = this.store.getSessionHoldMicros(this.sessionId) - this.heldMicros;
64629
+ if (spent + heldByOthers + quoteMicros > this.capMicros) return false;
64630
+ if (quoteMicros > this.heldMicros) {
64631
+ this.store.adjustSessionHold(this.sessionId, quoteMicros - this.heldMicros);
64632
+ this.heldMicros = quoteMicros;
64633
+ }
64634
+ return true;
64635
+ }
64636
+ /** A payment was signed: it is spend now, and replaces part of the hold. */
64637
+ recordSigned(amountMicros) {
64638
+ if (amountMicros <= 0n) return;
64639
+ this.signedMicros += amountMicros;
64640
+ this.lastSignedMicros = amountMicros;
64641
+ this.store.addSessionCost(this.sessionId, amountMicros);
64642
+ this.shrinkHold(amountMicros);
64643
+ }
64644
+ /**
64645
+ * The gateway answered the payment just signed with a 402: nothing settled.
64646
+ * Payments within one request are signed and sent one at a time, so the
64647
+ * decline always belongs to the latest signature. Undone at most once.
64648
+ */
64649
+ voidLastSigned() {
64650
+ const undo = this.lastSignedMicros;
64651
+ this.lastSignedMicros = 0n;
64652
+ if (undo <= 0n) return;
64653
+ this.signedMicros -= undo;
64654
+ this.store.addSessionCost(this.sessionId, -undo);
64655
+ }
64656
+ /**
64657
+ * An attempt succeeded. When nothing was signed for this request (the API
64658
+ * key rail), the local estimate is the only record of the spend. When a
64659
+ * payment was signed, it is already recorded at its real amount.
64660
+ */
64661
+ recordSuccess(estimateMicros) {
64662
+ if (this.signedMicros > 0n || estimateMicros <= 0n) return;
64663
+ this.store.addSessionCost(this.sessionId, estimateMicros);
64664
+ this.shrinkHold(estimateMicros);
64665
+ }
64666
+ /** The request is over: return whatever it still holds. Idempotent. */
64667
+ release() {
64668
+ if (this.released) return;
64669
+ this.released = true;
64670
+ this.shrinkHold(this.heldMicros);
64671
+ }
64672
+ shrinkHold(byMicros) {
64673
+ const drop = byMicros < this.heldMicros ? byMicros : this.heldMicros;
64674
+ if (drop <= 0n) return;
64675
+ this.heldMicros -= drop;
64676
+ this.store.adjustSessionHold(this.sessionId, -drop);
64677
+ }
64678
+ };
64679
+ CostCapError = class extends SpendPolicyError {
64680
+ constructor(reason) {
64681
+ super(reason);
64682
+ this.name = "CostCapError";
64683
+ }
64684
+ };
64685
+ }
64686
+ });
64687
+
64545
64688
  // src/compression/types.ts
64546
64689
  var DEFAULT_COMPRESSION_CONFIG;
64547
64690
  var init_types2 = __esm({
@@ -65389,6 +65532,9 @@ var init_session = __esm({
65389
65532
  };
65390
65533
  SessionStore = class {
65391
65534
  sessions = /* @__PURE__ */ new Map();
65535
+ // Kept apart from `sessions`: a hold belongs to requests still running, so it
65536
+ // must not vanish when the idle-session sweep drops an entry mid-request.
65537
+ sessionHolds = /* @__PURE__ */ new Map();
65392
65538
  config;
65393
65539
  cleanupInterval = null;
65394
65540
  constructor(config = {}) {
@@ -65564,14 +65710,34 @@ var init_session = __esm({
65564
65710
  this.sessions.set(sessionId, entry);
65565
65711
  }
65566
65712
  entry.sessionCostMicros += additionalMicros;
65713
+ if (entry.sessionCostMicros < 0n) entry.sessionCostMicros = 0n;
65567
65714
  }
65568
65715
  /**
65569
65716
  * Get the total accumulated cost for a session in USD.
65570
65717
  */
65571
65718
  getSessionCostUsd(sessionId) {
65572
- const entry = this.sessions.get(sessionId);
65573
- if (!entry) return 0;
65574
- return Number(entry.sessionCostMicros) / 1e6;
65719
+ return Number(this.getSessionCostMicros(sessionId)) / 1e6;
65720
+ }
65721
+ /**
65722
+ * Get the total accumulated cost for a session in USDC micros.
65723
+ */
65724
+ getSessionCostMicros(sessionId) {
65725
+ return this.sessions.get(sessionId)?.sessionCostMicros ?? 0n;
65726
+ }
65727
+ /**
65728
+ * Budget held by a session's in-flight requests (USDC micros) — admitted
65729
+ * against the strict cap but not yet spent. See session-spend.ts.
65730
+ */
65731
+ getSessionHoldMicros(sessionId) {
65732
+ return this.sessionHolds.get(sessionId) ?? 0n;
65733
+ }
65734
+ /**
65735
+ * Grow (positive) or release (negative) a session's in-flight hold.
65736
+ */
65737
+ adjustSessionHold(sessionId, deltaMicros) {
65738
+ const next = (this.sessionHolds.get(sessionId) ?? 0n) + deltaMicros;
65739
+ if (next > 0n) this.sessionHolds.set(sessionId, next);
65740
+ else this.sessionHolds.delete(sessionId);
65575
65741
  }
65576
65742
  /**
65577
65743
  * Stop the cleanup interval.
@@ -96274,6 +96440,19 @@ async function startProxy(options) {
96274
96440
  }
96275
96441
  const account = walletKey ? privateKeyToAccount(walletKey) : void 0;
96276
96442
  const x402 = authMode === "wallet" ? new x402Client() : void 0;
96443
+ x402?.onBeforePaymentCreation(async (context) => {
96444
+ const spend = paymentStore.getStore()?.sessionSpend;
96445
+ if (!spend?.isStrict) return;
96446
+ const quote = parseQuotedAmountMicros(context.selectedRequirements);
96447
+ if (quote === void 0) {
96448
+ throw new CostCapError("ClawRouter cost cap: unreadable payment amount in the 402 quote");
96449
+ }
96450
+ if (!spend.coverPayment(quote)) {
96451
+ throw new CostCapError(
96452
+ `ClawRouter cost cap exceeded: signing $${microsToUsd(quote).toFixed(6)} would push this session past its maxCostPerRun limit`
96453
+ );
96454
+ }
96455
+ });
96277
96456
  if (x402 && account) {
96278
96457
  const evmPublicClient = createPublicClient({ chain: base, transport: http() });
96279
96458
  const evmSigner = toClientEvmSigner(account, evmPublicClient);
@@ -96316,6 +96495,11 @@ async function startProxy(options) {
96316
96495
  if (store) store.amountUsd = amountUsd;
96317
96496
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
96318
96497
  });
96498
+ x402?.onAfterPaymentCreation(async (context) => {
96499
+ const spend = paymentStore.getStore()?.sessionSpend;
96500
+ const amount = parseQuotedAmountMicros(context.selectedRequirements);
96501
+ if (spend && amount !== void 0) spend.recordSigned(amount);
96502
+ });
96319
96503
  const payFetch = authMode === "api-key" ? withSpendPolicy(
96320
96504
  createApiKeyFetch(apiKey, fetch, apiBase),
96321
96505
  options.spendControl ?? getSharedSpendControl(),
@@ -96334,7 +96518,10 @@ async function startProxy(options) {
96334
96518
  // Only the wallet rail settles per call. The API-key rail is billed
96335
96519
  // server-side against account credit, so there is no per-request
96336
96520
  // settlement for an observer to report.
96337
- onPayment: options.onPayment
96521
+ onPayment: options.onPayment,
96522
+ // A 402 answer to a signed payment means the gateway declined it and
96523
+ // nothing settled — take it back off the session's budget.
96524
+ onPaymentDeclined: () => paymentStore.getStore()?.sessionSpend?.voidLastSigned()
96338
96525
  });
96339
96526
  let balanceMonitor;
96340
96527
  if (options._balanceMonitorOverride) {
@@ -97618,6 +97805,17 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
97618
97805
  return { success: true, response };
97619
97806
  } catch (err) {
97620
97807
  const errorMsg = err instanceof Error ? err.message : String(err);
97808
+ if (err instanceof CostCapError) {
97809
+ return {
97810
+ success: false,
97811
+ errorBody: JSON.stringify({
97812
+ error: { message: errorMsg, type: "cost_cap_exceeded", code: "cost_cap_exceeded" }
97813
+ }),
97814
+ errorStatus: 429,
97815
+ isProviderError: false,
97816
+ errorCategory: "payment_error"
97817
+ };
97818
+ }
97621
97819
  if (err instanceof SpendPolicyError) {
97622
97820
  return {
97623
97821
  success: false,
@@ -98697,14 +98895,23 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98697
98895
  }
98698
98896
  }
98699
98897
  }
98898
+ let sessionSpend;
98700
98899
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
98701
- const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
98702
98900
  const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
98703
- const thisReqEstUsd = thisReqEstStr ? Number(thisReqEstStr) / 1e6 : 0;
98704
- const projectedCostUsd = runCostUsd + thisReqEstUsd;
98705
- if (projectedCostUsd > options.maxCostPerRunUsd) {
98901
+ const admission = SessionSpend.admit(
98902
+ sessionStore,
98903
+ effectiveSessionId,
98904
+ usdToMicros(options.maxCostPerRunUsd),
98905
+ parseMicros(thisReqEstStr) ?? 0n
98906
+ );
98907
+ if (!admission.admitted) {
98908
+ const spentUsd = microsToUsd(admission.spentMicros);
98909
+ const heldUsd = microsToUsd(admission.heldMicros);
98910
+ const estUsd = microsToUsd(admission.estimateMicros);
98911
+ const projectedCostUsd = spentUsd + heldUsd + estUsd;
98912
+ const breakdown = `spent $${spentUsd.toFixed(4)}${heldUsd > 0 ? ` + in flight $${heldUsd.toFixed(4)}` : ""} + est $${estUsd.toFixed(4)}`;
98706
98913
  console.log(
98707
- `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) > $${options.maxCostPerRunUsd} limit`
98914
+ `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (${breakdown}) > $${options.maxCostPerRunUsd} limit`
98708
98915
  );
98709
98916
  res.writeHead(429, {
98710
98917
  "Content-Type": "application/json",
@@ -98713,7 +98920,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98713
98920
  res.end(
98714
98921
  JSON.stringify({
98715
98922
  error: {
98716
- message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) would exceed limit $${options.maxCostPerRunUsd}`,
98923
+ message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (${breakdown}) would exceed limit $${options.maxCostPerRunUsd}`,
98717
98924
  type: "cost_cap_exceeded",
98718
98925
  code: "cost_cap_exceeded"
98719
98926
  }
@@ -98722,7 +98929,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98722
98929
  deduplicator.removeInflight(dedupKey);
98723
98930
  return;
98724
98931
  }
98932
+ sessionSpend = admission.spend;
98933
+ } else if (options.maxCostPerRunUsd && effectiveSessionId) {
98934
+ sessionSpend = SessionSpend.track(sessionStore, effectiveSessionId);
98725
98935
  }
98936
+ const requestStore = paymentStore.getStore();
98937
+ if (requestStore) requestStore.sessionSpend = sessionSpend;
98726
98938
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
98727
98939
  const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
98728
98940
  const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
@@ -99118,11 +99330,9 @@ data: [DONE]
99118
99330
  actualModelUsed = tryModel;
99119
99331
  recordOutcome(tryModel, requestKind, true);
99120
99332
  console.log(`[ClawRouter] Success with model: ${tryModel}`);
99121
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99122
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99123
- if (costEst) {
99124
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99125
- }
99333
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99334
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99335
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99126
99336
  }
99127
99337
  break;
99128
99338
  }
@@ -99196,11 +99406,9 @@ data: [DONE]
99196
99406
  upstream = retryResult.response;
99197
99407
  actualModelUsed = tryModel;
99198
99408
  console.log(`[ClawRouter] Explicit-pin retry succeeded for: ${tryModel}`);
99199
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99200
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99201
- if (costEst) {
99202
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99203
- }
99409
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99410
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99411
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99204
99412
  }
99205
99413
  break;
99206
99414
  }
@@ -99267,11 +99475,9 @@ data: [DONE]
99267
99475
  upstream = retryResult.response;
99268
99476
  actualModelUsed = tryModel;
99269
99477
  console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
99270
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99271
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99272
- if (costEst) {
99273
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99274
- }
99478
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99479
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99480
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99275
99481
  }
99276
99482
  break;
99277
99483
  }
@@ -99365,6 +99571,7 @@ data: [DONE]
99365
99571
  );
99366
99572
  const rawErrBody = lastError?.body || structuredMessage;
99367
99573
  const errStatus = lastError?.status || 502;
99574
+ const costCapRefusal = errStatus === 429 && rawErrBody.includes('"cost_cap_exceeded"');
99368
99575
  const transformedErr = transformPaymentError(rawErrBody);
99369
99576
  requestHadError = true;
99370
99577
  if (headersSentEarly) {
@@ -99401,7 +99608,8 @@ data: [DONE]
99401
99608
  "Content-Type": "application/json",
99402
99609
  "x-context-used-kb": String(originalContextSizeKB),
99403
99610
  "x-context-limit-kb": String(CONTEXT_LIMIT_KB),
99404
- ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
99611
+ ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {},
99612
+ ...costCapRefusal ? { "X-ClawRouter-Cost-Cap-Exceeded": "1" } : {}
99405
99613
  });
99406
99614
  res.end(transformedErr);
99407
99615
  deduplicator.complete(dedupKey, {
@@ -99835,6 +100043,8 @@ data: [DONE]
99835
100043
  throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
99836
100044
  }
99837
100045
  throw err;
100046
+ } finally {
100047
+ sessionSpend?.release();
99838
100048
  }
99839
100049
  const logModel = routingDecision?.model ?? modelId;
99840
100050
  if (logModel) {
@@ -99921,6 +100131,7 @@ var init_proxy = __esm({
99921
100131
  init_auth();
99922
100132
  init_api_key();
99923
100133
  init_spend_control();
100134
+ init_session_spend();
99924
100135
  init_compression();
99925
100136
  init_errors5();
99926
100137
  init_version4();
@@ -122622,7 +122833,7 @@ var init_client5 = __esm({
122622
122833
  function resolveSpendControl(deps) {
122623
122834
  return deps?.spendControl ?? getSharedSpendControl();
122624
122835
  }
122625
- function usdToMicros(usd) {
122836
+ function usdToMicros2(usd) {
122626
122837
  return String(Math.ceil(usd * 1e6));
122627
122838
  }
122628
122839
  async function signUnderSpendPolicy(deps, quote, action, sign4) {
@@ -122888,7 +123099,7 @@ Re-call with confirm:true to sign and submit.`,
122888
123099
  payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
122889
123100
  network: `eip155:${POLYGON_CHAIN_ID}`,
122890
123101
  asset: PUSD_COLLATERAL,
122891
- amount: usdToMicros(notional)
123102
+ amount: usdToMicros2(notional)
122892
123103
  };
122893
123104
  let r2;
122894
123105
  try {
@@ -1,7 +1,7 @@
1
1
  # 9 Free AI Models, Zero Cost: How BlockRun Gives Developers Top-Tier LLMs for Nothing
2
2
 
3
3
  > **The count in this title is a snapshot, and so is every model name below.**
4
- > It was 9 when this was written; the published free tier is <!-- br:models.free -->6<!-- /br:models.free --> today, and _none_ of the
4
+ > It was 9 when this was written; the published free tier is <!-- br:models.free -->7<!-- /br:models.free --> today, and _none_ of the
5
5
  > nine originals is still in it. Free hosting is volatile — NVIDIA retired four
6
6
  > of the five visible free models in a single sweep on 2026-08-30 — so the tier
7
7
  > gets rebuilt rather than topped up. The URL keeps its original slug so existing
@@ -23,7 +23,7 @@ BlockRun's answer: just take them.
23
23
 
24
24
  ---
25
25
 
26
- ## The Lineup: <!-- br:models.free -->6<!-- /br:models.free --> Models, $0.00
26
+ ## The Lineup: <!-- br:models.free -->7<!-- /br:models.free --> Models, $0.00
27
27
 
28
28
  Through [ClawRouter](https://github.com/BlockRunAI/ClawRouter) — BlockRun's local AI routing proxy — you get zero-cost access to the following (checked against the live catalog 2026-09-29):
29
29
 
@@ -236,13 +236,13 @@ Look at the cost trajectory over the past three years:
236
236
  - **2023:** GPT-4 dominates alone at $30/$60 per M tokens
237
237
  - **2024:** Open-source models surge, prices halve repeatedly
238
238
  - **2025:** DeepSeek, Qwen push top-tier inference below $1/M
239
- - **2026:** BlockRun offers <!-- br:models.free -->6<!-- /br:models.free --> free models through a single API
239
+ - **2026:** BlockRun offers <!-- br:models.free -->7<!-- /br:models.free --> free models through a single API
240
240
 
241
241
  **A free tier that gets rebuilt rather than retired isn't just a product feature — it's a signal.** Baseline AI capability is becoming infrastructure. Like internet bandwidth before it, the cost of "good enough" AI inference is converging toward zero.
242
242
 
243
243
  BlockRun and ClawRouter exist to be the **routing layer** in this transition: not locked to any single provider, not bound to any single model, always giving developers the lowest-cost path to the right capability.
244
244
 
245
- Today it's <!-- br:models.free -->6<!-- /br:models.free --> free models, and they are not the same seven as last month. Tomorrow it could be 50. Prices will only drop. Capabilities will only improve. The names will keep churning.
245
+ Today it's <!-- br:models.free -->7<!-- /br:models.free --> free models, and they are not the same seven as last month. Tomorrow it could be 50. Prices will only drop. Capabilities will only improve. The names will keep churning.
246
246
 
247
247
  **The one constant: your code doesn't need to change.**
248
248
 
@@ -257,7 +257,7 @@ clawrouter start
257
257
 
258
258
  Point your `base_url` to `http://localhost:8402/v1`. That's the whole setup.
259
259
 
260
- <!-- br:models.free -->6<!-- /br:models.free --> free models. Up to 1M context. Unlimited calls. Zero cost.
260
+ <!-- br:models.free -->7<!-- /br:models.free --> free models. Up to 1M context. Unlimited calls. Zero cost.
261
261
 
262
262
  Go build something.
263
263
 
@@ -50,7 +50,7 @@ This is where you're paying for real value:
50
50
 
51
51
  ## The Solution: ClawRouter
52
52
 
53
- [ClawRouter](https://github.com/BlockRunAI/ClawRouter) is an open-source local proxy that sits between your app and <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> AI models. It saves you money in three ways: **smart routing**, **token optimization**, and **response caching**.
53
+ [ClawRouter](https://github.com/BlockRunAI/ClawRouter) is an open-source local proxy that sits between your app and <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> AI models. It saves you money in three ways: **smart routing**, **token optimization**, and **response caching**.
54
54
 
55
55
  ```
56
56
  ┌─────────────┐ ┌──────────────────────────────┐ ┌──────────────────┐
@@ -31,7 +31,7 @@ This is exactly the problem [ClawRouter](https://github.com/BlockRunAI/ClawRoute
31
31
 
32
32
  ## ClawRouter: Smart Routing for Agents
33
33
 
34
- ClawRouter is an open-source local proxy that sits between your agent and <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> LLM models across 9 providers. It analyzes every request across 15 dimensions and routes it to the cheapest model that can handle it — in under 1ms, entirely locally.
34
+ ClawRouter is an open-source local proxy that sits between your agent and <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> LLM models across 9 providers. It analyzes every request across 15 dimensions and routes it to the cheapest model that can handle it — in under 1ms, entirely locally.
35
35
 
36
36
  ```
37
37
  Your Agent → ClawRouter (localhost:8402) → Best model for the job
@@ -74,12 +74,12 @@ Then point your agent at `http://localhost:8402/v1/` with any OpenAI-compatible
74
74
 
75
75
  - **No API keys to manage** — wallet-based cryptographic auth
76
76
  - **No subscriptions** — pay per request in USDC (Base or Solana)
77
- - **No vendor lock-in** — <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models, switch anytime
77
+ - **No vendor lock-in** — <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, switch anytime
78
78
  - **You control your wallet** — non-custodial, funds never held by a third party
79
79
  - **Budget caps** — set a max spend per session, ClawRouter gracefully downgrades when budget runs low
80
80
  - **Token compression** — 7-layer pipeline reduces token costs by 7–40% before they hit any provider
81
81
  - **Response caching** — identical requests within 10 minutes cost $0
82
- - **<!-- br:models.free -->6<!-- /br:models.free --> free models** — for tasks that don't need a paid model at all
82
+ - **<!-- br:models.free -->7<!-- /br:models.free --> free models** — for tasks that don't need a paid model at all
83
83
 
84
84
  ### Routing Profiles
85
85
 
@@ -222,7 +222,7 @@ OpenRouter doesn't always pass through provider-specific features correctly. Ima
222
222
 
223
223
  **Direct provider routing.** ClawRouter routes through BlockRun's API directly to providers — not through a second aggregator. One hop, not two. Provider-specific features work because there's no middleman translating them.
224
224
 
225
- ![Guaranteed Feature Parity & Direct Connectivity — Three-panel diagram: Vision (image_url auto-detected → vision-capable models only), Tool Calling (toolCalling flag → agentic models only), Catalog (curated <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models with automatic legacy-to-modern redirects). Direct provider routing means no dropped payloads.](./assets/clawrouter-feature-parity-direct-connectivity.png)
225
+ ![Guaranteed Feature Parity & Direct Connectivity — Three-panel diagram: Vision (image_url auto-detected → vision-capable models only), Tool Calling (toolCalling flag → agentic models only), Catalog (curated <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models with automatic legacy-to-modern redirects). Direct provider routing means no dropped payloads.](./assets/clawrouter-feature-parity-direct-connectivity.png)
226
226
 
227
227
  ---
228
228
 
@@ -240,7 +240,7 @@ When new models launch, OpenRouter's catalog lags. Users configure a model that
240
240
 
241
241
  ### How ClawRouter Solves This
242
242
 
243
- ClawRouter maintains a curated catalog of <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models across 9 providers (including <!-- br:models.free -->6<!-- /br:models.free --> free models), updated with each release. Delisted models have automatic redirect aliases:
243
+ ClawRouter maintains a curated catalog of <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models across 9 providers (including <!-- br:models.free -->7<!-- /br:models.free --> free models), updated with each release. Delisted models have automatic redirect aliases:
244
244
 
245
245
  ```typescript
246
246
  // Delisted models redirect automatically
@@ -268,7 +268,7 @@ No silent drops. No stale catalog. Models are benchmarked for speed, quality, an
268
268
  | **Rate limits** | Per-key, shared | Per-wallet, independent |
269
269
  | **Vision support** | Images sometimes dropped | Auto-detected, vision-only fallback |
270
270
  | **Tool calling** | Silent failures with some models | Flag-based filtering, guaranteed support |
271
- | **Model catalog** | Laggy, silent drops | Curated <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models, redirect aliases |
271
+ | **Model catalog** | Laggy, silent drops | Curated <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, redirect aliases |
272
272
  | **Budget control** | Monthly invoice | Per-session cap (`maxCostPerRun`) |
273
273
  | **Setup** | Create account, paste key | Agent generates wallet, auto-configured |
274
274
  | **Average cost** | $25/M tokens (Opus direct) | auto-routed = **<!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% savings** |
@@ -489,9 +489,15 @@ plugins:
489
489
  # graceful (default): when budget runs low, ClawRouter automatically downgrades
490
490
  # to cheaper models (premium → auto → eco → free). Tasks keep running.
491
491
  # Only returns an error if no model can serve the request at all.
492
+ # Spend is the amount each signed payment authorized, counted when it is
493
+ # signed — a payment whose response is lost still counts.
492
494
  #
493
495
  # strict: immediately returns 429 (X-ClawRouter-Cost-Cap-Exceeded: 1) once
494
496
  # the session spend reaches the cap. Use when you need a hard budget ceiling.
497
+ # Requests in flight hold their estimated cost, so concurrent calls cannot
498
+ # all pass on the same spent total; every signed payment counts at once,
499
+ # even one whose response is lost; and each payment (including retries and
500
+ # fallbacks) is checked against the cap before it is signed.
495
501
  maxCostPerRunMode: graceful # or: strict
496
502
 
497
503
  # Note: image generation endpoints (/v1/images/generations) bypass maxCostPerRun.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.282",
4
- "description": "Smart LLM router — save 84% on inference costs. 82 models (6 free). Pay by credit card (API key) or USDC (x402 wallet).",
3
+ "version": "0.12.283",
4
+ "description": "Smart LLM router — save 84% on inference costs. 88 models (7 free). Pay by credit card (API key) or USDC (x402 wallet).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clawrouter
3
- description: Hosted-gateway LLM router — save 84% on inference costs. A local proxy that forwards each request to the blockrun.ai gateway, which routes to the cheapest capable model across 82 models from OpenAI, Anthropic, Google, DeepSeek, xAI, Z.AI, and more. 6 free open-weight models included. Also exposes realtime market data (global stocks, crypto, FX, commodities), Twitter/X intelligence, prediction-market data across Polymarket, Kalshi, Limitless, Opinion, Predict.Fun, dFlow + UMA oracle resolution + wallet identity & clustering, phone-number intelligence (carrier + SIM-swap fraud detection) plus AI-powered outbound voice calls (Twilio + Bland.ai), AND the Surf unified crypto data API (84 endpoints — CEX/DEX, on-chain SQL over 80+ ClickHouse tables, 100M+ labeled wallets, prediction markets, social/CT mindshare, news, VC fund intel) as built-in agent tools. Not a local-inference tool — prompts are sent to the blockrun.ai gateway.
3
+ description: Hosted-gateway LLM router — save 84% on inference costs. A local proxy that forwards each request to the blockrun.ai gateway, which routes to the cheapest capable model across 88 models from OpenAI, Anthropic, Google, DeepSeek, xAI, Z.AI, and more. 7 free open-weight models included. Also exposes realtime market data (global stocks, crypto, FX, commodities), Twitter/X intelligence, prediction-market data across Polymarket, Kalshi, Limitless, Opinion, Predict.Fun, dFlow + UMA oracle resolution + wallet identity & clustering, phone-number intelligence (carrier + SIM-swap fraud detection) plus AI-powered outbound voice calls (Twilio + Bland.ai), AND the Surf unified crypto data API (84 endpoints — CEX/DEX, on-chain SQL over 80+ ClickHouse tables, 100M+ labeled wallets, prediction markets, social/CT mindshare, news, VC fund intel) as built-in agent tools. Not a local-inference tool — prompts are sent to the blockrun.ai gateway.
4
4
  triggers:
5
5
  - "clawrouter"
6
6
  - "claw router"
@@ -53,7 +53,7 @@ metadata:
53
53
 
54
54
  # ClawRouter
55
55
 
56
- Hosted-gateway LLM router that saves <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% on inference costs by forwarding each request to the blockrun.ai gateway, which picks the cheapest model capable of handling it across <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models from 9 providers (<!-- br:models.free -->6<!-- /br:models.free --> free open-weight models). All billing flows through one BlockRun credential — either a USDC wallet paying x402 per call, or a BlockRun API key drawing on card-funded account credit. Either way you do not hold provider API keys.
56
+ Hosted-gateway LLM router that saves <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% on inference costs by forwarding each request to the blockrun.ai gateway, which picks the cheapest model capable of handling it across <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models from 9 providers (<!-- br:models.free -->7<!-- /br:models.free --> free open-weight models). All billing flows through one BlockRun credential — either a USDC wallet paying x402 per call, or a BlockRun API key drawing on card-funded account credit. Either way you do not hold provider API keys.
57
57
 
58
58
  **This is not a local-inference tool.** ClawRouter is a thin local proxy. Your prompts are sent over HTTPS to the blockrun.ai gateway for model execution. If your workload requires inference that never leaves your machine, use a local runtime like Ollama — ClawRouter is not the right tool for that use case.
59
59
 
@@ -147,7 +147,7 @@ it against a baseline nobody wrote down. See
147
147
 
148
148
  ## Available Models
149
149
 
150
- <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models including: gpt-6-astra [GPT-6 flagship, 1M ctx], gpt-6-sol, gpt-6-luna [fast, $0.10/$0.50], gpt-5.6-terra [balanced, stable default], gpt-5.6-sol [flagship], gpt-5.6-luna [cost-efficient], gpt-5.6-sol/terra/luna-pro [pro reasoning tiers], gpt-5.5, gpt-5.5-pro [max compute], chat-latest [ChatGPT Instant], gpt-5.4, gpt-5.1, gpt-4o, o3, claude-fable-5.1, claude-fable-5, claude-opus-5.5 [newest Opus, $4/$20], claude-opus-5 [1M ctx], claude-opus-4.8, claude-opus-4.7, claude-opus-4.5, claude-sonnet-5.5, claude-sonnet-5, claude-sonnet-4.6, gemini-3.1-pro, gemini-3.6-flash, gemini-3.5-flash-lite, gemini-2.5-flash, deepseek-v4-pro, deepseek-chat, grok-4.7, grok-4.6, grok-4.5, grok-4.3, grok-build-0.1, glm-5.3 [Z.AI flagship, 1M ctx], glm-5.3-flash [natively multimodal, 1M ctx — $0.15/$0.50], glm-5.2, minimax-m3, kimi-k3 [1M ctx flagship], kimi-k2.7, qwen3.7-max [Qwen flagship, 1M ctx], qwen3.7-plus, qwen3.7-flash, mimo [Xiaomi MiMo-V2.5 Pro, 1M ctx], and <!-- br:models.free -->6<!-- /br:models.free --> free open-weight models (nemotron-3.5-lightning [1M ctx, thinking mode — the free default], nemotron-3-ultra-550b [550B/55B MoE, 1M ctx — the largest free model], nemotron-3-nano-omni-30b-a3b-reasoning [256K ctx], llama-3.2-11b-vision [Meta Llama], north-mini-code [Cohere, coding, sub-second], laguna-xs-2.1 [Poolside, coding, ~161 tok/s]). The free tier is text-only — image turns route to a paid vision model, because the two free models the catalogs advertise as vision-capable fail a solid-colour probe on both chains and do so with an HTTP 200.
150
+ <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models including: gpt-6-astra [GPT-6 flagship, 1M ctx], gpt-6-sol, gpt-6-luna [fast, $0.10/$0.50], gpt-5.6-terra [balanced, stable default], gpt-5.6-sol [flagship], gpt-5.6-luna [cost-efficient], gpt-5.6-sol/terra/luna-pro [pro reasoning tiers], gpt-5.5, gpt-5.5-pro [max compute], chat-latest [ChatGPT Instant], gpt-5.4, gpt-5.1, gpt-4o, o3, claude-fable-5.1, claude-fable-5, claude-opus-5.5 [newest Opus, $4/$20], claude-opus-5 [1M ctx], claude-opus-4.8, claude-opus-4.7, claude-opus-4.5, claude-sonnet-5.5, claude-sonnet-5, claude-sonnet-4.6, gemini-3.1-pro, gemini-3.6-flash, gemini-3.5-flash-lite, gemini-2.5-flash, deepseek-v4-pro, deepseek-chat, grok-4.7, grok-4.6, grok-4.5, grok-4.3, grok-build-0.1, glm-5.3 [Z.AI flagship, 1M ctx], glm-5.3-flash [natively multimodal, 1M ctx — $0.15/$0.50], glm-5.2, minimax-m3, kimi-k3 [1M ctx flagship], kimi-k2.7, qwen3.7-max [Qwen flagship, 1M ctx], qwen3.7-plus, qwen3.7-flash, mimo [Xiaomi MiMo-V2.5 Pro, 1M ctx], and <!-- br:models.free -->7<!-- /br:models.free --> free open-weight models (nemotron-3.5-lightning [1M ctx, thinking mode — the free default], nemotron-3-ultra-550b [550B/55B MoE, 1M ctx — the largest free model], nemotron-3-nano-omni-30b-a3b-reasoning [256K ctx], llama-3.2-11b-vision [Meta Llama], north-mini-code [Cohere, coding, sub-second], laguna-xs-2.1 [Poolside, coding, ~161 tok/s]). The free tier is text-only — image turns route to a paid vision model, because the two free models the catalogs advertise as vision-capable fail a solid-colour probe on both chains and do so with an HTTP 200.
151
151
 
152
152
  ## Built-in Agent Tools
153
153