Inspiration
It started with a Slack message: "Hey, can someone order more whiteboard markers?" That message sat in a channel for three days. Nobody knew who was supposed to buy them, whether they'd been ordered, or if someone had already grabbed a different brand. Eventually someone just ordered them on their personal Amazon account and submitted an expense report.
This happens constantly on teams. Somebody needs something, a monitor arm, a USB-C hub, a box of pens, and the "process" is a Slack message, a verbal approval, and a hope that someone follows through. There's no system because the existing systems are overkill. Nobody's going to spin up Coupa or SAP Ariba for a $40 keyboard.
I built BuyerBot to be the system that's as lightweight as the problem. You message it what you need. It finds it on Amazon. Your manager approves it with one click. The approved item lands in a pre-filled Amazon cart, and the buyer checks out on their own account. The entire purchasing loop — from "I need a standing desk" to a filled cart — happens inside a Slack conversation.
What I Learned
Slack's 3-second rule changes your architecture. Slack requires slash commands and interactions to respond within 3 seconds, but a real-time Amazon product search can take 2-4 seconds. You can't just call the API inline. I had to rethink the entire search flow: enqueue the search immediately, return an acknowledgment to Slack, process asynchronously via Cloudflare Queues, and post results back through response_url. This constraint pushed me toward a much better architecture — non-blocking, cacheable, and resilient — but it wasn't obvious upfront.
LLM tool-calling is powerful but needs guardrails. BuyerBot's conversational agent uses OpenRouter (Deepseek v4) with a tool-calling pattern. The LLM decides when to search Amazon, check the cart, or submit a purchase request. The tricky part is preventing runaway behavior. I implemented per-team rate limiting with Durable Objects (sliding window, 100 requests per 60 seconds), event deduplication (Slack sometimes sends duplicate events), and careful prompt design to keep the agent focused on purchasing and not hallucinating product data.
Image URLs expire, and that breaks Slack. Amazon CDN URLs for product images have short-lived tokens. If you embed them directly in Slack messages, the images break within hours. I built an R2-backed image proxy that caches product images and serves them through a stable api.buyerbot.ai/images/ URL. This was a problem I didn't anticipate until I saw broken thumbnails in day-old messages.
Multi-tenant OAuth is harder than it looks. Supporting multiple Slack workspaces means storing per-workspace bot tokens, resolving the right token on every API call, handling reinstalls gracefully, and cleaning up on uninstall. I built a fallback chain (workspace token from D1 → environment variable) and CSRF-protected OAuth state in KV with short TTLs.
How I Built It
BuyerBot is built entirely on Cloudflare's developer platform, using nearly every primitive they offer:
- Cloudflare Workers (with the Hono framework) handles all HTTP routing — Slack commands, events, interactions, OAuth, and the image proxy
- D1 (SQLite at the edge) stores workspaces, users, purchase requests, approval records, and cart line items
- KV caches search results (12-hour TTL), product details (24-hour TTL), thread conversation context, and OAuth state
- R2 caches product images from Amazon to prevent broken thumbnails in Slack messages
- Queues processes Amazon search requests asynchronously, keeping responses within Slack's 3-second window
- Durable Objects implements per-team sliding-window rate limiting for the conversational agent
The search layer uses RapidAPI's Real-Time Amazon Data API behind a provider abstraction, so the search backend can be swapped without touching the rest of the codebase. The conversational agent runs on OpenRouter's inference API with a tool-calling loop - six tools (search Amazon, view cart, check history, submit request, search Slack messages, check pending requests) that the LLM invokes based on the conversation.
The marketing site at buyerbot.ai is plain HTML/CSS deployed on Cloudflare Pages; no framework, no build step, just fast static pages with privacy policy, terms of service, and GDPR/COPPA compliance documentation.
Everything is TypeScript, and the entire stack deploys with a single wrangler deploy.
Challenges
Slack's approval UX required careful state management. When a purchase request is submitted, every approver in the workspace receives a DM with approve/reject buttons. When one approver acts, all the other approvers' messages need to update to reflect who approved it. This means tracking the message timestamp and channel for every approval message in the database, then issuing chat.update calls to each one. Race conditions are real — two approvers can click "Approve" simultaneously — so the handler checks request status before processing.
Cart deduplication was subtler than expected. If two people request the same ASIN, it should merge into one cart line item with combined quantity, not create a duplicate row. I solved this with a unique index on (cart_id, asin) and an UPSERT pattern, but getting the SQL right for D1's SQLite dialect took iteration.
Balancing the LLM agent with deterministic flows. The slash command (/buy) is fast, predictable, and always works. The conversational agent is flexible and natural but slower and occasionally unpredictable. I made the agent robust but kept the slash command as the primary entry point, so teams always have a reliable fallback. The agent enhances the experience but never gates it.
Built With
- claude
- cloudflare
- drizzleorm
- hono
- openrouter
- rapidapi
- slack
- slack-agent-builder
- slack-ai-capabilities
- typescript
- wrangler
Log in or sign up for Devpost to join the conversation.