Repository navigation
feat(router): pay the router's quoted fee, capped at $0.003, and route only on the paid route - #534
Conversation
…uires` The 23 strict answer schemas in decision.ts become tolerant readers: an unknown field is dropped instead of failing the whole answer, and every known field keeps its type, bounds and checks. An object naming the keys of two arms of one union (a job's pollUrl or running rule) is still refused. An offer may list `requires`, in the `accepts` vocabulary, on the answer or on any spec. A build without a listed feature skips that offer alone: an other offer's spec (and any choice that runs it) is dropped, and when the offer is the answer's own, the answer becomes a `native` with reason code `requires_unmet` naming the missing features. The hook warns for each skipped offer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m text The over-cap note reads each listed service's price from its spec's `priceAtomic` only; the regex that read dollar amounts out of an offer's line, and the quoted-text guard beside it, are gone. An offer without specs names no listed price. A spec may set `timeoutS` (whole seconds, 1 to 120, the paid leg's own ceiling): the provider probe's deadline and the least the paid leg waits, in place of `--timeout`. It may set `maxBytes` (1 to 200 MiB): the cap on a binary result and on each linked media file, in place of the 200 MiB every call had. Both are optional and fall back to today's values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`requestDecision` always calls `POST /api/x402-router/route`. A call the
payer skips (`RouteSkipped`) returns `skipped` at once, with no second
request; with no payer, a 402 is a skip with the `no_payer` notice, and a
router charging nothing (the fee off, in development) answers the call.
Deleted: the free-route fallback in `requestDecision` and `freePath` on its
outcomes, the `paid-path-absent` router memo and the payer's 404 branch,
`PAID_PATH_ABSENT`, and the `FEE_REQUIRED`/`isFeeRequired` handling in
hooks.ts and tool.ts (including the tool's ask-twice retry). `routeFor` is
now synchronous and never null; `DecisionRoute` loses `path`.
The `tenjin hook <kind>` command form routes nothing: it tells the person
once per session to run `tenjin install`, and stays silent in a session the
plugin serves. Browse, `{tool}` and outcome reports keep the free route.
Copy and docs: the MCP instructions, `tenjin hook`/`tenjin mcp` help, the
install disclosure and docs/agent-permissions.md no longer describe a free
routing path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in#1046 step 4) The router's 402 is the only price. The payer reads the quote before the wallet is unlocked and pays it when it is at or below `ROUTING_FEE_CEILING_ATOMIC` (3000 atomic USDC, fee.ts), inside the SDK's per-payment cap, the deposit cap, the per-call limit and the daily budget. The per-payment checks run at the quoted amount. A quote above the ceiling is not paid: the call is skipped with `fee_above_ceiling`, reported as blocked, and the once-per-session notice names the quoted price. The payment policy keeps any `batch-settlement` canonical-USDC offer at or below the ceiling; no payee or exact price is pinned. The payer pays only the configured router's paid route: `routeFor(baseUrl).send` refuses any other origin or path unsent, so the ceiling never applies to a provider's 402, which `runPay` holds to that spec's price. A charge below the quote (batch settlement) is recorded by the SDK and builds the next voucher; a test pins it. The fake router can now quote and charge any amount. No fee amount is left in copy: the deposit line names the quoted fee, the unpaid line names none, and the install question, doctor, MCP instructions, README and docs say "a small flat fee". Counts with no quote (calls left in the channel, the funded-enough checks) use the ceiling and say "at least". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oday Three fixture files had drifted from tenjin's origin/main (lib/x402-router/fixtures/wire, 0414118e): - wire-hook-execute-spec.json carried the Exa `type` enum and default from tenjin#988 (`lite`), which is unmerged; the server still sends `auto | fast | instant`. Taken from the server. - wire-lookup-spec-by-id.json lacked the CoinMarketCap `slug` and `symbol` descriptions the server added. Taken from the server. - README.md described ROUTER_VERSION 2026-10-13.1 and one routing route. Taken from the server (2026-10-24.1: the paid route, the harness section, job specs, `running` reports), then extended: this repo is now the canonical set, `harness/` stays client-only, a "Reading an answer" section states the tolerant reader and `requires`, the paid-path section states the fee ceiling, the spec section names `timeoutS` and `maxBytes`, and the client-only `headers` token and its fixture are listed. wire-hook-execute-headers.json stays: it is this client's fixture for the `headers` token, which the server sends only once tenjin#988 lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opening The one-path change reworded that line of docs/agent-permissions.md to start "Every routing call goes to the router's paid route."; the suite-wide skills-text test still looked for the free-path opening. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tolerant` tells the server this build reads answers as a tolerant reader and honors `requires`. Released CLIs (alpha.25 to 31 carry most paid traffic and never auto-update) read strictly, so the server can send `requires`, `honored`, `timeoutS`, `maxBytes` or any later field only to a client that lists this token, and today's shape to the rest. Either repo can then ship first. Today's server ignores the token: it reads `accepts` only through `includes()` of the tokens it knows (catalog.ts `clientRuns` and `routerClient`, discovery `bazaarLane`), logs only known tokens, and bounds the list at 16 tokens of 1 to 32 characters; this build sends 9. Listed in the wire README's tokens and docs/agent-permissions.md; a test pins it in `acceptsFor` with and without the Bazaar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The install consent now reads "Tenjin charges a small flat fee per routing call, never more than $0.003, ...": the ceiling is the amount the person approves, as EIP-1559's max fee is. It renders `ROUTING_FEE_CEILING_ATOMIC`, a CLI constant, so it cannot go stale when the router's price changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erver sends The server always adds `specs` to an open-Bazaar `discovered` answer (`discoveredDecision` in app/api/x402-router/route.ts), and every released CLI takes it as optional; wire-hook-discovered.json and wire-lookup-discovered.json lacked it. Each now carries the server-generated spec, one per file, under the answer's own id; the files keep their inline style. wire.test.ts expects `specs` on that answer and pins its id to the answer's. tool.test.ts now covers both: an answer with the spec is shown and kept under its id, and the specless candidate path keeps its own test on a copy with `specs` removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- decision.ts: the union-refusal note names only `pollUrl`/`running`; the browse row is no longer called strict; `maxBytes` says it does not yet cap a job's polled result; `tolerant` gating is the server's once it reads the token (R2); `DecisionRoute` names the `unreached` case. - hooks.ts, gate.ts, tool.ts, mcp.ts: no comment credits `tenjin hook <kind>` with routing, free decisions or a signer-less leg; the request tool pays a routing fee and one payment per take. - hook-command.ts: `STDIN_TIMEOUT_MS` is the stub's own and no longer exported; the wire test that budgeted stdin plus the gate for a leg that no longer exists is gone. - `tenjin hook --help` describes `alive` as live, and only the six routing kinds as left from older setups. - docs: the free docs lookup pays the routing fee, a keychain that does not answer skips the call, safety-model.md drops "deciding costs nothing", and a skipped offer is "no provider paid", the answer's own offer becoming a named skip (same in the changeset). - fixtures README: the `jobs` condition on never-inline services, the stray blank line, the "no fee" paragraph, this client's job-poll rules, the `takes.max`, `requires`, `timeoutS` and `maxBytes` bounds, and three fixture rows for files this directory does not have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A `requires_unmet` skip says "no provider was paid": the routing fee for that answer was. The footer names it `native tools (offer needs newer tenjin)`, not the router choosing native. - A skip with no-fault reason (`channel_busy`, `busy`, `unlock_timeout`) tells the model in plain words that the next call usually routes, instead of putting the code in its text. - The deposit line says each decision costs "at most" the quote: the router may charge less. - Doctor's routing-fee row names the $0.003 ceiling, as install consent does. - A body over a spec's `maxBytes` under 1 MiB names the cap in bytes, not "0 MB". - A compare or multi-take quote probe waits as long as the spec's `timeoutS`, as the call's own probe does. - The hook-command stub marks its once-per-session notice with its own marker, so it no longer overwrites the wallet notice that "Tenjin wallet funded/unlocked" reads. - fee.test's list of every skip reason includes `fee_above_ceiling`, so the fix-text guard covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom the #532 review - decision.test's "refuses a hook answer carrying a contract" passed only because its fixture hint lacked the id; the hint now names it, and the test pins what tolerant reading does: the offer is read, the contract dropped. - Test names and comments no longer call routing free or the call "going free" (decision, routing-payer, mcp, tool, wire, fee tests), and wire.test names `headers` and `tolerant` as client-side tokens. - Dead helpers: codex-paid-hooks `connect`'s unused fetch parameter, and hooks-fee's `NATIVE` and 200 defaults on its one 402 router. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The paid leg's floor was the spec's deadline when one was set, so a `timeoutS` below the CLI timeout (30 s in `tenjin mcp`) cut the request after the signed authorization had left: the seller could settle, and the result was lost. The floor is now the larger of the two: a spec may lengthen the paid leg, never shorten it. The probe still takes the spec's deadline. A test with no advertised bound fails without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names `requires` is a backstop: the server sends only tokens a client lists in `accepts` (S2 filters offers by it), so a client meets an unmet token only when that rule broke. When the backstop drops anything the answer's line names (the answer's own offer, a spec whose id the line carries, or any option of its choice set), the line cannot be shown as written, so the whole answer becomes the named `requires_unmet` native. A dropped spec the line does not name is still just dropped. This replaces trimming the choice set while keeping a line that still offered the trimmed options. The fixtures README states the contract rule; the permissions doc and the changeset say the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tenjin doctor` and `tenjin install` probed the free route, which no
longer routes, so a paid route that 404s or quotes above the ceiling
stopped every routing call while the router row read ok. The probe now
sends one request to `POST /api/x402-router/route` in the `warm` form
(`{schemaVersion: 1, warm: true}`), which runs no gate:
- 402: live and charging. Its quote is read, and one above
`ROUTING_FEE_CEILING_ATOMIC` warns that every routing call is skipped;
a 402 with no routing offer this CLI reads warns too.
- 200: live with the fee off. 400: a server with no `warm` form, live.
- 404 or 5xx: a failure, as before.
The x402 paywall answers a request with no payment 402 before the handler
reads the body (`withX402FromHTTPServer` returns the payment error before
calling the route handler), so this holds before tenjin#1050 ships `warm`.
Reading a quote moves to `routing-quote.ts`, shared with the payer, and
the probe loads it only on a 402, keeping x402 out of the chunk install
and the status line share. Doctor ends the routing backoff on any
answered probe, a fee warning included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… path move to their own PR At the reviewer's and the owner's request, #532 keeps the tolerant contract alone, and the quoted fee and the one routing path move to a PR stacked on it (vraspar/router-quoted-fee), which restores exactly what this commit removes. #532 keeps: the tolerant answer schemas, `requires` and its fail-safe, `tolerant` in `accepts`, prices and `timeoutS`/`maxBytes` from spec fields (with the paid-leg floor and the quote probe's timeout), the fixture drift fix and the open-Bazaar `specs`, the fixtures README's contract rules, and the review fixes to those pieces. Removed here, to return in the stacked PR: the quoted fee under the `ROUTING_FEE_CEILING_ATOMIC` ceiling, `routeFor`'s route guard, `fee_above_ceiling`, the install consent naming the ceiling, the one routing path (tenjin#1026: one paid request, the deleted free-path fallback, the hook-command stub), the doctor's paid-route probe, their docs and changesets, and their review fixes. On this branch requests take the paid path with the free-path fallback, as on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing path Stacked on the tolerant contract (#532), this restores the part split out of it in 23a8ad9: - The quoted fee (tenjin#1046 step 4): the payer pays the router's 402 quote at or below `ROUTING_FEE_CEILING_ATOMIC` (3000 atomic USDC, fee.ts), inside the SDK's per-payment cap, the deposit cap, the per-call limit and the daily budget, with the per-payment checks at the quoted amount. A quote above it is skipped as `fee_above_ceiling`, named once with its price. No payee is pinned; `routeFor` pays only the configured router's paid route. A charge below the quote is recorded by the SDK. - No fee amount in copy: the deposit line names the quote as "at most", and install consent and doctor name the ceiling. - One routing path (tenjin#1026, CLI half): `requestDecision` always calls the paid route; a call the payer skips returns `skipped` with no second request, and with no payer a 402 is a `no_payer` skip. The free-path fallback, the `paid-path-absent` memo and `FEE_REQUIRED` handling are gone. `tenjin hook <kind>` routes nothing and tells the person once to run `tenjin install`. Browse, `{tool}` and outcomes keep the free route. - `tenjin doctor` and `install` probe the paid route in the `warm` form, read its quote and warn above the ceiling. - Docs, changesets and the #532 review fixes for these pieces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es your wallet The owner approved a consent clause: the routing payment from the wallet names its address, which the router sees with each routed turn. Install text must not grow, so it is folded into the fee sentence by rewording: the deposit sizes go in parentheses, "while your balance is under" becomes "below a ... balance", and "each deposit counts against these limits, and fees from it are not counted again" becomes "... counts against these limits once". The full spend question goes from 412 to 410 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-topup #532 was split: the quoted fee, one routing path, the doctor probe and the wallet-address consent moved to #534, which this PR now stacks on. Conflicts, resolved by meaning: - spend-question.ts and its two pinned tests: kept C2's sizing (no $0.25 / $0.03 split) and the fee branch's wallet-address clause and closing sentence: "paid from funds set aside, up to $0.25 at a time, in a payment naming your wallet address. Unused funds remain yours; each deposit counts against these limits once." The whole question is 376 characters, the same as C2's before the merge (the fee branch's is 410). - gate.ts: the fee branch's comment without the command form's stdin wait, with C2's top-up wait. - routing-payer.ts: the quote parsing moved to `routing-quote.ts`, so C2 keeps its `TopUp` type and `this.quoted`, and drops its own `routingOffers`. - tool.ts: C2's top-up sentences, the fee branch's no-fault fallback, and a line of its own for `top_up_unavailable`, which that fallback would misdescribe. - hooks.ts imports; the hook test's deposit line now reads "costs at most"; one test name taken from the fee branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A1igator
left a comment
There was a problem hiding this comment.
Review at a6fef25 (layer over #532 at 23a8ad9): 0 blocking, 2 should fix, 0 nits
Should fix
- The PR body references the private repositories five times (
BackTrackCo/tenjin-notes#43,BackTrackCo/tenjin#1046,#1026,#1050,#1052), which this public repository's rule forbids: each reference publishes a private PR number and creates a cross-repo backlink. Replace them with plain words (the plan this is part of; the server changes that charge under the quote and add thewarmform) and keep#532, which is this repository's own. #534 STALE_HOOK_LINEhands a terminal command to the person: it is shown in the harness'ssystemMessageto whoever is typing, and the operator's rule is that person-facing text never hands a person a command (it says "ask your agent to ..."). Reword it to tell the person to ask their agent to set Tenjin up again, and let the changeset and thetenjin hookhelp text follow.tenjin-agent/src/router/hook-command.ts
Line 43 in a6fef25
Nothing else I would hold the code on: a skipped payment makes no second request on any route (the Codex hook log pins one route unpaid line), routeFor refuses any URL off the configured router's origin and paid path before anything is sent so a provider's 402 never meets the ceiling, quoteOf takes the largest routing offer at or below $0.003 and routingPolicy offers the SDK only those, a charge below the quote is left to the SDK and tested, the doctor probe's warm POST maps 402 (quote read, above-ceiling warns), 200, 400 and 429 to live and 404 and 5xx to failed, and the hook chunk graph stays free of the SDK (dist-chunks passes on a fresh build since routing-quote is loaded only on a 402). The current plugins run mcp_tool legs, so the stub only meets a copy from an older build. Typecheck, eslint and the touched test files pass locally.
There was a problem hiding this comment.
🤖 Reviewer · Review PR #534: feat(router): pay the quoted routing fee under a $0.003 ceiling,… · claude-code · claude-opus-5-5
Deep review of a6fef25 against base 23a8ad9 (48 files, 1,994 changed lines). The review used a context pack, 12 line auditors plus 2 catch-up auditors, 11 file owners and 6 independent reviewers, then verifiers. A coverage script confirmed that every changed and context line is in a ledger, so no lines were left unaudited.
Verdict. The money paths hold up:
quoteOfandroutingPolicyagree, so the SDK can only pick an offer at or below the amountfeeRefusalchecked.- The
routeFororigin/path guard builds its URL the same way asrequestDecision, and the call runs withblockRedirects. - A quote above the ceiling is refused before unlock.
- The SDK's corrective-402 retry reuses the first 402's requirements (
@x402/fetchindex.mjs:69), so the amount can't be raised mid-call.
The two p2s are about what the person is told:
- On older Claude Code, the new stale-hook line sends them to
tenjin install, which can't fix it there. - The install disclosure leaves out the wallet address that the spend question now names.
The rest are nits: comments or messages made untrue by the switch to a floating quote, and a few edge behaviours.
Leads. eslint (repo config) is clean on the changed files. semgrep p/default and p/security-audit had one hit (bidi characters, tool.test.ts:2554). It is not within three lines of a change, so I dropped it.
Single-source, not confirmed (not posted inline)
src/router/routing-payer.ts:609-619: the 402 body is left undrained whenaskFirstrefuses. The base had the same pattern for its other refusals; this PR adds thefee_above_ceilingexit. Whether it really keeps sockets out of the pool is unverified.
Refuted or out of scope (dropped)
- Per-call limit not re-checked on a corrective 402. Refuted: the SDK retries with
createPaymentPayload(paymentRequired)from the first 402. docs/safety-model.md:23"same limits". Refuted: the day's budget binds through the reserved deposit.- Fixed
$0.03CHANNEL_DEPOSIT_ATOMICagainst a floating hint in eligibility/doctor. Already true at the base. src/router/fixtures/README.mdfee_required. It describes the server's free route, which is still accurate.docs/agent-permissions.md:125-126"five files" and "docs fetch" background process. Stale since before this PR.
Test gaps
src/commands/update.test.ts:191:not.toContain('Routing costs')can no longer fail, becauseROUTING_FEE_TERMSnow starts "Tenjin charges…".hooks-fee.test.ts:- The deposit-line check (≈199) uses the default quote, which equals the ceiling, so it can't tell
route.fee()from the constant. - No hook-leg case for a quote above the ceiling (≈222-281).
- The
not POST /api/x402-routerbusy-channel assertion (≈216) is vacuous now.
- The deposit-line check (≈199) uses the default quote, which equals the ceiling, so it can't tell
tool.test.ts: no case whereRouteSkipped(FEE_ABOVE_CEILING, { quotedAtomic })reaches the agent text.- Doctor: a
warnprobe clearing the unreachable memo (doctor.ts:329) is untested. hooks-backoff.test.ts: the 5xx test's memo assertion can't fail, because a skip returns before the memo code.routing-payer.test.ts:unpaidWhyreturns undefined for afailedoutcome too.- The refused-URL test doesn't assert the reason.
- The two-offer test doesn't assert
route.fee().
fee.test.ts:EXPECTEDlacksfee_above_ceiling, and the comment at :93 says notices name "no fee amount".- No fake router can simulate a server missing
/route(404) any more. - Stale test names or comments:
reachability.test.ts:84"empty body"hooks.test.ts:808-812codex-paid-hooks.test.ts:192mcp.test.ts:1075
…ed skip, name the ceiling
- tool.ts: a skip whose reason lasts (money, the wallet, a limit, the
fee's price) gets "Tell the user this once, and use your own tools for
now." again, as the base did for these reasons; a busy channel, a slow
unlock or a payment still out keeps "Use your own tools for this one."
- A skip that came after the router answered (its 402, a 5xx, a busy
channel, a refused deposit, no payer) now carries `answered`, and the
hook leg clears the `unreachable` memo for it, as any answer does.
- The deposit line names the ceiling ("Each routing call costs at most
$0.003"), the bound every later call keeps, instead of the last quote.
`DecisionRoute.fee` had no other reader and is gone.
- install prints a warn router check (a 402 it does not pay) as a
warning with its detail, not "The router did not answer".
- doctor's routing fee row says the quote is paid only when at most the
ceiling, so it no longer contradicts a router check that warns.
- Tests: the deposit-line check uses a quote below the ceiling; the
vacuous free-route assertion is gone; the 5xx backoff test now checks
the memo is cleared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…names the wallet address - `spendChoiceQuestion()` is 366 characters (371 at 23a8ad9, 410 before this): each routing call pays a quoted fee of at most $0.003, from funds set aside ($0.25 at a time, or $0.03 under a $0.50 balance), and names the wallet address; unused funds remain yours, and each deposit counts against the limits once. - DISCLOSURE's first line says each routing call's text goes beside the session id and the wallet address, which its payment names; the docs lookup sentence is shorter, so the disclosure is 679 characters (702 at 23a8ad9). - update.test asserts against the current fee terms, so it can fail again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
STALE_HOOK_LINE, shown in `systemMessage`, said "Run `tenjin install`". It now says to ask the agent to set Tenjin up again, updating Claude Code or Codex first if Tenjin needs a newer one: below 2.1.287 a settings install defers the plugin, so reinstalling alone does not fix it there. The `tenjin hook` help, the one-routing-path changeset and the permissions doc follow, and the doc says the stub is silent in a session the plugin serves. An old settings install's `tenjin hook` commands are stubs now, so install's deferred path, doctor and the doc no longer say that install "keeps routing": it stays as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted fee - The fee is quoted and never more than $0.003, not "a small flat fee": the request tool's instructions, README, the permissions and safety docs. A wallet under $0.50 deposits the server's hint, not a fixed $0.03. The quoted-fee changeset names doctor among what names the ceiling. - Comments: the stub's marker is shared with builds up to alpha.31's unpaid-fee notice; the hook legs are run by `tenjin mcp`'s `hook` tool, not the command form; 1.5 s are left after the decision, 500 ms only on a deposit call; a non-fail probe can be a warn for an unreadable 402; the `warm` form is the server's, stated without asserting what the server does with it. - Tests: `fee_above_ceiling` joins the notice table and its comment is true; stale names and comments in reachability, hooks, codex and mcp tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🤖 Reviewer · Review PR #534: feat(router): pay the quoted routing fee under a $0.003 ceiling,… · claude-code · claude-opus-5-5
Re-review of a6fef25..3343ea8. Both p2s are resolved, and so are 15 of the 19 nits. I found nothing new.
p2s, resolved
- Stale-hook loop (
hook-command.ts:43): the line no longer hands the person a command. It now says to ask the agent to set Tenjin up again, updating Claude Code or Codex first if needed. Below the floor, install's deferred notice already namesclaude update, so the advice now leads somewhere. The "keeps routing" wording is gone from all four places: install's deferred reason, notices and steps; doctor's migration and plugin rows; anddocs/agent-permissions.md:137, which now says the oldtenjin hookcommand entries no longer route. - Disclosure (
install.ts:111): it now names the wallet address beside the session id.
Nits, resolved
- The spend question is 366 characters, under the base's 371. I measured it with
npx tsxat this head. - An answered skip now ends the backoff.
call.answeredis set after the first response of any status and carried on everyRouteSkippedthrown after it, includingno_payer. The hook leg clears the memo. The updated 5xx test writes an expired memo, whichreadRouterMemoignores but does not delete, so itsreaddirassertion would fail without the fix. tool.tsuses the lasting-reasonnextStepagain.- install prints a
warnrouter check as itself, not as "did not answer". - The deposit line names the ceiling, and
DecisionRoute.feeis gone. - Doctor's fee row and the comment at :328 are reworded.
- "Small flat fee" is replaced in
mcp.ts, README and the docs. - The changeset, the "once per session" doc line and the fixed $0.03 deposit are reworded.
- The
gate.tscomment's numbers hold: 5 − 3.5 = 1.5 s, andDEPOSIT_GATE_TIMEOUT_MS4.5 s leaves 0.5 s. - The
warmcomment and the hook-leg docstrings are reworded. - The shared marker is now documented as intended.
update.test.tsnow asserts againstROUTING_FEE_TERMS.
Still open, unchanged and with no reply (all nits; the original threads stand)
hooks.ts:1692: a fee-off decided call can still print "Tenjin wallet unlocked."hooks.ts:1685: afee_above_ceilingmarker is never released.fee.ts:88: a"0"quote still passesfeeRefusalunder a zero budget.status.ts:149: "at least N calls".
Checks I recorded at this head: npx vitest run src/router src/commands/update.test.ts passed (1733 tests), and npm run typecheck passed.
The pending release note still said $0.03 and counted calls at the old fixed fee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🤖 Reviewer · Review PR #534: feat(router): pay the quoted routing fee under a $0.003 ceiling,… · claude-code · claude-opus-5-5
Re-review of ee3eefb..0e7284b, which edits only .changeset/pre/sdk-routing-deposit.md.
Resolved: the changeset nit at fee.ts:35. The routing-deposit note now says a small wallet sets aside "the router's suggested amount, $0.05 today, and keeps routing until it drops below that". That matches CHANNEL_DEPOSIT_ATOMIC and the payer, which sizes from the live hint. Dropping the "once every 83 calls" and "about every 10 calls" counts is right, since those assumed the old fixed fee.
One correction to my earlier comment: I called this note already released. It is not in CHANGELOG.md, so it is still pending, and editing it is the right fix.
Still open: the fee.test.ts:50 nit (the alignment test only checks that the SDK echoes an explicit hint). It is untouched and does not block.
The diff is release-note text only, so I ran no new checks. The router tests and typecheck I recorded at ee3eefb cover the code, which is unchanged.
It pins the constant to the router's announced $0.05 hint; it cannot see what the production 402 sends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🤖 Reviewer · Review PR #534: feat(router): pay the quoted routing fee under a $0.003 ceiling,… · claude-code · claude-opus-5-5
Re-review of 0e7284b..3cc1893, a one-line test rename in src/router/fee.test.ts.
Resolved: the fee.test.ts:50 nit. The test is now named "pins the constant to the router's announced $0.05 hint (extra.minDeposit 50000)", which matches what it checks.
All findings from my reviews of this PR's later heads are now resolved. The diff changes only a test title, so I ran no new checks; the router tests and typecheck I recorded at ee3eefb still cover the code, which is unchanged.
A1igator
left a comment
There was a problem hiding this comment.
Re-review at 3cc1893: nothing left I would block on.
Round 3, layer over #532 at 23a8ad9. Delta since round 2 (3343ea8): CHANNEL_DEPOSIT_ATOMIC is 50000 and the wallet_low sentence, the README, docs/agent-permissions.md, the deposit test table and the pre-release changeset all say $0.05; the payer still sizes from the 402's extra.minDeposit through the SDK (depositAmountForRequest, which also discards a hint under the quote), with the constant used only where no 402 exists (doctor, the install question, the static wallet_low line). No $0.03 or 30000 reference remains in the layer. Locally at 3cc1893: vitest on the seven touched test files (450 tests) passes.
A1igator
left a comment
There was a problem hiding this comment.
Approved again at 3cc1893. The review at this head is clean: the CLI sizes a new channel from the router's $0.05 hint, the wallet floor and the docs agree, and the scoped tests pass locally. CI does not run while the base is a stack branch, so this approval rests on the review and the local runs; the CI rollup evaluates once this PR targets main.
…ontract The fee change now merges on its own, first, so it no longer sits on the tolerant contract (#532). It needed nothing from it to work: it only borrowed two of its names in code that both PRs rewrite. Rebuilt on main, a decided answer carries no `skipped` offers and the paid path reads answers with main's `readDecision`. The fixtures README drops "the same bodies and answers as the free route", which stopped being true once every routing call goes to the paid route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ceiling The payer signed the SDK's $0.25 ceiling from any wallet holding twice it ($0.50), else the hint, and refused a wallet below the hint as wallet_low. It now signs the smallest of the 402's extra.minDeposit ($0.05 when a 402 has none), the wallet and the per-call limit, never below ten quoted fees or what the next voucher needs, and never above $0.25. A wallet below that amount is wallet_low, so one between ten fees and the hint deposits all it holds; doctor's canPayRouting and the wallet_low line use the same floor, ten fees at the ceiling ($0.03). The deposit still rides the routing call. The README, the permissions doc, the deposit changeset and the install question say the new rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pdate Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base branch was changed.
|
@A1igator done as you asked on the plan:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
🤖 Reviewer · Review PR #534: feat(router): pay the quoted routing fee under a $0.003 ceiling,… · claude-code · claude-opus-5-5
Re-review of 3cc1893..27a3ced. This PR now targets main directly instead of #532.
What changed
- A clean merge of
main: hash-wasm (#521) and others. I checked thatgit merge-treeof the two parents equals the merge commit; that merged code is not reviewed here. 9a6e1f7drops #532's tolerant contract. Againstmain,pay.ts,http.ts,choices.ts,jobs.tsand #532's three changesets are now untouched.decision.tskeeps only this PR's one-route change onmain'sreadDecision.8a7af5fadds a new deposit-sizing rule.27a3cedis docs only.
I re-emitted the guide because the base and the deposit approach both changed.
The new deposit rule
The deposit is the smallest of three amounts: the 402's minDeposit (or $0.05 when it has none), the wallet balance, and maxAutoSpend. It is never below ten quoted fees, and never above min(SDK cap, $0.25).
- I traced it against the SDK.
parseAnnouncedMinDepositdrops a hint below the quote. The SDK'sresolveDepositAmountthen requires at leastminimumDepositAmountand clamps tomaxDeposit, both of whichroutingDepositAtomicalready satisfies. - The router's hint can't push the deposit past the per-call limit or $0.25 (see the confirmed note inline).
- Behaviour change, documented in the commit: a funded wallet now deposits the hint, $0.03 in production today, instead of $0.25. So deposits, which take the longer 4.5 s budget, come about every ten calls instead of every ~83.
One new nit (inline, eligibility-check.ts:148): doctor sizes its deposit from $0.05 while production announces $0.03. With $0.03–$0.05 left in the daily budget, doctor reports a refusal the payer would not make. I reproduced this with npx tsx.
CI
- The Node 22 job (lint, typecheck, build, test, audit) passed at this head.
- The Node 24 job failed one test,
codex-paid-hooks.test.ts > show the real reason…, withENOTEMPTY … rmdir '/tmp/router-codex-paid-*/router'inafterEach. - That looks like a pre-existing teardown race, not this change. The same error hit a different test in that file on #532's run 40961476849. The router observer's chained, unawaited
writeObservation(router-observer.ts:101) can still be writing underrouter/when the test'srmruns. - It passed 6/6 locally on Node 24.22. A rerun should clear it. Awaiting or disposing the observer in
server.close()would fix the flake.
Earlier nits: still open from the first review and unchanged:
hooks.ts:1713: the "unlocked" notice can fire with the fee off.- The
fee_above_ceilingmarker is never released. fee.ts:108: a zero quote passes under a zero budget.status.ts:149: "at least N calls".
None of these blocks.
|
@A1igator both aether threads are resolved. The deposit-rule one is a confirmation with nothing to change. The doctor-sizing nit is declined with its reason in the thread, because the proposed fix would turn into the opposite mismatch once the router announces $0.05. The head is still 27a3ced. 🤖 Generated with Claude Code |
Keeps this PR's one paid request (no free-path retry, so no `freePath`) and adds main's `skipped` offers on top. The paid route's answer, and a payer's unreached answer, are both read through `readOffer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8b6efca
|
@A1igator I merged main, which now has #532, into this branch at 8b6efca. There were three conflicts, all on lines that #532 and this PR both changed:
Typecheck, lint and the four router test files pass locally (431 tests). The push dismissed your approval, so could you re-approve once CI is green? 🤖 Generated with Claude Code |
There was a problem hiding this comment.
🤖 Reviewer · Review PR #534: feat(router): pay the quoted routing fee under a $0.003 ceiling,… · claude-code · claude-opus-5-5
Re-review at 8b6efca. Since 27a3ced, the only change is a merge of main, which now includes #532 (tolerant contract, CLI side). Compared with main, the PR still changes the same 52 files, so the previous reading guide carries forward.
Merge resolution. git show --remerge-diff shows conflicts in three files, all resolved correctly:
src/router/decision.ts:DecisionOutcomekeeps main'sskipped?: SkippedOffer[]ondecidedand dropsfreePath. That's correct, since this PR removes the second request to the free route.- In the
RouteSkippedcatch, the payer'sunreachedanswer is now read throughreadOfferinstead ofreadDecision, so an offer this build can't run is taken out on that path too. - The paid answer still goes through
readOffer. - The free-route reads are as they are on
main: browse still usesreadDecision, and the tool spec read still usesreadOffer. - No
freePathreference is left anywhere insrc.
src/router/hooks.ts: the import merges both sides (REQUIRES_UNMET,ROUTE_PAID_PATH). Main's skipped-offer warning loop runs on anydecidedoutcome, which covers the paid answer and anunreachedanswer alike.src/router/fixtures/README.md: keeps this PR's fee and ceiling paragraph.
The other files merged without conflicts and match main's #532 content, which I didn't re-review.
Prior findings. Neither file was touched by this push:
src/router/routing-payer.ts:760: the confirmed note still holds.src/router/eligibility-check.ts:148: the doctor-sizing nit is still open. Doctor checks the policy against $0.05, but production's hint is $0.03.
CI. All jobs are green at this head, including Node 24, so the codex-paid-hooks teardown flake from the last head didn't come back. Greptile and Aether Review are still pending.
The four nits from the first review are unchanged and still non-blocking. No new findings.
What
The CLI pays the router's quoted fee, capped at $0.003, sends every routing call to the paid route, and deposits about 5 cents at a time. Based on
main, and merges on its own, first. It was split out of #532 at review and needs nothing from the tolerant contract.RoutingPayerpays it at or belowROUTING_FEE_CEILING_ATOMIC($0.003, what every install agreed to), inside the SDK's per-payment cap, the deposit cap and the spend limits. A quote above it is skipped asfee_above_ceiling, with one notice per session.routeFor(baseUrl)refuses any other origin or path and blocks redirects. Provider 402s go throughrunPayagainst their spec price.extra.minDeposit: $0.03 from today's production, $0.05 once the router's update ships), less if the wallet or the per-call limit is smaller, at least ten fees ($0.03 at the ceiling) and at most the SDK's $0.25 cap (fee.tsroutingDepositAtomic). It replaces the rule that signed $0.25 from any wallet holding $0.50. A wallet below the deposit iswallet_low, andtenjin doctoruses the same $0.03 floor.requestDecisionmakes exactly one request, to the paid route. A skipped payment is a skip with the once-per-session notice, never a second request.freePath, thepaid-path-absentmemo, the payer's 404 branch,FEE_REQUIRED/isFeeRequiredand the tool's ask-twice retry.tenjin hook <kind>keeps a stub that tells the person once per session to ask their agent to set Tenjin up again (updating Claude Code or Codex first if needed).tenjin doctorchecks the paid route. OnewarmPOST to/route:warm) passes;The paywall answers 402 before reading the body, so this works on today's production. A local dev server on tenjin main with the fee off reads as failed until the server change that adds the
warmform lands.Checks
main(9a6e1f7): typecheck, eslint and prettier pass, andvitest runpasses on the 25 changed test files plus both suite-wide consistency tests (26 files, 1,368 tests).vitest runpasses on the 25 changed test files (1,364 tests). Putting back the oldwallet_lowcheck fails two sizing cases.extra.minDeposit: "30000", so this build's first deposit there is $0.03.warmform and the fee off:tenjin doctorpassed its paid-route check with onewarmPOST.main)🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no new blocking issue was established.Summary
The CLI pays the router’s quoted fee up to $0.003 and sends routing decisions only to the paid route.
tenjin doctorchecks whether the paid route can answer.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Hook or request tool] --> B[Paid routing route] B --> C{Router asks for payment?} C -->|No| D[Read decision] C -->|Yes| E{Quote within ceiling and limits?} E -->|No| F[Skip routing; use native tools] E -->|Yes| G[Pay through routing channel] G --> DReviews (6) · Last reviewed commit: "Merge main (tolerant contract, CLI side)..." · Reviewed by Greptile