Skip to content

feat: an API key is a way in, and Solana is the default chain (0.10.0) - #33

Merged
VickyXAI merged 2 commits into
mainfrom
feat/api-key-rail-solana-default
Sep 5, 2026
Merged

feat: an API key is a way in, and Solana is the default chain (0.10.0)#33
VickyXAI merged 2 commits into
mainfrom
feat/api-key-rail-solana-default

Conversation

@VickyXAI

@VickyXAI VickyXAI commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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 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.

What changed

New _apikey module — 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, because api.blockrun.ai returns the gateway's body verbatim and the shapes already match.

api_key carries either credential. The brk_ prefix picks the account rail; anything else is a wallet key, exactly as before. No wallet key format begins with brk_, so existing callers cannot be misrouted — pinned by test_a_wallet_key_is_not_an_api_key.

Coverage on the new rail: chat (incl. streaming), /v1/messages, /v1/responses, images (generations + edits, with the image2image path rewrite), 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_source: "blockrun_account". cost_usd is null on this rail, deliberately: there is no per-call on-chain charge and there never will be. Tagging those rows litellm_estimate would 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 / --chain on the sidecar, plus BLOCKRUN_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 own Authorization (the local BLOCKRUN_PROXY_TOKEN guard) 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_base argument → BLOCKRUN_API_URLBLOCKRUN_CHAIN → the credential probe. BLOCKRUN_CHAIN=solana on 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) and test_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.ai and both gateways, then reconciled against the account metering ledger:

  • Provider mode + sidecar: chat, streaming, /v1/messages, /v1/responses, images, speech — all 200, native system_fingerprint preserved (fp_5b6abc5941).
  • Wallet rail unaffected: Solana (implicit default) and Base (BLOCKRUN_CHAIN=base) both returned real settled charges ($0.001 / $0.002).
  • Billing correct: image billed exactly $0.052500 (list), speech $0.000998, chat at token × list price. Failed calls billed nothing (exact_usd: null, paid_usd_self: 0).
  • Native Gemini returns 501 with the explanation, and costs no round trip.

🤖 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:

  1. The chain the CLI recorded was ignored. ~/.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-chain solana, older .chain base).
  2. An explicitly passed wallet key now picks its own chain from its format — a hex key is not base58, so routing it to the other signer cannot work.

Three more taken from it: Retry-After passthrough 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.py imports blockrun_llm.api_key.resolve_api_auth, which exists in no released SDK — it needs blockrun-llm#58, itself still open, and ships a requirements-api-preview.txt git ref. blockrun-litellm cannot go to PyPI in that state. This branch adds no dependency. #32's auth= design is cleaner and worth revisiting once #58 releases.

Tests now 399 passed, 1 skipped; both rails live re-verified after the rework.

1bcMax and others added 2 commits September 5, 2026 10:58
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
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.

1 participant