AI for personal finance — Grok ↔ Binance through Fly + Bright Data, locked behind a passkey
I want to ask Grok "what's my USDT?" from my phone, get a real number back, and have nobody-but-me ever be able to ask the same thing of the same MCP. Three layers, each solving exactly one problem: WebAuthn at the door, Cloudflare for the public surface, a small Fly proxy through Bright Data at the egress so a country-blocked exchange thinks it's hearing from a perfectly normal Argentine residential IP.
What it does, today
The MCP at money.ask-meridian.uk is a one-user vault. Tools live behind OAuth 2.1 + PKCE; OAuth itself is gated by a WebAuthn passkey tap. The current tool surface is intentionally read-only and small — balance and portfolio queries — because the trust model has to be solid before signing transactions ever lives upstream of the chat box.
Tomorrow's tools (planned, not yet wired): get_mp_balance() for MercadoPago, get_coinbase_holdings(), eventually prepare_withdraw_usdc() that drafts a withdrawal a human still has to confirm. The architecture below is the foundation those build on.
The stack
Each box solves one problem; none of them try to do more.
Layer 1 — WebAuthn at the door
A finance MCP can't have a shared secret you paste into Grok. The chat-side LLM is a black box; whatever you give it, you've given to whoever runs the LLM. The trust boundary has to be a browser session I control, with a key the LLM literally can't see.
The bootstrap is a one-time dance:
-
Mint a registration link — I POST to
/admin/create-registration-linkwith myX-Admin-Secretheader. Returns a URL likemoney.ask-meridian.uk/register/<random-token>valid for one hour. - Register the passkey — I open that URL on the device that should own the key, the browser prompts for biometric, the public key is stored in KV, and the registration link self-destructs (KV entry deleted).
-
Wire up Grok — paste the MCP URL into Grok's connector settings. Grok discovers OAuth via
.well-known/oauth-authorization-server, redirects me to/authorize, which renders a single passkey login page. One Touch ID tap, OAuth code emitted, Grok exchanges it for a bearer.
After that, daily use is just chat. Grok holds the bearer (TTL 7 days), I never see another passkey prompt unless I revoke. The MCP enforces single-tenant: one USER_ID in env, all challenges keyed to that user, registration endpoint refuses additional passkeys after the first one is bound.
All of this is implemented with @simplewebauthn/server on the worker — there's no CF-specific WebAuthn primitive, just the standard library running in the worker runtime via nodejs_compat. Passkey storage is one KV entry per credential, ~250 bytes each.
Layer 2 — the Cloudflare Worker
The worker is the public face. Its responsibilities are exactly:
- OAuth 2.1 + PKCE issuer (mirrors the Meridian and pharmacy MCP shape).
- WebAuthn registration + login endpoints.
- The MCP
/mcpJSON-RPC endpoint with bearer validation. - Tool dispatch — small switch on tool name, calls into per-exchange clients.
It deliberately doesn't talk to Binance directly. Two reasons. First, Binance geo-blocks Argentina at the API level — requests from CF edge IPs in LATAM get a CloudFront 451 long before reaching anything useful. Second, even if I worked around that, every CF edge has a different IP — Binance's "whitelist this IP for this API key" policy needs one fixed IP forever. CF Workers' egress IPs aren't fixed.
So the worker delegates egress to layer 3.
Layer 3 — the Fly proxy through Bright Data
binance-proxy is ~120 lines of Node.js running on Fly.io. It does two things: forward signed requests from the worker to Binance, and route every outbound connection through a Bright Data static-residential proxy with sticky sessions. The result: every request to api.binance.com arrives from the same Argentine residential IP — 89.32.132.226, in my case — which is whitelisted on my Binance API key.
import { HttpsProxyAgent } from "https-proxy-agent"
const BD_URL = `http://${BD_USER}:${BD_PASS}@brd.superproxy.io:33335`
const agent = new HttpsProxyAgent(BD_URL)
// Forwards GET /fapi/v1/klines etc. through the BD agent
const r = await fetch("https://api.binance.com" + req.url, {
method: req.method,
headers: filterHeaders(req.headers), // strip Fly-* and X-Forwarded-*
body: req.body,
agent,
})
The filterHeaders step is critical and easy to miss. Fly inserts Fly-* and X-Forwarded-* headers into every request — leak those through to Binance and CloudFront blocks the whole connection with a 403. The fix is an allowlist: x-mbx-apikey, content-type, content-length, user-agent. Anything else, drop.
Bright Data's "sticky session" is the bit that gives us a single IP forever. The proxy username carries a session id (brd-customer-...-zone-...-session-<id>); BD pins a single egress IP to that session for as long as you keep using it. Whitelist that one IP on the Binance key and you're set.
HMAC signing in the worker
Binance's authenticated endpoints want an HMAC-SHA256 signature over the query string, with the API secret as the key. CF Workers don't have Node's crypto module, but they do have Web Crypto, which is enough:
async function hmacSha256Hex(secret: string, message: string): Promise<string> {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
)
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message))
return [...new Uint8Array(sig)]
.map(b => b.toString(16).padStart(2, "0"))
.join("")
}
The signed query is appended as &signature=..., the request goes through the Fly proxy, Binance verifies the HMAC against the API key, and you get JSON back.
What it doesn't do (yet)
- Trading actions. Read-only is the safe place to start. Order placement is gated behind a "draft, then human-confirms" pattern:
prepare_market_buy(...)returns a structured plan with the calculated size, fees, and slippage band; a separateconfirm_order(plan_id)tool requires a passkey-fresh OAuth bearer to actually execute. - MercadoPago integration. MercadoPago's API uses access tokens that don't have geo issues, but do need a different sliver of OAuth dance for first-time setup. Adding their balance endpoint is half a day of work, mostly token-refresh logic.
- Coinbase / Kraken / etc. Same shape as Binance — exchange client class, route through the proxy if needed (Coinbase doesn't geo-block AR), HMAC where applicable.
Why three layers instead of two
You could imagine a simpler stack: Grok talks to a Fly worker directly, the Fly worker handles auth + Bright Data + Binance. One process. Less plumbing.
The reason I split is that each of the three layers has a different operational profile. The worker is hot, free, scales horizontally, has KV and is on the global edge — ideal for the auth-heavy public surface. The Fly proxy needs an attached static IP and a long-lived TCP connection to Bright Data — Workers can't currently do CONNECT-based proxy chaining cleanly. Bright Data only sells in their own product. Putting all three in one process means the worker is paying CF Workers' constraints on outbound proxy support that it doesn't need to pay.
And honestly: the boundary at the worker / Fly seam is the place where I want to be able to swap implementations. Bright Data not delivering? Try Smartproxy, or a self-managed VPS in AR. The split lets me swap layer 3 without touching auth or tools.
Costs
| Layer | Service | Monthly |
|---|---|---|
| Worker + KV + custom domain | Cloudflare | $0 (free tier) |
| Fly proxy | Fly.io 1× shared-cpu-1x, 256MB | ~$0–2 (free trial covers it) |
| Bright Data static residential | BD's smallest tier | ~$15 |
| Domain (ask-meridian.uk) | Cloudflare Registrar | ~$0.80 |
~$16/mo for a single-user finance MCP across two exchanges. Substantially less than I'd spend on a single trade with Binance's sloppier UI.
Source
- github.com/LuuOW/finance-mcp — worker, OAuth, WebAuthn, Binance client
- github.com/LuuOW/binance-proxy — Fly + Bright Data bridge
- Earlier post on the OAuth + connector flow — same auth shape as this MCP
Up next: the actual get_balance tool surface, plus the design notes on the "draft then confirm" pattern for trading actions. The architecture is in place; what's left is wiring tools onto it carefully.