Nine AI agents race nine pharmacies in parallel to find your cheapest prescription price — OCR your script, live-stream every scout, and cache the winner in Postgres.
Built with:
react · vite · node.js · express · server-sent-events · postgresql · ghost-postgres · guild-ai · tinyfish-sdk · openai-gpt-4o-mini · multer · pg
Inspiration
Filling a single prescription in the US is a weirdly adversarial experience. The same generic drug can cost \$8 at Cost Plus, \$42 at CVS, and \$180 at Walgreens — and the only way to know is to open nine tabs, fight through captchas, and try to find a price buried behind a login wall on each pharmacy's site.
We wanted to crush that nine-tab tax into a single button press. Agents are great at browsing messy web UIs, and the expensive part of comparison shopping is exactly the thing LLMs-plus-browsers are built for: read the page, find the number, ignore the upsell.
So we built RxScout: point an agent at each pharmacy, run them in parallel, rank by cash price, done.
What it does
- Input — type a drug (e.g.
atorvastatin 20mg), or upload a photo of your prescription. - Parse — if an image was uploaded, the
rx-ocrGuild agent extracts the medication text, and therx-parseragent turns it into a structured query. - Fan out — up to 9 pharmacy scout agents launch in parallel (Cost Plus, GoodRx, Walmart, Costco, CVS, Walgreens, Kroger, Rite Aid, and more). Each agent gets a pharmacy-specific prompt with program hints (e.g. Walmart's \$4 generics, Costco's non-member price, GoodRx coupon ranking).
- Stream — every agent pushes live progress events over SSE. The UI shows a card per pharmacy with a streaming browser view, status chips, and per-agent elapsed time.
- Rank — results are sorted by price; the winner is revealed with a dramatic "best price" animation.
- Cache — results are persisted in a Ghost-provisioned Postgres database (
scout_runs+scout_cachewith a 60-minute TTL) so repeat queries for the same drug/ZIP skip the live scrape.
How we built it
Architecture
┌───────────────┐ SSE ┌──────────────────┐
│ React + Vite │ ◄──────────────┤ Express │
│ (rxscout/) │ │ orchestrator │
└───────────────┘ │ (server/) │
└──┬───────────────┘
│ fan-out (Promise.all)
┌───────────────┼────────────────┬──────────────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐
│ TinyFish │ │ Guild: │ │ Guild: │ │ Ghost │
│ browser │ │ rx-ocr │ │ rx-parser │ │ Postgres │
│ × N │ │ (photo→tx) │ │ (tx→med) │ │ (cache) │
└──────────┘ └────────────┘ └────────────┘ └──────────┘
Orchestration — Guild AI + Node fan-out
We scrapped an earlier LangGraph supervisor in favor of a simpler pattern: Node owns the fan-out, and each "agent" is a local Guild AI agent (server/agents/pharmacy-scout, rx-ocr, rx-parser). The server spawns guild agent chat --ephemeral --mode json per invocation and parses the JSON reply — no orchestration framework, no graph, just Promise.all over the pharmacy list.
Two scrape modes are selectable from the UI:
tinyfish— Node calls the TinyFish SDK directly with a structured-extraction goal. Single browser run per pharmacy, structured JSON back. Fast.experimental-fetch— Thepharmacy-scoutGuild agent fetches the page itself withguildTools. Slower but useful when we want the agent to reason about the page.
Streaming UX
Every orchestrator event (plan, pharmacy_started, pharmacy_stream, pharmacy_progress, pharmacy_done, ranked, done) is forwarded as SSE. The React client renders each pharmacy as a live card; TinyFish's streaming browser URL is embedded so you can literally watch the agent type into the pharmacy's search box.
Persistence — Ghost Postgres
Ghost provisioned a dedicated Postgres instance in seconds. Two tables:
scout_runs— every invocation (drug, ZIP, ranked JSON, best price, best pharmacy, timestamp).scout_cache— per-pharmacy payload withexpires_at = NOW() + 60 min. On a cache hit the orchestrator skips TinyFish entirely and the UI badges the result ascached.
Prescription OCR
If the user uploads a prescription photo, it's base64-embedded into the prompt for the rx-ocr agent (GPT-4o-mini via Guild). The extracted text is then fed to rx-parser, which returns a normalized medication query (atorvastatin + 20mg + 30 tablets).
Challenges we ran into
- Pharmacy anti-bot defenses. Walmart, Costco, CVS, and Walgreens flat-out refuse datacenter IPs. We solved it by marking those pharmacies with a
browserProfile: 'stealth'+ US residentialproxyConfigin the catalog, so TinyFish routes them through a stealth browser. - Agents echoing their inputs. Early on, the
pharmacy-scoutagent would sometimes just reflect the request payload back at us when it couldn't browse. We added anlooksLikeInputEcho()guard inorchestrator.jsthat detects the input-shape and surfaces a cleannot_foundstatus instead of a fake result. - Ghost Postgres +
sslmodeconflict. Ghost's connection string ships with?sslmode=require, butnode-postgreswanted us to configure SSL via thesslobject. We stripsslmodefrom the URL indb.jsand setssl: { rejectUnauthorized: false }explicitly. - Stale cache eating live updates. Our first cache implementation cached across every run, which meant the first scrape result haunted demos forever. We made
CACHE_ENABLEDopt-in (off by default) and added acachedbadge so it's obvious when a result came from Postgres vs. a fresh scrape. - Stale dev server port.
node --watchoccasionally leaves a zombie process holding:8788. Fixed by killing the stale PID; still worth alsof -ti:8788 | xargs kill -9one-liner in your muscle memory.
Accomplishments we're proud of
- Nine agents, one button. Watching nine pharmacy browsers all stream side-by-side is genuinely fun, and the price spread between them is a small lesson in US healthcare economics every time you run it.
- Sub-second cache hits. With the Ghost cache warm, repeat queries for the same drug/ZIP return in <200ms — you go from "nine agents scraping the internet" to "one SQL query" with no UI code change.
- Prescription-photo pipeline. OCR → structured parse → live scout is a proper end-to-end flow, not a demo gimmick.
What we learned
- Guild agents are a nice middle ground between raw SDK calls and a full orchestration framework — you get the structured-prompt-plus-tools ergonomics without adopting a runtime.
- Real pharmacy sites are hostile. Pharmacy-specific hints (Walmart \$4 generics, Costco non-member pricing, GoodRx coupon ranking) mattered far more than any generic "find the price" prompt.
- For streaming UIs, SSE beats WebSockets for this shape of problem — it's a one-way event stream and
fetch+ReadableStreamon the client is all you need.
What's next for RxScout
- Insurance-aware prices. Right now we quote cash prices only; plugging in a SureScripts/MedImpact-style API would let us show "with your insurance" beside "cash".
- More pharmacies. Independents, mail-order (Amazon Pharmacy, Capsule), and Canadian cross-border options.
- Push alerts. "Atorvastatin dropped to \$6.80 at Cost Plus this week" via the cache history table.
- A phone-friendly mode. The app already works on mobile; a PWA wrapper with camera-first onboarding would close the loop for the "I just got a script, what do I do" use case.
Built With
- chainguard
- ghost
- guild.ai
- insforge
- tinyfish
Log in or sign up for Devpost to join the conversation.