36 DUNES
ServicesWorkBlogAboutClientsRequest a proposal
← All posts
BUILD LOG

Building Alpaca Explorer: Asking the Market a Question Instead of Reading a Dashboard

A chat interface over live Alpaca market data, with Claude running a read-only tool-use loop instead of a wall of tickers. The architecture, the "unusual options activity" heuristic, and what I'd build next.

Gold candlestick chart cresting into a network of connected nodes on a dark navy background

Most retail market tools are dashboards: screeners, a dozen preset filters, a chart you have to know how to read. I wanted something closer to how I actually think about the market — ask a specific question, in plain English, and get back the answer plus the data behind it. "Give me unusual call activity in the top traded stocks." "How's my watchlist doing, chart the moves." "Which of today's biggest gainers have heavy put buying?"

That's Alpaca Explorer: a chat UI backed by Claude running a tool-use loop over Alpaca's market-data API. One constraint shaped everything else — it's read-only. Claude can look at snapshots, bars, options chains, and news. It cannot place an order. The agent has no tool that touches the trading endpoints at all, so there's no prompt-injection or bad-judgment path that ends in a trade I didn't make.

The shape of it: a chat UI and an agent that narrates itself

The frontend is a Next.js chat interface; the backend is a small Express API with one real route, POST /api/ask. That route doesn't wait for Claude to finish and hand back JSON — it opens a Server-Sent-Events stream and pushes events as they happen: text deltas as the answer is written, a tool_call event the moment Claude decides to check something, a tool_result event with timing (and the error, if it failed) when that call returns.

The point of streaming the tool calls, not just the final text, is that the UI can show its work: a small step list that fills in live — "get_most_active_stocks · 420ms", then "scan_unusual_options · 1.8s" — so a question that takes six or seven seconds doesn't feel like a frozen spinner. It also makes the agent debuggable from the browser console alone; I didn't need server log access to see why an answer was wrong, just the step list that produced it.

Eight read-only tools, one model, fifteen rounds of back-and-forth

The agent runs on Claude Opus 5 through the SDK's toolRunner, with eight tools wrapping specific Alpaca endpoints: most-active screeners, market movers, stock snapshots and bars, an options-flow scanner, a single-name option chain, news, and account/clock lookups so Claude knows whether it's looking at today's session or Friday's close. A ninth tool, show_chart, doesn't call Alpaca at all — it just hands a chart spec back to the UI, which renders it inline exactly where Claude called it, so a chart shows up mid-answer instead of bolted on at the end.

A few settings in that call matter more than they look. The loop is capped at 15 tool round-trips, which is generous for research questions but still a hard ceiling against an agent that keeps rescanning instead of answering. Tool inputs stream as they're generated (eager_input_streaming) rather than arriving all at once, which is part of why the step list in the UI feels immediate instead of laggy. Thinking runs in adaptive mode, so Claude spends more of it on "which 12 of these 20 most-active names are actually real equities, not leveraged ETFs" and less on questions that don't need it. And the call opts into Anthropic's server-side fallback beta, so a safety decline on one model retries automatically on the recommended fallback instead of just failing the request.

The system prompt does real work too, not just tone-setting. It tells Claude to resolve vague universes itself — "top traded stocks" means call get_most_active_stocks, then actually pick roughly 10–15 real names out of the result, skipping leveraged and inverse products unless asked — and to scan all chosen symbols in one options call rather than one call per symbol, which is the difference between a three-second answer and a twenty-second one.

Defining "unusual": the options-flow heuristic

"Unusual options activity" isn't a field Alpaca returns — it's a rule I had to decide on. A contract counts as flagged when its day volume is at least 1.5x open interest, that volume is at least 500 contracts, the implied premium is at least $50,000, and it expires within 45 days. Same-day (0DTE) contracts are excluded by default, because their volume dwarfs open interest as a matter of course — that ratio is close to meaningless on expiration day itself.

Premium isn't in the API response either; it's volume × VWAP × 100, which tracks what was actually paid across the day's trades better than volume × the last print does, since one stale quote shouldn't swing a $50K/$49K threshold. All of these thresholds are just defaults — they're parameters on the tool, and the system prompt tells Claude to loosen them and rescan when a search comes back empty rather than report a false negative as "nothing unusual today."

Watchlist, saved prompts, and a 9:45am email

The watchlist bar under the header is just localStorage on the client, sent along with every question so "how's my watchlist doing" resolves without Claude asking what that means. Saved prompts work the same way — star a question after asking it, and it shows up on the start screen next time. Neither needed a database; the app has no accounts at all.

There's also a standalone script, report.js, that runs the same agent once against a fixed prompt — unusual calls, unusual puts, watchlist, today's movers and the news behind them — and emails the result through Resend. On Render it's a cron job, and the scheduling has a small gotcha worth mentioning: Render cron expressions run in UTC, and 9:45am ET is either 13:45 or 14:45 UTC depending on daylight saving, so the job is scheduled at both times and the script itself checks the current Eastern hour and exits immediately unless it's really 9am ET. Cheaper than maintaining two separate cron entries that get swapped twice a year, and it can't drift out of sync with itself.

What's actually working, and what I'd change next

The part that works well: natural-language questions reliably turn into the right sequence of tool calls, the answers read like a person wrote them — a finding up top, a compact table, a couple of notes on what stands out — and Claude is honest about the data under it, unprompted, when the caveats matter (more on that below).

The list of things I'd improve, roughly in order:

  • The free-tier data feed is the real ceiling. On Alpaca's free plan, stock data is IEX-only — a few percent of consolidated volume — and options run on the 15-minute-delayed "indicative" feed. Claude already states this caveat when it's relevant, which is the honest short-term fix. The real fix is flipping ALPACA_STOCK_FEED=sip and ALPACA_OPTIONS_FEED=opra with an Algo Trader Plus subscription — the code already reads both env vars, I just haven't paid for the upgrade yet.
  • There's no evaluation harness for the heuristic itself. I chose 1.5x volume/OI and $50K premium because they're reasonable starting points, not because I've checked whether flagged contracts actually predict a move. The honest next step is logging every flag with a timestamp and checking forward returns a day and a week out — the same discipline I built into Cellar Index's feature set, applied to a rule instead of a model.
  • History doesn't survive a refresh. Conversation history is capped to the last 10 turns and lives only in the browser tab that asked. There's no server-side session, so "what did we find out yesterday" doesn't work from a different device, or even the same device after closing the tab.
  • Auth is one shared password, not accounts. APP_PASSWORD keeps a public Render URL from burning API credits on strangers, which is fine for a tool one person uses. It's not a real auth model, so this stays a personal tool rather than something I'd hand to a client as-is.
  • Charts are bar or line only. Price action sometimes wants a candlestick, and right now Claude narrates that in prose instead — a sentence doing a chart's job.
  • No caching layer. Two people (or two browser tabs) asking about the same ticker inside a minute hit Alpaca twice. A short-lived cache in front of the snapshot and bars endpoints would cut real rate-limit risk for free.

None of that is a knock on the thing as it stands today — it answers real questions correctly, fast, and tells me what it doesn't know. It's just not a public product the way Cellar Index is; it's a personal research tool running on Render's free tier, gated behind a password because every question it answers costs real API credits.

The best thing a tool like this can say is "here's what I found, and here's why you shouldn't fully trust the feed it came from."
START A PROJECT

Have live data you'd rather question than dashboard-ize?

Tell us what you're staring at. We'll tell you honestly whether an agent over it is the right call, or just the trendy one.