0.51.0 — round four went at round three - #150
Merged
Merged
Conversation
…count-rail errors that classify
Three items verified by hand after the 0.49.0 audit, plus the test file its
tail called out.
- blockrun_wallet action:"delegate" wrote spent: 0 unconditionally, so an
agent that had exhausted its allocation could re-delegate its own agent_id
and start again — and delegate is a tool the model can call. The ledger now
carries across a re-delegation and the response says what carried over; a
limit is the operator's to raise, spend is not theirs to erase. The global
cap was never bypassable this way, so the sub-cap simply meant nothing.
- The status-code boundary excluded a following dot outright, which kept
$402.50 from reading as a status but also meant every account-rail message
('BlockRun account API error: 502.', the SDK's own shape) fell through
unclassified: the wallet rail got guidance, the account rail got none. A dot
NOT followed by a digit is punctuation; a dot followed by one is a decimal
point and still disqualifies. One shared STATUS_END constant now, so the
three matchers cannot drift.
- raw-call.ts — the rail switch for eight path-based tools — had no test on
either rail. Eight now, covering which rail runs, that the other is never
touched, and that paidUsd stays null (never 0) on a wallet call, since
recordActualSpend books a real charge as free for 0.
- docs/mcp-schema-overhead.md re-measured at 0.49.0 (19 tools, 12,657), with a
line saying the README card is the source of truth and this page is dated
prose.
654 tests, typecheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…o the route's own limit, and correct four stale claims Six more from the audit tail, each read end to end first. - The model cache was a single shared list with no key, so blockrun_wallet action:"chain" left the previous gateway's catalogue in place for the rest of the 5-minute TTL — and the two gateways do not serve the same one (78 chat models on Base, 83 on Solana, measured 2026-09-09). Keyed by rail+chain now, with the in-flight fetch shared between the tool and the resource instead of both issuing it. - blockrun_models was annotated openWorldHint:false while fetching the live catalogue over the network. It is readOnlyOpenWorld: the hint describes whether the tool reaches outside the process, and this one does. - VIDEO_POLL_TIMEOUT_MS was 90s against a gateway poll route that declares maxDuration = 60, so 30s of the signed authorization's window was spent on a request the server had already abandoned. The invariant test now states the real unclamped worst case (budget + interval + timeout), which is what its own comment always said. - README FAQ said 'a few media/paid tools settle on Base only (noted above)': not media, and not noted. It names them — blockrun_defi, blockrun_modal and native Anthropic chat. - budget_action accepts 'check' and DEFAULTS to it; neither the examples nor the field description said so. - skills/blockrun claimed 'No API keys, no accounts' — the account rail has existed since 0.46.0. 654 tests, typecheck clean. README card and docs/mcp-schema-overhead.md re-measured (12,686 full / 5,189 trading). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…not only what it is for
The setup prompt explained the purpose ('settle YOUR signed orders from the
deposit wallet') and never the size: the default grants an UNLIMITED pUSD
allowance to four collateral spenders plus all-or-nothing ERC-1155 operator
rights to five contracts. That is standard for Polymarket and it is exactly the
kind of thing to state before the signature rather than after.
POLYMARKET_BOUNDED_APPROVALS has existed all along and nothing surfaced it at
the moment of consent; the prompt now names the amount on both branches and
points at the bound, with a test pinning both wordings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
Found by the round-2 audit, which exists because each round must regression-hunt the last one's fixes. Both are about a funded wallet. - The continuity pin was written by asking getChain again after provisioning and comparing. Under BLOCKRUN_KEYCHAIN=strict that question cannot be answered at that point: minting stores the key and DELETES .solana-session, so the file check misses and the keychain probe returns the value memoised before the mint. It answered "base" both times, no pin was written, and the next start found the stored key and moved a funded Base user onto an empty Solana wallet — the 0.32.3 failure CHAIN_AUTO_FILE exists to prevent. The pin is now written off the provisioning FACT (did THIS call mint the other chain's wallet), which is local and cache-free, and the probes are dropped after a mint so a same-process reader is honest too. - ensureSolanaWallet assigned its cache only after awaiting createSolanaWallet, so two overlapping callers both minted; 0.49.0 made that reachable from two entry points at once (the blockrun://wallet resource and action:"setup"), and last-writer-wins means one caller gets a funding QR for an address whose key was discarded. Single-flighted — but the rejection is deliberately NOT cached, or unlocking a keychain and retrying would stay broken until restart, which is the poisoning 0.49.0 removed when it stopped memoising a miss. Both were untested: chain-precedence runs with the keychain off and keychain-precedence mocks persistKey to a no-op, so neither could see the strict delete. Six new tests cover the strict-mode pin, the fresh-install direction, the no-mint case, concurrent callers, and a rejection that must not stick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…default chain too 0.49.0 taught the media tools two things and taught them per-rail, so Solana — the default chain since 0.46.0 — ended up with neither. Round-2 audit. - blockrun_music (both wallet rails) and blockrun_speech (Base) signed whatever the 402 quoted: no sanity check, no re-reservation against the cap. Music's Solana call passed no onQuote at all, so the helper's guard hook fired against nobody and the SPL transfer was signed for whatever the quote said. Both now run assertQuoteNearEstimate and re-reserve the real amount before signing, the way video and image already did. - Giving up while a paid request may still be in flight booked the charge on Base (paidPollInFlight) and on the account rail (BilledJobError), and nothing on Solana — where the shared helper's own message says a poll in flight at the deadline can settle server-side. A settled Solana render therefore moved no budget at all and the caller was invited to pay for it again. Both tools now book the quote conservatively there and say the charge MAY have gone through, instead of the Base-only "No payment was taken". - formatError told users to fund their wallet on messages that say in the same breath that nothing was charged: the uncharged markers gated only the "payment" keyword clause, so a bare 402, "balance" or "insufficient" still earned the footer. Two of this repo's own messages did it — the video tool's unreadable-quote refusal (a wallet holding $1,000 told to top up) and RealFace's "No payment taken", which was not even in the marker list. The markers now cover every phrasing the four authors use and gate the whole branch. A genuinely empty wallet still gets the advice. Ten new tests: five for Solana rail parity, five for the funding footer, including negative controls on both sides. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…r booked at zero Round-2 audit, the last of the P2s. All three are the same shape: 0.49.0 taught one surface that "we do not know whether it landed" and left its siblings saying "nothing happened". - The order card's stale-amount guard wrote place.disabled on every input event, including while a submit was in flight, so nudging the amount up and back down during the CLOB round-trip re-enabled an ARMED button reading "Submitting…" — one more click placed a second identical real-money order with no confirmation. The card now tracks submitting and outcome-unknown explicitly, disables the amount field during a submit, and re-arms only when the failure says nothing was signed. A throw is transport-level, which is exactly when the order may already be live, so it now warns instead of inviting a retry; a declined consent prompt still restores the card, because that one really did sign nothing. - withSettledCost short-circuited the account rail with no try/catch, so onSettledThrow never fired there: a chat call the gateway accepted, billed and then dropped mid-stream booked $0 and read as a free failure, whose obvious next step is to pay for it again. The account rail now reports a billed failure with the amount unknown, the ledger books the estimate, and the note points at the dashboard for the exact figure. - blockrun_realface had no in-flight tracking on its paid POST on any of the three rails, so an abort after the gateway settled left a real charge unbooked and the reservation released. It gets the same flag and the same wording video and music already had. The order card's two post-failure predicates moved out of the DOM into apps/order-safety.ts and are unit-tested, replacing a test that grepped two string literals out of the minified bundle. 673 tests, typecheck and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
tx-fee.ts has stated the rule since 0.40.1 — reserve high, book accurate, and do not collapse the two numbers — and exactly one file honoured it. Every path-based tool passed its RESERVE (base + TRANSACTION_FEE_USD, $0.002, rounded against us on purpose) as recordActualSpend's fallback, and on the wallet rail there is never a settled figure to override it, so the ledger booked the reserve on every call. Measured with unauthenticated 402 probes, no payment header: route reserved Base Solana rpc/ethereum (single) $0.0040 $0.0030 $0.0020 pm/* $0.0095 $0.0085 $0.0075 phone/lookup $0.0120 $0.0110 $0.0100 search (max_results=10) $0.2645 $0.2635 $0.2625 On Solana, the default chain since 0.46.0, the gateway charges no transaction fee at all — so that is $0.002 of invented spend per call. An agent delegated a $1.00 cap making only rpc calls was cut off after 250 of them having actually spent $0.50, and action:"report" showed $1.00. The same inflated figure is what the spend-confirmation dialog showed the human. ledgerFallback() in raw-call.ts, next to the rail switch it depends on, converts a reserve into the observed charge for the seven tools that route through it. The gate is untouched and still reserves the higher figure; the result is clamped so it can never exceed the reserve. Found by the round-2 audit's completeness critic, which noted that both prior rounds walked past it. 678 tests, typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…s first run Round 2's regressions had one fingerprint: six agents fixed 0.49.0 in parallel and each hardened the rail it was looking at. The quote guard landed on video and image but not music and speech; the in-flight booking on Base and the account rail but not Solana, the default chain; music's Solana call passed no onQuote at all, so the helper's guard hook fired against nobody. Every one was a money path and every one passed CI. So this is the table the completeness critic asked for instead of a round 3: every paid tool, every rail it serves, every treatment a paid call needs — quote guard, re-reserve at the real price, in-flight booking, honest give-up wording, ledger figure. A cell is a claim about the source, and adding a rail-specific guard without filling in its siblings turns the file red. It also asserts the DIVISION is deliberate: the seven tools whose 402 the SDK owns must NOT grow a quote guard, because they cannot see the quote, and it fails if a paid tool is missing from the table altogether. It found four gaps on its first run, all now fixed: - realface had no quote check on any rail and never re-reserved at the quoted price — it read the 402 amount and signed it five lines later. payAndPostJson takes an onQuote hook, wired on both the Base and Solana rails and to both enrollment calls; the account rail has no 402 to check and says so. - speech booked nothing when it gave up, though its own message already said a charge MAY have settled. It books conservatively now, and only when the timeout happened AFTER the signature went out — a timeout on the unpaid 402 probe charges nothing and must not invent spend. - image had no in-flight tracking, so an abort after the gateway settled left a real charge unbooked and released the reservation. 686 tests, typecheck and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
… stale claims Round-2 P3s. - getSolanaUsdcBalance took the balance query off the SDK in 0.49.0 and dropped SOLANA_RPC_HEADERS with it, so a private RPC that authenticates by header answered 401 and every balance read as "unavailable". Same parse the SDK does, same failure mode on malformed JSON. - resolveSolanaKey collapses "absent" and "the keychain would not open" into undefined, so buildSolanaClient told a user with a funded, locked wallet to run setup — advice that invites a second wallet. ensureSolanaWallet already refuses to mint on that distinction; solanaKeyUnavailableReason() exposes it so the sync callers can say the same thing. - blockrun_video and blockrun_realface told account-rail users their "wallet is out of funds" and offered a card top-up for a wallet that is not paying. Music and speech got the isApiKeyMode branch in 0.49.0; these two did not. - The Stanford demo preflight asserted a nine-tool trading profile including surf, so it now always fails; skills/rpc still routed agents to a removed tool; index.ts's own comment cited the pre-removal counts; and blockrun_markets described polymarket/wallets/profiles as a POST batch route, which 404s — GET returns the 402 (probed). Not changed: the completeness critic filed phone.ts's unknown-reserve comment as citing a route that no longer exists. /v1/phone/numbers/search answers 402 today, so the comment is accurate and the finding is refuted. 686 tests. Context figures resynced after the description edits (12,698 full). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
Round-2 P3s. - publish.yml curled `releases/latest` of a third-party binary straight into tar and then into /usr/local/bin, in the job that holds the npm publish token. Its contents could change between two runs of the same commit with nothing here recording it. Pinned to v1.8.1 and checksum-verified against the same release: pinning is the part that matters, the checksum only proves the download is intact. - solanaPaidPost parsed the settled 200 body unguarded, so a truncated or aborted payload threw AFTER the money moved and the caller reported a failure with nothing booked — the one direction that must never happen. The 200 is the settlement; the charge is handed back whether or not the body parsed. - The wallet card's primary CTA read "Buy USDC with card" on Solana, where card top-up is Base-only: the user clicked, watched "Minting link…", and landed on a plain-text refusal. It now says what the active chain can actually do. - The order card labelled the worst fill "signed max/min", which reads as a guarantee about the order about to be placed. Place re-walks a fresh book on the server, so the bound is from THIS quote and the label now says so. - blockrun_image's quote guard was untested: deleting it left every test green. Two tests now pin the refusal, the tolerance, and that image calls the guard on the one rail that surfaces a 402 amount. 686 tests, typecheck and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…is unknown The last two round-2 P3s. - sendWalletBatch had `await getRelayClient()` inside its try. That call derives CLOB credentials and creates a builder key — real network calls that happen before the RelayClient exists, so they cannot have signed or posted anything — and their failures armed the double-send guard, wedging the user behind a five-minute deadline for a transfer nothing had signed. It is hoisted out. The 4xx detector also read only the JSON shape, while the CLOB SDK's ApiError puts its code on a `.status` property and leaves the message bare, so every definite refusal it raises looked ambiguous; it now reads the property, the JSON shape, and a bare "HTTP 403" in the text. - The catalogue sweep claimed to check every FREE_CHAT_MODELS member against the live price, and could only check the ones the catalogue returns: six are absent from both gateways today, and two more were skipped by the `available === false` guard. A free member is now checked even when marked unavailable — "retired today" does not promise "still free when it returns" — and one the catalogue does not list is reported as UNVERIFIED rather than counted as checked. Delisting is not death here (gpt-oss-120b is the gateway's own free fallback and answers while absent from the catalogue), so settling one costs a real POST and stays a human's call. The comment in constants.ts that overclaimed the coverage now says exactly this. 690 tests, typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…, not just inside one call "Signed at the worst fill you saw" held within ONE call: the book walk that produced the preview also set the signed limit. But the preview and the confirm are two calls, and the confirm re-walks a fresh book — so a book that moved in between was signed at a price the card never displayed, which is the direction it moves exactly when it matters. blockrun_polymarket takes an optional max_fill_price for market orders: the worst fill the caller was shown. A walk that comes out worse is refused before anything is signed, with the two prices named and nothing charged. Buy is a ceiling, sell a floor. Absent, behaviour is unchanged and the walk stands on its own — this is a bound the caller opts into, not a new failure mode for callers who do not pass it. The order card carries its own displayed figure automatically, so the guarantee now holds for the surface that makes it. Four tests: a book that moved against the quote, one that moved in the user's favour (a better price is not a reason to refuse), the sell side's inverted comparison, and the unchanged no-bound path. 694 tests. Context figures resynced (12,767 full). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
0.49.0's thirty-seven fixes were written by six agents in parallel, and this round went looking for what that costs. It found the shape immediately: each agent had hardened the rail it was looking at. Thirty findings survived adversarial verification and NOT ONE was a P0 or a P1 — 0.49.0's own list had one of each — so the general search is spent. What is not spent is that class, so the last change here is a table rather than a fix: test/rail-parity.test.ts states which treatments every paid tool needs on every rail, and turns red when a rail-specific guard lands without its siblings. It found four more gaps on its first run. Minor, not patch: max_fill_price is a new input, the ledger now books the observed charge rather than the reserve, and the order card refuses submits it used to allow. 694 tests, typecheck, build and verify:prices all green (0 under-reserving on either chain; the catalogue sweep now names the free-list members it cannot verify instead of counting them as checked). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
… keychain The gate deciding whether to consult the OS keychain asked `existsSync`. The loaders on the far side of it -- the SDK's resolveFromFiles() and loadSolanaWallet() -- both `.trim()` the file and treat whitespace as NO KEY. So a zero-byte session file read as PRESENT to the gate and ABSENT to the loader, and the two disagreed in the one direction that costs money: the keychain was skipped, a brand new wallet was minted, and the persistKey() call immediately after overwrote the keychain entry that still held the funded key. No delete call is involved and nothing is printed. saveWallet() is a plain non-atomic writeFileSync, so an interrupted write, a full disk, a restore tool's placeholder or a stray shell redirect all leave exactly this file. Both rails now ask whether the file HOLDS a key. getChain() already asked it that way in two places, each with a comment explaining why existsSync is the wrong question; these were the two callers that had not. An unreadable file counts as present: we cannot tell whether it holds a key, and guessing toward the keychain is how a stale entry shadows a live wallet -- the loader then hits the same unreadable file and fails loudly, which is the outcome we want. Also fixes keychainDelete's platform asymmetry. It documents "true when the entry is gone, including was never there" and only macOS honoured that; LINUX_ITEM_NOT_FOUND was defined in the file and unused, so `secret-tool clear` on a miss reported the key as still in the keychain when it was not. It had zero callers, which is why nothing caught it. Tests: both empty-file cases are red before this change and green after (verified by reverting each gate in turn); the file-outranks-keychain precedence test stays green in both directions, so rotation by replacing ~/.blockrun/.session is unaffected. keychain-delete.test.ts covers both backends without spawning a real helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…to delete the live credential path The file header described the ERC-7739 wrapped L1 signature as the workaround in force and signed off with "if fixed upstream, delete this module and use client.createOrDeriveApiKey()". Both halves are now wrong, and following the second one breaks trading. The wrap was the wrong diagnosis of clob-client-v2#65: the CLOB rejects the wrapped envelope with "Invalid L1 Request headers", L2 creds are bound to the owner EOA even in POLY_1271 mode (matching rs-clob-client-v2 src/auth.rs), and both call sites pass sigType 0. The long note at the buildClobClient() call site in client.ts has said so since; the module header never caught up. And the module has meanwhile become the home of deriveApiCreds(), which every Polymarket action needs -- so deleting it on a version bump removes credential derivation entirely. Header rewritten to say what is true: the wrapped path is a tested reference implementation of the envelope, correct about the bytes and wrong about what the server wants, reachable by no caller. deriveApiCreds's own doc no longer offers sigType 3 as if it were a working alternative. Also adds test/axios-scope.test.ts. applyClobProxyOnce() sets axios.defaults.httpsAgent process-wide, which is unavoidable -- clob-client-v2 reaches for the hoisted axios itself, so there is no instance to scope. The whole safety argument is that only Polymarket shares that axios, and it lived in a comment. Now a non-Polymarket axios import turns the suite red instead of silently routing that module's traffic through an operator's POLYMARKET_CLOB_PROXY. Verified by adding an import to utils/http.ts and watching it fail. The second case pins that the Finland default is a HOST, not a proxy, so axios.defaults stays untouched unless an operator opts in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…omised to hide
Three of the four Polymarket e2e scripts state in their own doc comment that
wallet addresses and transaction ids are never printed. Each implemented that
with a different regex:
e2e-readonly 0x[hex]{40} -> <wallet> addresses only
e2e-approve 0x[hex]{40,} -> <redacted> both, unlabelled
e2e-live 0x[hex]{64} -> <tx> hashes only
e2e-withdraw 0x[hex]{64} -> <tx> hashes only
The last two are the ones that move real money, and {64} does not match a
40-hex address. withdraw.ts interpolates the bridge response into its error
text and that response carries `address.evm`, so the address printed in full.
Only the isError branch was guarded at all. A thrown exception -- a network
failure inside fetchPositions(), a viem revert -- bypassed redaction entirely
and Node printed the raw message and stack to stderr.
One redaction now, in scripts/redact.ts, longest-match-first in a single pass
so a hash cannot be half-eaten by the address rule, applied on every exit path
including uncaught throws. A 32-byte private key comes out as <tx>:
mislabelled, but not printed, which is the direction that matters.
test/scripts-redaction.test.ts covers the helper and fails if any e2e script
grows its own 0x[a-fA-F0-9]{...} regex again.
scripts/ is now in tsconfig include. These files import from src/ and were
typechecked by nothing, so a changed signature in withdrawFunds or
redeemPosition would first show up while running against a funded wallet.
Adding them surfaced no errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
`npx tsx scripts/smoke-speech.ts` registered the real blockrun_speech handler and immediately spent from the machine-global wallet at ~/.blockrun/.session. No flag, no prompt, and `limit: null` so nothing capped it. Its header said "real $0.001 speak"; the run ends with a $0.0525 sound effect, fifty times that, and about $0.054 total. This repo has already lost $0.42 to a paid handler that was run because it looked like a read. Nothing sitting in scripts/ should spend money by being run. It now refuses without --confirm (or BLOCKRUN_SMOKE_CONFIRM=1), names the real total in the refusal, and sets a $0.15 budget cap as a second backstop for a moved price or a doubled retry. Verified: a bare run exits 1 having charged nothing. test/scripts-spend-gate.test.ts pins this for the next script like it — anything in scripts/ that calls register…Tool() must carry a confirm gate ahead of its first paid call and a numeric budget limit. The assertions are static on purpose: a test that proved the gate by running the script would charge the wallet the day the gate regressed, which is the failure it exists to catch. Verified by setting limit back to null and watching it fail. The polymarket e2e scripts are deliberately out of scope and the test says so: they reach paid paths through utils/ rather than a handler, run only as explicitly named npm targets, and carry their own bounds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…out failing
Four scripts that had never been audited, each able to produce confident
output from a failure.
measure-tool-schema.mjs
- rpc() resolved on any reply with a matching id and never looked at
msg.error. Callers destructure `{ result }`, so an error reply handed them
undefined -> `result?.tools ?? []` -> a measurement of ZERO tokens across
ZERO tools, printed as a real figure at exit 0. With --svg that reaches
assets/context-cost*.svg as "0.0K tokens" and, since `cut` divides by the
total, a literal "NaN% less". A server answering initialize with an error --
the foreign-server case this tool advertises, when the package wants auth --
is all it takes. Now rejects, naming the method and the error.
- stdout was decoded per chunk (`buf += chunk` on a Buffer), so a multi-byte
character split across a boundary became U+FFFD. JSON.parse still succeeds,
so only the number comes out wrong. The payload is ~50KB against a 16KB pipe
buffer and these descriptions are full of em dashes. The in-process test
that pins these figures uses InMemoryTransport and never exercises this
reader, so it would have surfaced as an unexplainable README diff.
setEncoding("utf8").
- a profile that lists no tools is now a failure, not a measurement of zero.
stamp-server-json.mjs stamped nothing when no package entry matched pkg.name,
left the template's "0.0.0-template" (valid semver, so mcp-publisher validate
passes it), and printed "Stamped server.json → …" regardless. publish.yml
would then point PulseMCP, Glama and the rest at an npm version that does not
exist. The input was guarded against a placeholder; the output was not.
Verified by renaming the identifier and watching it exit 1.
changelog-section.mjs and measure-tool-schema.mjs both compared a realpath'd
import.meta.url against a non-realpath'd process.argv[1]. Node realpaths the
ESM main entry, so from any checkout reached through a symlink (macOS
/tmp -> /private/tmp, npm link) the CLI half silently did nothing at exit 0.
publish.yml guards on `if ! node scripts/changelog-section.mjs "$VERSION"`, so
exit 0 with empty stdout skips the generic fallback and publishes a release
with an EMPTY body -- the one thing that file's header promises cannot happen.
Verified: through a symlinked path it printed nothing and exited 0 before,
prints the section now.
sync-brand-numbers.mjs rendered remote values with String(value) and
interpolated them into src="…" and alt="…" with no escaping. Those files are
README.md, CONTRIBUTING.md and skills/*/SKILL.md, and brand-sync.yml commits
and pushes them to the default branch weekly, unattended, with contents:write.
A value carrying a quote or an angle bracket closed the attribute and injected
markup into every consuming repo; write access to the awesome-blockrun mirror
was enough to reach them. Rendered values are now checked at the point of use
(a finite number or a short plain label) and escaped on top. Checked at use
rather than over the whole artifact because the payload legitimately carries
prose fields we never render. Verified with a quote-carrying value: exit 1,
nothing written. Its --check also no longer lists stale fenced markers and
then declares everything up to date, and "keys in use" no longer counts
mcp.tools and mcp.tools@badge as two.
Documentation claims nothing was watching: README said "same 20 tools either
way" two lines under a marker rendering 19 (now the marker, so the sync keeps
it right), and docs/mcp-schema-overhead.md kept a second copy of the
profile-cost table that no test pinned. Both covered now, plus the profile
list itself, hardcoded in the script and the test, which would have left a
newly added profile measured and pinned by neither.
Found by a read-only audit subagent; every finding re-verified here before
fixing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…and call the SDK directly Two defects, one of which produces a broken tool. The dead references. `src/tools/surf.ts` went away with the tool in 0.49.0, and CONTRIBUTING still named it four times: as the template to copy in step 1 of "Adding a new MCP tool", as the reference example of the path-based pattern, and as the example for the sync payment call. Step 1 was literally uncopyable. `skills/surf/SKILL.md` was offered as the structural template for a new skill; it is now a retirement map for a removed tool, which is the one thing a new skill should not be modelled on. The one that costs money. The x402 section documented `client.getWithPaymentRaw(endpoint, params)` and `client.requestWithPaymentRaw(endpoint, body)` as how a tool makes a paid call. No tool in src/ has called those directly for some time, and for a reason raw-call.ts states in its own header: there are THREE payment rails and the SDK knows two. On the account rail requestWithPaymentRaw degrades to a plain Bearer fetch and throws away the x-blockrun-cost-usd response header, so a tool built that way silently does not support API-key users and cannot say what the call cost. utils/raw-call.ts is the single entry point that exists so no tool chooses a rail for itself -- and per-tool rail divergence is the exact fingerprint of every money bug rounds 1 through 3 found. Now documented as rawGet/rawPost with the SDK-direct path called out as wrong and the ledgerFallback() note, since the gate and the ledger are deliberately different numbers and Solana has no gateway tx fee. Also repoints the examples at src/tools/markets.ts and skills/prediction-markets/SKILL.md, both of which exist and both of which demonstrate what the surrounding sentence claims, and drops the retired surf from raw-call.ts's own list of its callers. test/doc-file-refs.test.ts is the guard: every `src|test|skills|scripts|apps| assets`-rooted path named in README, CONTRIBUTING or docs/ must exist; the raw-call guidance must stay in CONTRIBUTING; and every path-based tool must actually import raw-call and must not call the SDK raw methods directly. Verified against the pre-fix CONTRIBUTING: tests 1 and 2 both fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…found a P0 The 0.50.0 entry opened as "a second audit round" and stated that the general search was spent. Two more rounds have since landed in the same unreleased version, and round 3 found a path that destroys a funded wallet key -- so the release now contradicts its own headline. Reframed honestly: the general SWEEP was spent, and another pass over the same files would have returned docs and cosmetics. What was not spent was the surfaces no sweep had opened, and the class where a comment states a contract the code does not honour on some branch or platform. Comments cannot fail a test, which is why nothing had ever checked them and why four of round 3's five findings were that shape. Version stays 0.50.0: it is unreleased (npm latest is 0.49.0), so these are additional contents of the same release, not a new one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…what the wire said, and books what the rail bills The three OpenAI-compat chat paths and the native claude-* path now share one question on a throw — did money move? — answered from evidence, not from the words in the message: settlementOnThrow returns none/unknown/settled from the error's type, status, prefix and transport cause. Before, the account rail booked the full reserve for every refusal (a typo'd model id exhausted a delegated cap at $0 real spend — C5/C18/C23/C31), the wallet rails read a payment sent-and-never-answered as $0 and let the routing loop pay a second model (C19), and the native path booked nothing at all after a settled 524 (C20). - Solana chat streams via SolanaLLMClient.stream() — the 60s non-streaming abort on the default chain, after the SPL payment was sent, is gone. - The native path streams too (maxRetries 0): the SDK refused non-streaming thinking budgets above ~21k tokens (D51) and retried a settled 5xx with a fresh x402 payment each time (C20). - The account rail books exact usage at the model's rate, no fee, no floor (accountLedgerUsd / anthropicAccountLedgerUsd) instead of the gate reserve that ran 3-50x high (C33/D53/D58); a settled x-blockrun-cost-usd, zero included, wins when present. - served_model, finish_reason and truncated_output ride on every reply; partial text survives a mid-stream failure (D55/D57). - claude-* without `thinking` is allowed on Solana through the compat path (D5); the refusal now names the one thing the compat path cannot carry. - formatError: an account-rail 402 says "out of credit", never "run setup" (C28); a 5xx or transport failure after the payment left says the charge MAY stand instead of "try again in a few minutes" (C38); the Base-only replay-nonce reading of "Payment was rejected" is a shared hedge. - The free tier is re-swept: six of eleven routed ids answered as another model on both chains, so the tier keeps only the five that echo their own name; the rest stay $0 in FREE_CHAT_MODELS. - anthropic/claude-opus-4 gets a price row: the account sheet bills the hidden-but-served id at $15/$75 and it was reserving the $5/$30 default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ssion cannot raise, and a revoked agent keeps its ledger BLOCKRUN_BUDGET_LIMIT seeded the same mutable `budget.limit` that blockrun_wallet action:"budget" writes, so the agent it constrained could clear or raise it in one free tool call — and the denial text at the cap pointed it at exactly that tool. The env value is now sealed at wallet-tool registration: `set` clamps to it, `clear` restores it, `delegate` clamps the child cap to it, and every response says when and why it clamped. delegate replaced the Map entry and revoke deleted it while reserveBudget's closure held the old object, so a re-delegation mid-render stranded the estimate on the ledger, and revoke+delegate reset `spent` to zero — the refill 0.50.0 said it had closed. Entries are now mutated in place and revoke tombstones them for the next delegate of the same id. recordActualSpend treats an explicit 0 as a settled figure, not as "unknown": a free-priced account call was booked at the reserve and a $0.05 delegate was cut off after seven of them having spent nothing (D33/D40). action:"status" reports this session's spend and cap next to the balance — the question an agent on an allotment is actually asking. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…booleans, and every give-up is classified by fact, not by prose 0.50.0's `paidRequestInFlight` flag was copied into each media tool and every copy was wrong in its own way: speech, realface and image cleared it in a `.finally()` that runs BEFORE the rejection reaches the catch, so the booking branch was dead code the rail-parity matrix greenlit; speech set it on Base only and image's Base rail never set it; realface's was module-global, so one call's outstanding payment booked a phantom charge against another call's failure; image's Solana branch armed it before the unpaid 402 probe, so a probe timeout booked a whole render. utils/in-flight.ts is the replacement: armed only when a signed (or Bearer) request is about to leave, settled by any response, read by the catch through settleGiveUp. The async tools' catches now classify in one fixed order on all three rails — settlement observed and booked; billed at submit (BilledJobError); the wallet refused (PaymentError, by type); a paid request outstanding (the tracker); the gateway said the job failed (JobFailedError); a labelled 5xx; a timeout with nothing outstanding. Each step is something the code observed. Before, MiniMax's "The operation was aborted due to timeout" in a not-charged failure booked a render (C13/C37), a moderation "Your request was rejected" opened a Coinbase top-up page (C36), and the Solana rail booked the estimate on ANY timeout including the unpaid quote probe (C32). - solana-402: the helper reads payment_status "settled_optimistic" off the submit answer and turns every later failure on that route into a certain charge — music's Solana route settles at POST, and "No payment was taken" was a lie there. onPaidRequest/onPaidResponse hooks give the tracker exact edges; a poll answered "in_progress" no longer books at the deadline. Settle-failure reasons are split by remedy: only "insufficient" is the wallet, "expired"/"invalid signature" are the authorization (D2). A missing Solana key says whether the keychain is locked or the wallet is absent (D3/D30). Messages name their caller — music no longer says "re-running blockrun_video" (D1/D22). - music: the Solana submit timeout is the 95s the audio route needs (shared MUSIC_SUBMIT_TIMEOUT_MS), not the 30s video default that aborted every track slower than 30s after the money had moved (C15). - image: no `quality` parameter — the gateway 400s every value for every listed model, before the 402, so the zod default of "standard" was 400ing every Base generate; the account rail polls the 202 + poll_url the 30s-inline window hands back instead of reporting "No image URL" (a regression of #140); Base books the observed charge, not the reserve. - realface: action:"list" asks the ACTIVE chain's gateway with the active chain's address (sol.blockrun.ai wants base58), reading the Solana key rather than minting an EVM one; the account rail books the settled cost header instead of a 20%-high estimate. - speech: the quote guard (assertQuoteNearEstimate) reaches the Solana rail. - video/music: a completed job whose payload had no URL says the charge stands and names the claimable job, instead of "failed" with model advice that invites paying again (D13). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… answered, and three pre-payment guards that were letting paid mistakes through
The seven path-based tools (search, exa, markets, rpc, defi, phone, modal)
each did `formatError(extractErrorMessage(err))` and nothing else, so a call
whose payment left and whose origin never answered booked $0 and read "try
again in a few minutes" (C38). utils/path-tool-catch.ts books the reserve when
settlementOnThrow says "unknown" AND the status is an edge 502/504/52x or a
transport failure AND the gateway attached no uncharged marker — narrower than
chat's rule on purpose, because the search route's bare 500 is pre-settle and
booking it would invent $0.26 of phantom spend per Grok blip.
- path-safety: decode per segment, never the whole string. One malformed `%`
after `?`/`#` threw decodeURIComponent, the fallback kept the literal
`%2e%2e`, and the guard said "not `..`" while the URL parser resolved it
natively: `blockrun_modal({path:"%2e%2e/phone/numbers/buy#%"})` priced as a
$0.003 modal op and POSTed the $5.001 number purchase; the mirror image
bought a $192 H100 sandbox through blockrun_phone's namespace pin. The
query/fragment cut also failed across a line terminator (`buy?\n`).
- modal: the gateway trims `gpu` before pricing and the estimator did not, so
`" H100 "` quoted $8.001 against a $0.102 reserve (and $192 against $2.40
at 24h) — past a cap and the confirm dialog. The body is trimmed the same
way before estimating and sending, and a gpu outside the five tiers is
refused with the list instead of an opaque 400.
- search: `sources` is checked against the gateway's live enum. X/Twitter was
removed upstream on 2026-07-05 and the description advertised
["web","x","news"] for two months; following it reserved $0.26, sat through
the confirm dialog and came back "Invalid request body".
- markets: a `?` or `#` in `path` is refused before payment — a query string
in the path bypassed every params-based pre-payment rule and, on the account
rail, concatenated into `…?limit=5?status=open`. The retired markets/listings
text says 404-before-payment, which is what it does now; the bare
`kalshi/markets/KXBTC…` example that 404'd on segment count is gone.
- exa/defi: the Base-only replay-nonce reading of "Payment was rejected" is
appended, so a 30-second upstream blip stops telling a $50 wallet to top up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e way a free link could have been signed for money
- An env key (BLOCKRUN_WALLET_KEY, and the SDK's own BASE_CHAIN_WALLET_KEY,
which the gate never read) is a signer override, not a wallet this machine
owns: it is no longer mirrored into the keychain with -U, and under strict
mode the read-back no longer deletes a .session still holding the FUNDED
key. persistKey refuses to remove a file whose key is not the one it just
verified.
- Two MCP servers on a machine that has never held a key (Claude Code,
Cursor and Desktop are commonly all installed `-s user`) could both find
every store empty, both mint, and both write; the last writer won and USDC
sent to the loser's address was unrecoverable once that process exited.
The mint is published exclusively (hard link, `wx` fallback) and the loser
adopts the winner's key — on both chains.
- The chain selector's keychain probes collapsed a read ERROR (locked
keychain, unlock-dialog timeout) into "absent" and memoised it for the
process, so a funded Base-only user under strict mode was routed to Solana
and told to run setup. "unknown" is never memoised and never a Solana
signal; the Base path then fails loudly with its unlock message.
- A missing `secret-tool` binary (fresh Ubuntu/Debian/WSL/Docker) reached the
Linux branch as `{status: null, error: ENOENT}` and was reported as a
keychain ERROR, so no wallet could be created on either chain with a message
blaming BLOCKRUN_KEYCHAIN=strict on a machine that never set it. A keychain
that does not exist is "unavailable", never a fault.
- The onramp's $0 x402 quote was a comment and a test fixture; the code
signed whatever the 402 said. It now refuses anything but a literal zero.
- The strict-mode refusal describes the state it found (an empty file, a
missing file) instead of a mode the user may never have set, and says a
backup restore is a fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e confirm is held to the preview, and the spend dialog reaches the trades - executeTrade: a submit that threw with no 4xx behind it (dropped socket, client timeout, relay 502/504) released the reservation and rendered as a plain error, steering the agent into a second real order while the session cap saw one. The notional now stays booked as unconfirmed, the message says what to check before retrying, and the balance-cache retry never re-submits on top of a possibly-live order. The order card keeps its lock across Re-quote and tells the model. - fund: the signed EIP-3009 authorization stays executable for 300s, and a lost response, 5xx or success:false read as "Funding failed" — the retry signed a second full transfer. A definite 4xx clears; anything else arms a pendingFund guard that refuses to re-sign until the window has passed. The $0.01 gateway fee is reserved and booked against the x402 budget like any paid call. - Market orders: the preview records its worst fill per (token, side) and a bare confirm:true is held to it — the documented preview → quote → confirm flow never carried max_fill_price forward, so "signed at the worst fill you saw" only held inside one call. - BLOCKRUN_CONFIRM_SPEND=on now asks the human before buy/sell/fund/withdraw sign, with the real notional; a $0.004 rpc call got the dialog while a $25 bet did not. confirm:true stays the model-supplied floor. - setup: a blocked egress printed "❌ Region: BLOCKED" and, two lines later, "🎯 Ready to trade" with ready:true — the field an agent keys on before funding. ready now requires the region check to pass. - relayer: the SDK's console.log progress lines went to stdout, the JSON-RPC channel, mid-money-operation; they are routed to stderr for the duration of every SDK call. Its private axios instance now gets the underscore-header bridge and POLYMARKET_CLOB_PROXY agent, so a relayed setup actually relays. - One isDefiniteRejection for the CLOB submit and the relayer batch, so the two money paths cannot drift on the question again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nothing in scripts/ moves money by being run - publish.yml refuses a package.json version BELOW npm latest (npm publish does not compare semver — a typo'd 0.5.1 would have become `latest` and downgraded every npx user), and the tag/release step is gated on the tag being ABSENT rather than on `pkg != npm`: a job that went red after the npm publish could never be repaired by a re-run, because the re-run found npm == pkg and skipped the step green. - brand-sync.yml: the direct push always fails on a protected main and GITHUB_TOKEN cannot open PRs here, and `|| echo` swallowed that too — the run went green having landed nothing, and a stale count sat in the README for 26 days behind a green badge. A drift the job cannot land is now a red run, from one fixed force-pushed branch. - verify:prices exits 2 when it could not look (too many `?` rows, an unreadable catalogue) instead of 0 — the day the gateway renames a header field, every row prints `?` and the release gate must not go green. The account rail's public pricing sheet is swept as a third catalogue (its other routes cannot be quoted without a key that would be charged, and the header says so). A probe row the CLIENT refuses (seedance-2.5@1080p) is marked as expecting an unpaid 4xx, now that blockrun PR #353 has deployed and the gateway refuses it too. - The live Polymarket e2e scripts require --confirm (or POLYMARKET_E2E_CONFIRM=1) before signing anything; every script installs the redacted exit handler so a thrown RPC error cannot print the wallet address the header promises to hide; the read-only preflight reports an outage as an error instead of `positions: []`. - smoke-speech prints the charge it computes from the estimator instead of a typed figure that understated it. - sync-brand-numbers.mjs documents why this copy is ahead of the source, so the next "copy the source over the consumer" remediation does not erase the render guard again (#84, #128). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e suite, and stop tracking a node_modules symlink - test/_setup.ts is preloaded into every test child (`--import`): ten mocked-handler suites never mocked utils/auth.js, and on a machine with ~/.blockrun/.api-key they ran their Base-rail assertions on the account rail. test/harness-pin.test.ts asserts the pin took effect. - A `node_modules` symlink pointing at itself was tracked in git and shipped in every clone; `git rm --cached` and a .gitignore that matches the symlink as well as the directory. test/repo-hygiene.test.ts keeps it out. - VERSION (0.41.1) was nine minors behind package.json; the two must agree, and test/version-file.test.ts pins them. - doc-file-refs scans every tracked markdown file and every relative link, not a hand-kept list of three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… less Description edits in this round moved every profile; the schema-tokens test pins the README rows, the overhead doc and the SVG cards to the live measurement (full 13,006 · media 5,790 · trading 5,411 · research 2,752 · chat 2,079). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…it escape is billed, and speech says when the charge stands Audit round 4, on round 3's in-flight tracker. A helper wrapped in sendPaid can inspect the response and THROW on it, so settle() was never reached and the catch classified the error by its prose — isTimeoutError's substring match — and image's account rail booked a whole render for a not_charged job whose upstream text read "aborted due to timeout", telling the user the charge MAY have gone through (the C13 shape, on the rail whose sibling tools had just documented why it must not happen). - in-flight: the verdict comes off the error, never its message. A numeric status (the SDK's APIError, AccountApiError, the Anthropic SDK), a typed job verdict (JobFailedError / BilledJobError) or the gateway's own uncharged marker is an ANSWER. isExplicitlyUncharged moves to a leaf module (utils/uncharged.ts) so in-flight stays linkable under the suites that mock utils/auth.js. - api-key-call: a not_charged terminal failure is a JobFailedError, and a 202 with no poll_url is the BilledJobError the callers' comments claimed it already was. - image: the account rail is not wrapped — video and music never were. - image, speech, realface: the Solana tracker is armed at the helper's onPaidRequest and settled at onPaidResponse, not at onQuote (a signing failure no longer reads as "may have settled"), and solanaPaidPost puts the status on the error it throws for a non-2xx answer — realface reads it instead of a regex that turned any "402" in a quote fault into "out of funds" plus a top-up page. - solana-402: on a settled-at-submit route EVERY post-submit escape is a BilledJobError — the reactive re-sign path could still throw "No charge was made" or a bare RPC error for a track the gateway had settled at POST. - speech: the charge-stands step video and music got in round 3 — a settled 200 whose body is unusable says so, on all three rails, instead of "failed" plus retry advice that pays again. - rail-parity: a new cell — the paid request ANSWERED 5xx with "timeout" in the body — on Solana and the account rail, for every manual-402 tool. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ds on that agent's ledger, and the fund fee reaches the ledger at all revoke → (the in-flight call settles) → delegate was still a per-agent refill: recordSpending looked the id up in the live map, found nothing, credited the global ledger only, and the next delegate carried a ledger that had forgotten the call. The tombstone is now credited too, and action:"report" lists revoked agents with their kept spend — the description promised "its spend is kept", and a tombstone nobody could read was not kept. mcp-handler never passed the ledger to registerPolymarketTool, so fund's $0.01 gateway fee was reserved and booked only in the unit test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t.key ranks below the keychain, and a keychain replacement is announced - Linux: `secret-tool lookup` exits 1 for a MISS and for a FAULT alike (libsecret returns 1 whether value == NULL or error != NULL); only the fault prints why. Round 3's tri-state read was macOS-only — a locked collection, a missing D-Bus session, a dismissed unlock prompt all read as "absent", and under strict mode both provisioners minted over the funded keychain wallet. A silent exit 1 is a miss; one that said something is an error, with the tool's own reason in the detail. - The SDK loader reads .session THEN the legacy wallet.key, and the EVM gate asked the loader — so once strict mode retired .session, a stale legacy file outranked the keychain and its key was stored over the funded one with -U. .session stays the rotation seam; wallet.key is consulted only after the keychain says "absent". - getChain()'s Base-wallet guard treated an unreadable .session as "no Base wallet" while ensureEvmWallet treated the same file as present; it now counts as present, and the Base path fails loudly on it. - publishMintedKey: an empty placeholder is CLAIMED (renamed aside, then the exclusive link retried) rather than replaced by a plain rename two losers could both make; the published file is read back and is what the process signs with. - persistKey announces on stderr, with the address, when it replaces a DIFFERENT key in the keychain — the mirror follows the file by design, but a rotation nobody meant is now visible the run it happens. - getSolanaUsdcBalance honours SOLANA_RPC_API_KEY as x-api-key, the SDK's other spelling for a keyed private RPC. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he counted amount Both wallet clients record the settlement on the paid retry's 2xx and only then read the body, so a non-JSON 200 surfaces as a bare SyntaxError — no status, no transport words, "none" to settlementOnThrow — for a call the SDK's own counter says was paid. rawGet/rawPost read the counter around the call and hand the delta to the catch as RawCallSettledError; pathToolFailure books it as a certain charge and says the charge stands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and the version gate refuses it `npm view ... || echo none` spelled every failure as "none", which both the gate and the npm step's `pkg != npm` accept — so a registry blip on a run whose package.json was BELOW latest, the exact case the gate was written for, would have published and downgraded `latest`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w and the project headers said there was The gateway removed it on 2026-07-05 and blockrun_search refuses it before payment since round 3; the search skill still taught `sources: ["x"]` with a worked example and "defaults to all three". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Supersedes #145 (this branch contains every
fix/audit-r2commit plus rounds three and four).settlementOnThrownone/unknown/settled, account rail books exact usage); one per-call in-flight tracker replaces five hand-rolledpaidRequestInFlightbooleans; the seven path tools book a payment the origin never answered;BLOCKRUN_BUDGET_LIMITis a ceiling the session cannot raise; four ways a funded key could be lost or shadowed are closed; Polymarket unknown-outcome handling,pendingFundguard, preview-bound confirm, and the spend dialog on trades; release gates that fail when they cannot verify.isAnswer), every settled-at-submit escape is billed,secret-toolexit 1 read by stderr (the P0: Linux strict mode minted over a funded keychain wallet), the legacywallet.keyranks below the keychain, revoked agents keep their ledger, the fund fee actually reaches the ledger,RawCallSettledError, and the publish gate's E404/unknown split.verify:pricesfixes:anthropic/claude-opus-4price row ($15/$75 on the account sheet, was reserving the $5/$30 default); theseedance-2.5@1080pprobe markedexpectRefusednow that blockrun #353 has deployed.--profile trading58% less.Full notes:
CHANGELOG.md§ 0.51.0.Test plan
npm run typechecknpm test— 1273/1273npm run buildnpm run verify:prices— exit 0 (0 under-reserving, 0 unreachable, catalogue sweep clean)initialize→tools/list= 19 toolsgh run watchthe publish run; verify npm, MCP registry, tagv0.51.0, GitHub release; one coldnpx -y @blockrun/mcp@latest🤖 Generated with Claude Code