Skip to content

0.49.0 — the error says whether money moved, and thirty-seven audit findings (fixes #132) - #143

Merged
VickyXAI merged 21 commits into
mainfrom
fix/issue-132-not-charged-wording
Sep 9, 2026
Merged

VickyXAI merged 21 commits into
mainfrom
fix/issue-132-not-charged-wording

Conversation

@VickyXAI

@VickyXAI VickyXAI commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #132. Grew into the release audit: 21 commits, 90 files. Unblocks #142 (the org brand-numbers refresh, red because this repo still shipped 20 tools).

Summary

The #132 fix (three layers). The gateway's (payment NOT charged) was dropped by the SDK sanitizer and again by this repo's extractor. @blockrun/llm 3.15.1 (blockrun-llm-ts#39) keeps it under detail; this repo reads it. A 501 is "not served", not "temporary". sports/* failures say the routes are degraded upstream since 2026-08-04 and that nothing was charged (only when the gateway's own evidence says so). Equity price/history answers up front: withdrawn 2026-09-05, nothing charged, catalog still free.

Quote guard. sol.blockrun.ai is a separate deployment that lags Base and quotes azure/sora-2 as Seedance 2.0 at $1.135 (2.7x). blockrun_video (both rails) and blockrun_image (Solana) refuse, unsigned, any 402 more than 1.5x above the estimate the model was shown. verify:prices reports a differing product label at a materially dearer price as a gateway bug.

From the audit — ten finders, one adversarial verifier per finding, 37 confirmed, all fixed:

  • P0 wallet: strict-keychain blockrun_wallet status minted a second Solana key over the funded one and deleted the file. Keychain-aware ensureSolanaWallet, refuses to mint on a failed read, no memoised miss, fresh-install provisioning, address-based Solana balance.
  • P1 phone: blockrun_phone was a root passthrough (modal/sandbox/create at a $0.012 reserve). Namespace pinned to phone/* and voice/* on the normalized route.
  • P1 Surf → tool removed: every Surf endpoint 410s since 2026-09-06. First a pre-payment retirement notice, then the tool itself: the server now ships 19 tools, matching mcp.tools: 19 in the published brand artifact (which is why the org's brand refresh, chore(brand): refresh the snapshot — the markers were rendering a stale catalog #142, was red against this repo). trading is 8 tools, research 5; the surf skill is a redirect map. Also from the same finding: the 0.48.1 sports remedy pointed at routes that 404 and now points at ones that 402.
  • P1 account rail: async media jobs billed at submit were abandoned on a dropped poll and told to "try again". BilledJobError carries cost and job id; both tools book it and never advise retry; Base poll loops survive a transient rejection; music and realface book before validating payloads.
  • P1 chat pricing: gpt-6-astra and claude-fable-5.1 ($10/$50) had no table row (2x under-reserve). Rows added; verify:prices sweeps both live catalogues; claude-sonnet-5 ledger corrected to $2/$10; free models are a set, not a vendor prefix.
  • Polymarket (P2): market orders signed at the previewed worst fill; withdraw validates to_address and labels the destination honestly; lost relayer submits stay tracked with anti-retry guidance; signer rotation resets deployed; optional POLYMARKET_MAX_FUND_USD; bridge errors no longer rewritten as CLOB advice; the order card refuses a stale amount.
  • Tests as a spend hazard (P2): image-cost.test.ts left the account rail unmocked and could POST with a real key; rail pinned. Confirm-spend guard now classifies paid files by import and holds all of them to reserve+confirm.
  • Startup (critic): the key-leak scanner branded the documented mcpServers.*.env.BLOCKRUN_WALLET_KEY override as a compromised key on every launch; now a note, with Cursor/Windsurf configs scanned. Unknown --profile is logged; update nudge no longer says claude mcp add; blockrun_dex validates the token path segment; skills install --help exits 0; CI runs Node 20.19 and 22.

Full prose in CHANGELOG.md under ## 0.49.0 (minor, not patch: the market-order limit, phone refusal, Surf removal and quote guard are behaviour changes; a config naming blockrun_surf now gets an unknown-tool error).

Verification

  • npm run typecheck clean; npm run build clean
  • npm test: 644 pass, 0 fail (was 462 on main)
  • npm run verify:prices: exit 0 — 0 under-reserved on either chain; new catalogue sweep: 83 Solana / 78 Base chat models, 0 would settle above the reserve; Sora/Solana substitution reported as a gateway bug
  • Live in-process calls against the gateway for the bug: price and markets routes return 502 in live MCP calls #132 paths and the Sora quote refusal
  • Context-cost table and card resynced after the tool drop: 12,657 tokens (12.7K, 6% of 200K), trading profile 59% less
  • brand-numbers.json refreshed from the live artifact (78 chat / 102 total / 6 free, tools 19)

Not in this PR

  • Gateway-side: redeploy sol.blockrun.ai so it knows azure/sora-2; Predexon sports/* remains degraded upstream
  • 22 unverified P3 audit items (docs drift, speech/music Base-rail re-reserve, model-cache invalidation on chain switch, leak-scanner paths for undocumented clients) are queued for the next audit round
  • docs/mcp-schema-overhead.md is a dated 2026-09-01 snapshot and now differs from the README table

Documentation

README.md troubleshooting and tools table, skills/blockrun-debug symptom rows, skills/surf (now a redirect skill), skills/crypto-data, skills/prediction-markets, skills/gentech-blockrun, skills/phone, skills/polymarket-trading, docs/polymarket-trading-setup.md, server.template.json, .claude-plugin/marketplace.json.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg

VickyXAI and others added 11 commits September 8, 2026 18:04
…ansient outage

@blockrun/llm 3.15.1 carries the gateway's own message — the field that names
the cause and says '(payment NOT charged)' — under `detail`. extractErrorMessage
only read message/hint, so the text was dropped a second time here and the
'not charged' branch never fired (#132).

A 501 is the gateway declining to serve a route, not a blip; the advice to
retry in a few minutes was wrong. The 'nothing was charged' claim is made only
when the 501 arrived before payment. hasLabelledServerStatus is exported so
route-specific formatters share one status rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…charged

All four Predexon sports/* routes have returned an upstream 500 since
2026-08-04. The gateway marks them degraded, withdrew them from discovery and
releases the payment nonce on upstream 5xx; this server still advertised them
as live and rendered the failure as 'after payment … try again' (#132).

The description now says the routes are degraded and points at markets +
league=. A sports 5xx renders the outage, the date and 'nothing was charged'.
No pre-payment block: the gateway decides whether Predexon has recovered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
Since 2026-09-05 the gateway returns a pre-payment 501 for every
stocks/{market}/price|history call — 'We do not currently serve equity
prices'. On the default Solana chain the Base-only guard fired first and told
the user to switch chains to pay for a route that cannot succeed (#132).

Paid stock calls now return the gateway's own answer up front: withdrawn on
2026-09-05, nothing charged, the ticker catalog is still free, contact for
coverage. The description stops promising equity quotes. The paid-tool
confirmation table drops the stock row with a note on how to re-add it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…-cost table

README, four skills and the plugin manifest still sold paid stock quotes and the
sports/* Predexon routes. Both now read as what the gateway serves. The
context-cost profile table is updated to the measured totals after the
description edits (12,992 full / 5,606 trading).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
Bump @blockrun/llm to ^3.15.1 (the SDK half of #132) and package version to
0.48.1. Also ships the three fixes that landed on main after 0.48.0: image
account-rail settled cost (#140), OpenClaw verification and DeepSeek repricing
(#131), brand numbers (#141).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
README troubleshooting and the blockrun-debug skill now carry the two
issue-132 symptoms as symptom → cause → fix: the equity 501 from
blockrun_price (withdrawn 2026-09-05, nothing charged, catalog still free)
and the sports/* upstream outage behind blockrun_markets (degraded since
2026-08-04, payment released; older builds rendered it as "API error after
payment: 502"). A third row covers the generic 501 "not served" wording and
when it can and cannot claim nothing was charged. The debug skill's
description and triggers pick up the new error strings.

The cost-reporting table drops "paid blockrun_price" — no paid price path
remains — and states blockrun_image's account-rail cost as settled (#140),
with the estimate only as the fallback when no figure is returned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…gning

verify:prices found the Solana gateway — a separate deployment that can lag
Base — does not know azure/sora-2 and quotes 'Seedance 2.0 Pro video
generation (5s)' at $1.135480 in its place, 2.7x the published Sora rate for a
different model. The only check on the 402 price was the budget cap, which
lets that through on any wallet holding $2.

assertQuoteNearEstimate (budget.ts) refuses, unsigned, a quote more than 1.5x
and $0.02 above the estimate the model was shown. Wired into blockrun_video on
both rails and blockrun_image on Solana; the Solana helper now passes the
decoded 402 to onQuote so callers can name WHAT was quoted. Messages end with
'no charge was made' and say how to proceed (for Sora: switch to Base).

Two Solana tests that used a 4x quote to exercise the re-reserve now use a
1.27x quote — the 4x case is the thing being refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…, not an under-reserve

The 402 says what is being sold (Base under resource.description, Solana under
accepts[0].extra.description). When the two gateways name different products
for the same request, reserving the substitute's price would legitimise the
substitution; since the tools now refuse such a quote before signing, it is
reported loudly as a gateway bug and does not fail the release gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
CHANGELOG paragraph for the refused-quote behaviour; a symptom → cause → fix
row in the debug skill; README profile totals resynced (13,023 full / 5,558
media). The full profile crossed 13,000 tokens, which rounds the badge's
share of a 200K window from 6% to 7%, so the card is regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…s also materially dearer

The Solana gateway writes longer descriptions for the same route (rpc/ethereum:
40 words against Base's 5, same $0.002), which the first cut read as a
substituted product. Substitution now requires the label to differ AND the
Solana price to sit >25% above Base; the per-row tag says whether the tool is
guarded (video/image) or merely reserves the Base figure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
@VickyXAI VickyXAI changed the title 0.48.1 — the error says whether money moved (fixes #132) 0.48.1 — the error says whether money moved, and the tool refuses a quote it was not told about (fixes #132) Sep 9, 2026
VickyXAI and others added 9 commits September 8, 2026 20:07
…nt over a key we could not read

ensureBothWallets called the SDK's file-only getOrCreateSolanaWallet(). Under
BLOCKRUN_KEYCHAIN=strict the .solana-session file is retired once the key is in
the keychain, so the default blockrun_wallet status call saw an empty slate and
minted keypair B; the next resolveSolanaKey() mirrored B over the funded key A
with -U and deleted the file. A was then nowhere (audit 2026-09-08, P0).

ensureSolanaWallet() is the twin of ensureEvmWallet(): env > file > keychain,
via keychainRead so a FAILED read refuses to mint instead of orphaning the key.
resolveSolanaKey() no longer memoises a miss, so a wallet provisioned later in
the process (or by another process) is visible without a restart.

Fresh install: Solana has been the default since 0.46.0 but nothing on that path
minted a wallet — getWalletInfo() went through the SDK constructor, which threw
'Private key required' for status/setup/qr/deposit. getWalletInfo() now
provisions via ensureSolanaWallet(), and buildSolanaClient() names the remedy
instead of the symptom when there is still no key.

getChainBalance('solana', address) queries the DISPLAYED address (the SDK's
getBalance only ever read the client's own wallet), and returns null, not $0,
when the RPC is unreachable.

price.ts: the equity pre-flight now runs before the market-required check, and
the dead Base-only chain guard behind it is removed with a note on re-enabling.

The keychain module header no longer claims protection against same-user code;
it protects the key at rest. A read-before-write guard in persistKey was tried
and dropped: replacing the key file IS the documented rotation path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…uard, prove the price pre-flight at the handler

image-cost.test.ts mocked the paid client but blockrun_image picks its rail from
the account key before asking for that client, so on a machine set up for
account mode the suite posted to the gateway with the real key. HOME is now a
temp dir with no key before the tool loads and the shared fetch helper is a trap.

The confirm-spend static guard keyed on reserveBudget and skipped any file
without one; it now classifies paid files by their imports and requires both a
reservation and a confirm, with blockrun_image in the behavioural table.

blockrun_price gets a handler-level test: equity price/history return the 501
text before the chain guard, the budget gate and the confirm dialog; stocks list
and crypto price still reach the client.

Also: a BLOCKRUN_BUDGET_LIMIT that does not parse now warns on stderr that the
cap is OFF; the image-edit confirm label names the realpath of every local file
about to leave the machine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…date withdraw destinations, track a lost relayer submit

Audit 2026-09-08, seven findings on the paths that move real USDC:

- Market orders were signed at an SDK-chosen limit with no user-visible bound.
  walkBook() walks the live asks/bids best-first; the preview prints the worst
  fill and that price is the signed limit, so a thinning book fills less, never
  worse. Sell proceeds are the walked total. A FOK the book cannot absorb is
  refused pre-sign.
- withdraw: to_address is checked with viem isAddress({strict:true}) before any
  I/O; the confirmation says 'your agent wallet' only when it is.
- setup: a signer/deposit-wallet rotation no longer inherits deployed:true.
- relayer: a lost submit response persists pendingWithdraw with anti-retry
  guidance; a definite 4xx or an untracked batch rethrows raw.
- fund: optional POLYMARKET_MAX_FUND_USD cap; the doc no longer claims every
  action is capped.
- withdraw/redeem: bridge and transport errors are reported as themselves, not
  rewritten into CLOB geoblock / market-resolved advice.
- MCP Apps order card: Place is disabled once the amount differs from the quote
  until Re-quote; the card shows the worst-fill row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…t hears about it; sports remedy, prices and manifest corrected

The gateway retired every Surf endpoint on 2026-09-06 (410 endpoint_retired,
no 402 is ever issued). blockrun_surf now returns the retirement and the
gateway's own alternatives before estimate, reservation or confirm dialog; the
tool stays registered, its description shrinks, and README plus four skills stop
selling 83 endpoints at $0.0095. skills/surf is a redirect skill.

0.48.1's own sports remedy pointed at markets+league and outcomes/:id, which the
gateway removed on 2026-08-04 and 404s before payment; all eight surfaces now
point at markets/search, polymarket/events and kalshi/markets, which 402 today.
describeDegradedSportsFailure claims 'nothing was charged' only when the message
carries the gateway's own evidence, and otherwise says to check the ledger.

markets/exa/defi descriptions quoted the reserve as the charge and rpc quoted
the base; all four now say base plus the gateway fee ($0.001 today, $0.002
reserved). server.template.json no longer tells Solana users to put a bs58 key
in BLOCKRUN_WALLET_KEY; SOLANA_WALLET_KEY and BLOCKRUN_API_KEY are listed.
The plugin manifest's surf entry and skills/blockrun's 'generated prices' claim
are corrected to match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…atalogue, and say when a settled call failed

Audit 2026-09-08. CHAT_PRICE_PER_MTOKEN claimed to hold every model above the
$5/$30 default; openai/gpt-6-astra and anthropic/claude-fable-5.1 ($10/$50 on
both gateways) had no row, so an explicit call reserved half of what settled.
Rows added, and verify:prices now sweeps GET /api/v1/models on both gateways:
it fails on an unpriced model above default, a row below the live rate, a
FREE_CHAT_MODELS member that costs money, or an Anthropic ledger row above
live — which is how claude-sonnet-5 drops from $3/$15 to its real $2/$10.

thinking.budget_tokens is folded into the reserve only for Claude ids.
anthropicCallCost resolves the gateway's dashed echoes (claude-fable-5-1,
claude-haiku-4-5-20251001) to their catalogue key with hasOwn, never a sibling's
rate by prefix. A chat call that settled and then failed mid-stream says so on
the explicit-model, multi-turn and routing paths, via a helper that runs
formatError on the bare error first so the settled note cannot earn 'needs
funding'. formatError appends 'nothing was charged' when the gateway said so,
and the #132 regression test now pins that sentence. FREE_CHAT_MODELS replaces
the nvidia/ prefix test as the free classifier.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…ide; unknown profiles, the update nudge, dex paths, skills --help, CI on Node 20

Completeness-critic findings, each confirmed by reading before changing:

- warnOnLeakedKeys flagged mcpServers.*.env.BLOCKRUN_WALLET_KEY / SOLANA_WALLET_KEY
  — the override README, server.template.json and the setup skill document —
  as a compromised key on every launch. That location now gets a short
  plaintext-and-synced note; the banner is reserved for a key in a header, an
  args entry or the old hosted-auth field. Findings carry kind: leak |
  env-override. The array branch never checked string elements, so a raw key
  in args was invisible; fixed. Cursor and Windsurf config paths the README
  documents are scanned, Windows paths come from %APPDATA%.
- An unknown --profile / BLOCKRUN_MCP_PROFILE silently exposed all 20 tools;
  it is logged with the known names, and the name is trimmed and lower-cased.
- The update nudge told installed users to run a claude mcp add that refuses
  an existing name; it now says restart (npx re-resolves @latest), or remove
  then re-add if pinned.
- blockrun_dex spliced the caller's token into the URL path unencoded; it is
  validated as an address shape (EVM, base58, TON, Sui) and refused otherwise.
- skills install --help exited 2; explicit help exits 0, unknown subcommand 2.
- ci.yml runs typecheck, build, the suite and --version on Node 20.19 and 22;
  a 'test' gate job keeps the branch-protection check name stable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…ver called free

Audit 2026-09-08, seven findings on the async media rails:

- api-key-call.ts: the poll fetch is wrapped and continues inside the deadline
  (as solana-402.ts already did), 429/502/503/504/522/524 keep polling with
  Retry-After bounded by the deadline; every give-up after a successful submit
  throws BilledJobError carrying the settled cost, the job id and a billing
  status (billed / not_charged / unknown). A submit that aborts says the job MAY
  have been billed; one that provably never connected is rethrown raw.
- video.ts / music.ts: the catch tests BilledJobError before isTimeoutError,
  books err.paidUsd against the cap, names the job and the dashboard, and never
  says 'please try again'. Base poll loops continue on a transient rejection
  instead of abandoning a 9-minute paid job. A poll aborted while carrying the
  payment header may still settle server-side: say so and book conservatively.
  The descriptions stop promising 'if we give up, you are not charged'.
- music.ts books a completed poll before validating the payload (video's
  0.39.1 fix); the inline 200 books unconditionally.
- realface.ts books the settled 2xx before checking asset_id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
… budget

Every other passthrough tool concatenates onto a fixed prefix; blockrun_phone's
prefix was /v1/ itself, so no traversal was needed: path:"modal/sandbox/create"
with an H100 body ran at phone's $0.012 unknown reserve, clearing any budget cap
and showing a confirm dialog 16,000x too small. The normalized route (decoded,
lower-cased, query dropped) is classified, so encoded or cased spellings of
in-namespace routes still pass and no spelling of an out-of-namespace one does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
…indings

The #132 fix grew into a full ten-angle audit with an adversarial verifier per
finding; every survivor is fixed in the commits above and written up under
'From the audit' in the release notes. Minor, not patch: market orders are now
signed at the previewed worst fill, blockrun_phone refuses off-namespace paths,
Surf is retired, and the quote guard refuses a 402 far above the published
rate — behaviour changes, not just hardening.

README context-cost table and card resynced to the measured 12,883 tokens
(Surf's description shrank more than the others grew): badge 12.9K, 6% of a
200K window, trading profile 58% less.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
@VickyXAI VickyXAI changed the title 0.48.1 — the error says whether money moved, and the tool refuses a quote it was not told about (fixes #132) 0.49.0 — the error says whether money moved, and thirty-seven audit findings (fixes #132) Sep 9, 2026
…and artifact

The gateway retired Surf on 2026-09-06; every /v1/surf/* path answers 410
endpoint_retired and no 402 is ever issued. Earlier in this release the tool was
made to answer with the retirement before reserving budget. That was the interim:
a tool that can only return an error still costs every agent its schema on every
turn, and blockrun's published brand artifact has read mcp.tools = 19 since the
delisting, which is why the org's brand-numbers refresh (#142) was red against
this repo.

Removed the tool, its registration, its profile memberships, its price probes and
its two test files; the 'trading' profile is now 8 tools and 'research' 5. The
surf SKILL stays as a redirect map from each former endpoint to the tool that
serves it, and names what has no replacement yet. The path-safety tests keep the
traversal exploit they were written for and run it against a namespace that still
exists, with the history noted.

brand-numbers.json refreshed from the live artifact (78 chat / 102 total / 6 free,
tools 19). Measured context cost drops 12,883 -> 12,657 tokens (badge 12.7K, 6% of
a 200K window); trading profile 5,160, now 59% less than full.

BREAKING: a client config that still names blockrun_surf gets an unknown-tool
error from its MCP client. Nothing can be charged either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNqnahSKcBqQemPn5TaMLg
@VickyXAI
VickyXAI merged commit c091b8d into main Sep 9, 2026
3 checks passed
@VickyXAI
VickyXAI deleted the fix/issue-132-not-charged-wording branch September 9, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: price and markets routes return 502 in live MCP calls

1 participant