feat: an API key is a way in, and Solana is the default chain (0.10.0) - #33
Merged
Conversation
The x402 wallet is the right primitive for an agent — it can hold one, and it needs no account. It is the wrong first step for a person evaluating this package for an afternoon, who now has to fund a Solana wallet before the first "hello". That was the wall, and it is the reason most people who found this repo never made a call from it. So: set BLOCKRUN_API_KEY=brk_live_... (issued at user.blockrun.ai, topped up by card) and every surface routes to api.blockrun.ai with a Bearer header. No wallet, no private key, no chain, no [solana] extra, no USDC, no gas. The wallet rail is untouched and stays the default when no key is present. One seam, not two implementations. The rail is chosen once, in _adapter, and everything above it — the LiteLLM provider, the sidecar's routes, the media job manager, the audit log — is unchanged, because the account API returns the gateway's body verbatim and the shapes already match. api_key on a litellm.completion() call now carries either credential: the brk_ prefix picks the account rail, and no wallet key format begins with one, so existing callers cannot be misrouted. Covered on the new rail: chat (incl. streaming), /v1/messages, /v1/responses, images (generations + edits), video submit/poll/download, speech, music, sound effects, /v1/models. The one gap is the native Gemini protocol, which api.blockrun.ai does not publish — the sidecar answers 501 naming the wallet rail and the /v1/chat/completions route to the same models, rather than letting a bare 404 come back from a host that never served it. cost_usd is null on this rail, deliberately. There is no per-call on-chain charge and there never will be; the new cost_source "blockrun_account" says the authoritative figure lives in the account ledger, where tagging it litellm_estimate would have claimed a real number existed and was missed. Solana also becomes the default chain for the wallet rail. It settles in about a second for a fraction of the fee, so it is where a caller who never made a choice should land. A host holding only a Base credential keeps using Base with a one-line warning: a hex key cannot be parsed by the SVM signer, so flipping it silently would have turned an upgrade into an outage. Anything explicit still wins, BLOCKRUN_CHAIN=solana on a Base-only host included — once a choice is made, quietly serving the other chain would be a lie about which chain moved money. Verified live against api.blockrun.ai and both gateways, and reconciled against the account ledger: chat, streaming, Anthropic messages, responses, images and speech all bill at exact list price; failed calls bill nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Je8JRrGK7mifyGbf8VjRM
…ecorded #32 shipped the same feature independently and caught two things this branch had wrong. Credit to @KillerQueen-Z. The real bug: the BlockRun CLI records the chain a user picked interactively in ~/.blockrun/payment-chain (legacy: ~/.blockrun/.chain), and this branch ignored it. Someone who ran the setup flow and chose Base had already answered the question the new Solana default asks — silently moving them is exactly the breakage the compatibility branch exists to prevent. Both files are read now, current name first. That order is not hypothetical: both exist on the machine this was developed on and DISAGREE, payment-chain saying solana and the older .chain saying base. An explicitly passed wallet key now picks its own chain from its format too. A hex key is not base58, so the key on the call is a better answer than anything on disk — routing it to the other chain's signer cannot work. Three more, same source: - Retry-After survives the hop. A 429 carries the wait time every stock OpenAI and Anthropic client schedules its backoff from; dropping it turned a client that would have waited into one that spins. - A malformed key raises instead of falling through to the wallet rail. The caller asked for the account rail, so a silent fallback either 402s for a wallet they do not have or spends from one they did not mean to touch. Passing both credentials at once is refused for the same reason: no reading is obviously right and the two spend different money. - Wallet keys are hashed out of the client-cache keys, which are dict keys and so surfaced in reprs, KeyErrors and anything dumping locals on a crash. The test suite's chain probe now stubs the chain files as well as the session files. It had to: this machine's .chain flipped a test the moment the feature landed, which is the same laptop-vs-CI divergence the session stub already existed to stop. 399 passed. Live re-verified on both rails after the rework. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Je8JRrGK7mifyGbf8VjRM
This was referenced Sep 5, 2026
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.
Why
The x402 wallet is the right primitive for an agent — it can hold one, and it needs no account. It is the wrong first step for a person evaluating this package for an afternoon, who currently has to fund a Solana wallet before the first
hello. That was the wall.BLOCKRUN_API_KEY=brk_live_…(issued at user.blockrun.ai, topped up by card) now routes every surface toapi.blockrun.aiwith a Bearer header. No wallet, no private key, no chain, no[solana]extra, no USDC, no gas. The wallet rail is untouched and stays the default when no key is present.What changed
New
_apikeymodule — one seam, not two implementations. The rail is chosen once in_adapter; everything above it (the LiteLLM provider, the sidecar's routes, the video job manager, the audit log) is unchanged, becauseapi.blockrun.aireturns the gateway's body verbatim and the shapes already match.api_keycarries either credential. Thebrk_prefix picks the account rail; anything else is a wallet key, exactly as before. No wallet key format begins withbrk_, so existing callers cannot be misrouted — pinned bytest_a_wallet_key_is_not_an_api_key.Coverage on the new rail: chat (incl. streaming),
/v1/messages,/v1/responses, images (generations + edits, with theimage2imagepath rewrite), video submit/poll/download, speech, music, sound effects,/v1/models. The one gap is the native Gemini protocol, whichapi.blockrun.aidoes not publish — the sidecar answers 501 naming the wallet rail and the/v1/chat/completionsroute to the same models, rather than letting a bare 404 come back from a host that never served it.cost_source: "blockrun_account".cost_usdisnullon this rail, deliberately: there is no per-call on-chain charge and there never will be. Tagging those rowslitellm_estimatewould have claimed a real number existed and was missed; the new value says the authoritative figure lives in the account ledger. A reconciliation job can finally tell "no charge exists here" apart from "we failed to read the charge".--api-key/--chainon the sidecar, plusBLOCKRUN_API_KEY,BLOCKRUN_API_BASE_URL,BLOCKRUN_CHAIN. Startup fails fast on the rail it is actually configured for — the old check demanded a wallet unconditionally, which an API-key deployment does not have. The client's ownAuthorization(the localBLOCKRUN_PROXY_TOKENguard) is stripped before forwarding, never leaked upstream.Behaviour change: Solana is now the default chain
An unconfigured host previously fell through to Base. Solana settles in about a second for a fraction of the fee, so it is where a caller who never made a choice should land.
This does not break an existing Base deployment. With nothing explicit set and only a Base credential on the host (
BLOCKRUN_WALLET_KEY,BASE_CHAIN_WALLET_KEY, or~/.blockrun/.session), the adapter keeps using Base and logs a one-line warning. A hex Base key cannot be parsed by the SVM signer, so flipping such a host silently would have turned an upgrade into an outage.Precedence:
api_url/api_baseargument →BLOCKRUN_API_URL→BLOCKRUN_CHAIN→ the credential probe.BLOCKRUN_CHAIN=solanaon a Base-only host is honoured and fails with "no Solana wallet" — once a choice is made, quietly serving the other chain would be a lie about which chain moved money.Tests
376 passed, 1 skipped. Two new files —test_api_key_rail.py(39 tests: resolution, chat, streaming, error mapping, media, proxy routes, audit rows) andtest_chain_default.py(15 tests pinning both halves of the flip). The conftest now points the wallet probe at a directory that does not exist, so the suite resolves the same default on a laptop as in CI.Four existing tests spelled "Base" as the absence of a chain setting; they now say
BLOCKRUN_CHAIN=base, which is what they always meant.Verified live
Against
api.blockrun.aiand both gateways, then reconciled against the account metering ledger:/v1/messages,/v1/responses, images, speech — all 200, nativesystem_fingerprintpreserved (fp_5b6abc5941).BLOCKRUN_CHAIN=base) both returned real settled charges ($0.001 / $0.002).exact_usd: null,paid_usd_self: 0).🤖 Generated with Claude Code
https://claude.ai/code/session_014Je8JRrGK7mifyGbf8VjRM
Update: #32 folded in
#32 shipped this feature independently — my fault for not checking open PRs before starting. I read both and folded its wins in here (7b40e4d); credit to @KillerQueen-Z in the CHANGELOG and commit. Full reasoning in my comment on #32.
Two real bugs it caught in this branch:
~/.blockrun/payment-chain/.chain— someone who ran the interactive setup and chose Base had already answered the question the new Solana default asks, and this branch silently overrode them. Exactly the breakage the compatibility branch exists to prevent. Now read, current name first, and the order is pinned by a test because both files exist on my dev machine and disagree (payment-chainsolana, older.chainbase).Three more taken from it:
Retry-Afterpassthrough on 429 (stock OpenAI/Anthropic clients schedule backoff off it); raising on a malformed key instead of falling through to the wallet rail, and refusing both credentials at once; hashing the wallet key out of the client-cache keys, which are dict keys and so surfaced in reprs and tracebacks.Why this PR is the base rather than #32: its
_auth.pyimportsblockrun_llm.api_key.resolve_api_auth, which exists in no released SDK — it needs blockrun-llm#58, itself still open, and ships arequirements-api-preview.txtgit ref. blockrun-litellm cannot go to PyPI in that state. This branch adds no dependency. #32'sauth=design is cleaner and worth revisiting once #58 releases.Tests now 399 passed, 1 skipped; both rails live re-verified after the rework.