Skip to content

feat: pay by BlockRun API key alongside the USDC wallet - #157

Merged
VickyXAI merged 4 commits into
mainfrom
feat/api-key-auth
Sep 5, 2026
Merged

VickyXAI merged 4 commits into
mainfrom
feat/api-key-auth

Conversation

@VickyXAI

@VickyXAI VickyXAI commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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

Wallet API key
Host sol.blockrun.ai/api / blockrun.ai/api api.blockrun.ai (no /api)
Auth x402 PAYMENT-SIGNATURE Authorization: Bearer brk_...
Settlement On-chain, per call Prepaid credit

A bearer key on the x402 hosts is ignored and still 402s; api.blockrun.ai 401s 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_endpoint are 404 on the x402 hosts too — dead references in src/, 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: paidUsd is only assigned inside the 402 branch, and exa.ts returned early on if (!settled). So 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. franklin stats marks estimated totals with ~ and reports the estimated portion separately; franklin balance refuses 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-usd response 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 canonical Authorization. A plain object held both, fetch sent 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 and franklin logs. Added.

Verification

  • 711 CLI tests + the desktop suite pass (23 new tests).
  • Live against the gateway with a real key: chat, Exa, Surf, and the payment proxy in key mode.
  • Live in wallet mode: chat settles exactly as before ($0.004046 recorded from the x402 amount).
  • Surf on the Solana wallet path fails with verification_failed — verified to reproduce identically on main, 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

1bcMax and others added 4 commits September 5, 2026 01:34
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
@VickyXAI

VickyXAI commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: a live API key was committed here and must be rotated

Commit e9611ef on this branch contained a real brk_live_ BlockRun API key in test/api-key.local.mjs. It was pushed to this public repository. e51674b replaces it with a synthetic value, but the key is still reachable in this branch's history and in this PR's timeline, so scrubbing the file does not undo the exposure.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01LZQ4mfQVs2JJhC2ALfyGjt

@VickyXAI
VickyXAI merged commit 96cb84d into main Sep 5, 2026
6 checks passed
@VickyXAI
VickyXAI deleted the feat/api-key-auth branch September 5, 2026 16:07
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