Repository navigation
feat: pay by BlockRun API key alongside the USDC wallet - #157
Merged
Merged
Conversation
Probes against the live gateways established that api.blockrun.ai is a separate, prepaid-credit gateway that shares no auth with the x402 hosts: a bearer key is ignored on blockrun.ai/api, and api.blockrun.ai 401s with no x402 fallback. That isolation is what lets API-key mode ship without touching wallet users. All 33 gateway paths Franklin calls route correctly under the key. The blocking gap is accounting: the key gateway reports no charge amount, and Franklin derives its entire ledger from the 402 handshake, so every paid call would record $0 and silently disable --max-spend, PreSpend hooks and budget ceilings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZQ4mfQVs2JJhC2ALfyGjt
Franklin could only pay one way: sign an x402 payment from a local USDC wallet. That gates the funnel behind "make a wallet, get USDC, fund it". BlockRun already runs a prepaid-credit gateway at api.blockrun.ai that authenticates with a bearer key, so this wires it up as a second payment mode rather than a replacement. The two gateways share no auth — a bearer key is ignored on blockrun.ai/api and still 402s, and api.blockrun.ai 401s with no x402 fallback — which is what lets key mode ship without touching wallet users. With no key set, resolvePayMode() returns byte-identical hosts and headers to before; the test asserts that per chain rather than leaving it to inspection. The blocking problem was accounting, not routing. The key gateway settles silently and returns no charge amount, while Franklin derives its entire ledger from the 402 handshake — `paidUsd` is only ever assigned inside the 402 branch, and exa.ts returned early on `if (!settled)`. Every paid call in key mode would have recorded $0, silently disabling --max-spend, PreSpend hooks and every budget in the product. Paid calls are now priced from BlockRun's published rate card at /.well-known/x402, and each row is tagged exact vs estimated so stats never overstate its precision. The estimate leans ~5% + $0.001/call high because that card is the x402 one; that is the safe direction for a ceiling and is disclosed, not hidden. Live-testing the payment proxy surfaced a real bug: Node lowercases the client's forwarded `authorization` while gatewayHeaders() returns the canonical casing, so fetch sent two Authorization headers and the gateway read the client's — every proxied call 401'd. applyGatewayAuth() now strips every case variant first. Also adds `brk_` to the secret redactor, which had no pattern for it, so a key could otherwise reach transcripts and `franklin logs`. Verified live against the gateway: chat, Exa, Surf and the proxy in key mode; chat and the wallet path unchanged in wallet mode. 711 tests pass. Not done: an editable key field in the desktop panel. The desktop RPC is read-only for credentials by design and adding a secret write path is a bigger security call than this change should make. The panel does now say when spend is coming from a key instead of presenting the USDC balance as the budget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZQ4mfQVs2JJhC2ALfyGjt
The previous commit put a real `brk_live_` key in test/api-key.local.mjs and pushed it to a public repo. The key is shaped-checked and masked by the tests, so a synthetic value serves them exactly as well; there was never a reason to use a working credential. Rewriting the earlier commit was not possible, and would not have helped much either — a force-pushed commit stays reachable from the pull request timeline. The exposed key must be rotated; scrubbing the file only stops it spreading further. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZQ4mfQVs2JJhC2ALfyGjt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZQ4mfQVs2JJhC2ALfyGjt
Contributor
Author
Heads-up: a live API key was committed here and must be rotatedCommit Required action: revoke that key at user.blockrun.ai and issue a new one. The test never needed a working credential — it only checks the key's shape and that masking hides it. It now uses 🤖 Generated with Claude Code |
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.
Adds a second way to fund Franklin: a prepaid BlockRun API key, next to the existing USDC wallet. Solana leads Base everywhere in the docs and CLI copy, and the README now says where to sign up and top up.
Design doc:
docs/plans/2026-09-05-api-key-auth-design.md(probed facts, not assumptions).The two gateways share no auth
sol.blockrun.ai/api/blockrun.ai/apiapi.blockrun.ai(no/api)PAYMENT-SIGNATUREAuthorization: Bearer brk_...A bearer key on the x402 hosts is ignored and still 402s;
api.blockrun.ai401s with no x402 fallback. That isolation is what lets this ship without touching wallet users — with no key set,resolvePayMode()returns the same host and headers as before, asserted per chain by test rather than by inspection.All 33 gateway paths Franklin calls were probed with a live key and route correctly. The four that return
unsupported_endpointare 404 on the x402 hosts too — dead references insrc/, left for a separate cleanup.The real problem was accounting, not routing
The key gateway settles silently and returns no charge amount. Franklin derives its whole ledger from the 402 handshake:
paidUsdis only assigned inside the 402 branch, andexa.tsreturned early onif (!settled). So every paid call in key mode would have recorded $0, silently disabling--max-spend,PreSpendhooks, and every budget in the product.Paid calls are now priced from BlockRun's published rate card at
/.well-known/x402, and each row is tagged exact vs estimated.franklin statsmarks estimated totals with~and reports the estimated portion separately;franklin balancerefuses to invent a credit balance it cannot read and links the dashboard instead.Known bias: that rate card is the x402 one, which carries a 5% margin and $0.001/call that key mode does not charge. Franklin therefore reads a few percent high. That is the safe direction for a spend ceiling and it is disclosed in the README, not hidden. A
x-blockrun-charged-usdresponse header on the gateway would remove the estimate entirely — see §7 of the design doc.A bug found by live-testing the proxy
Node lowercases the proxied client's
authorization;gatewayHeaders()returns the canonicalAuthorization. A plain object held both,fetchsent two Authorization headers, and the gateway read the client's — every proxied call 401'd.applyGatewayAuth()strips every case variant first. Regression test included.Also
brk_had no entry in the secret redactor, so a key could reach transcripts andfranklin logs. Added.Verification
verification_failed— verified to reproduce identically onmain, so it predates this branch. Flagged in §11, not fixed here.Not done
An editable API-key field in the desktop panel. The desktop RPC is read-only for credentials by design and adding a secret write path is a bigger security decision than this change should make alone. The panel does now state when spend is coming from a key instead of presenting the USDC balance as the budget. Keys are set with
franklin login.Still needs a human
Dashboard reconciliation. The gateway exposes no key-scoped balance or usage endpoint, so Franklin's tally cannot be automatically checked against
user.blockrun.ai. §6 has the manual procedure and the expected drift.🤖 Generated with Claude Code
https://claude.ai/code/session_01LZQ4mfQVs2JJhC2ALfyGjt