How I built a full-featured Texas Hold’em bot using only free cloud resources
The Problem
I wanted to build a poker bot for my friends’ Slack workspace. Nothing fancy; just a way to play a few hands during breaks without leaving Slack. But I had a few hard constraints:
- No server costs. Everything had to run on free tiers.
- No maintenance. I didn’t want to babysit a server.
- Scales to zero. If nobody’s playing, I shouldn’t be paying (ideally not paying at all).
The answer turned out to be a fully serverless stack: Vercel Edge Functions for compute, Upstash Redis for state, and GitHub Actions for CI/CD.
The Architecture
At a high level, the bot works like this:
- A player types
/poker startin Slack. - Slack sends a webhook to a Vercel Edge Function.
- The function reads and updates game state from Redis.
- The bot posts an interactive message back to the channel with action buttons (Fold, Call, Raise).
- Players click buttons, which trigger more webhooks, and the cycle repeats.
That’s it. No persistent server. No background process. Just functions that wake up, do their job, and go back to sleep.
Why This Stack?
| Component | Free Tier | Why It Works |
|---|---|---|
| Vercel | 100 GB / month bandwidth | Edge functions start in ~50ms; well within Slack’s 3s response window |
| Upstash Redis | 500 K commands/ month | Game state is tiny; TTLs handle cleanup automatically |
| GitHub Actions | 2,000 min/month | Plenty for running tests on every PR |
| Slack API | Unlimited messages | Bot messages are free |
Building the Bot
Handling Slack Commands
Slack sends a POST request to your server whenever someone types a slash command. The tricky part: you have 3 seconds to respond, or Slack shows an error to the user.
Edge Functions solve this elegantly. They cold-start in ~50ms, leaving plenty of headroom for game logic. The handler verifies the request signature (Slack signs every request with HMAC-SHA256), parses the command, and dispatches to the right handler; all well within the deadline.
Every command goes through the same pipeline: rate-limit check → signature verification → command dispatch. This keeps things secure and predictable.
State Management with Redis
Serverless functions are stateless by design, so all game state lives in Redis. Each game is stored as a JSON blob with a 24-hour TTL; if a game goes stale, Redis cleans it up automatically. Player bankrolls persist for 30 days.
The key insight here is that poker game state is small. A full game with 8 players, all their cards, the community cards, pot sizes, and betting history fits comfortably under 10KB. Redis is overkill in terms of power, but it’s perfect in terms of simplicity and cost.
One thing I had to be careful about: race conditions. When two players click buttons at nearly the same time, both requests might try to read and write the same game state simultaneously. The solution was a simple Redis-based locking mechanism; acquire a lock, read the game, make changes, write back, release the lock.
Interactive Buttons
This is what makes the bot feel like a real game rather than a command-line tool. Slack’s Block Kit lets you attach buttons to messages, and each button click sends a new webhook to your server.
When a player clicks “Raise”, the bot:
- Receives the action payload from Slack
- Loads the game state from Redis
- Validates the action (is it this player’s turn? do they have enough chips?)
- Updates the game state
- Posts an updated message to the channel showing the new game state
The whole round-trip takes under a second.
Scheduled Cleanup
One underrated feature of Vercel is Cron Jobs; you can schedule serverless functions to run on a schedule, just like a traditional cron job.
I use two:
- Daily at 3am: Clean up any games that have been inactive for over an hour.
- Every Monday: Reset the weekly leaderboard and post the results.
No background process needed. The functions just run, do their thing, and disappear.
The Fun Stuff
The Wall of Shame
If you go broke (under 20 chips), you can beg for more; but there’s a catch. You have to write a public message explaining yourself, and it gets posted to the channel for everyone to see. In exchange, you get 500 chips back.
The bot keeps a persistent, paginated “Wall of Shame” in Redis so your humiliation lives on even after the bot refreshes the data.
AI-Powered Feature Requests
This is the part I’m most proud of (I’m addicted to building this everywhere now; see Building a Bug-to-PR Pipeline for Kotoba). Users can submit feature requests directly from Slack with /poker feature "Add support for Omaha poker". The bot creates a GitHub issue automatically.
If the user is listed in CONTRIBUTORS.md, the bot goes one step further: it comments /oc on the issue, which triggers a GitHub Actions workflow that runs opencode — an AI coding agent — to actually implement the feature and open a PR.
The flow looks like this:
/poker feature "Add hand history"
↓
GitHub Issue #15 created
↓
User is a contributor → comment "/oc"
↓
GitHub Actions triggers opencode
↓
PR #16 opened with implementation
Deployment
Getting this running is straightforward:
-
Create a Slack app at api.slack.com/apps. You’ll need a few OAuth scopes:
chat:write,commands, andim:writefor sending hole cards privately. -
Deploy to Vercel. Connect your GitHub repo, add Upstash Redis through the Vercel dashboard (it auto-provisions the connection), and add your Slack credentials as environment variables.
-
Point Slack at your Vercel URL. Update the slash command and interactive components URLs in your Slack app settings.
-
Test locally with ngrok. Run
npm run devand use ngrok to expose your local server to Slack’s webhooks. This makes iteration much faster than deploying for every change.
Lessons Learned
Edge Functions are a great fit for Slack bots. The cold start time is negligible, and the 10-second execution limit is more than enough for game logic. If you’re building anything that responds to webhooks, this pattern works well.
Redis TTLs are underrated. Instead of writing cleanup jobs for everything, just set a TTL and let Redis handle it. Game state expires after 24 hours. Sessions expire after an hour. It’s one less thing to think about.
Rate limiting is non-negotiable. Without it, a single user can spam commands and exhaust your free tier limits in minutes. A simple Redis-based rate limiter (increment a counter, set an expiry) is enough to protect against this.
Test with real Slack webhooks early. The Slack API has some quirks — payload formats, response timing, ephemeral vs. in-channel messages — that you won’t discover until you’re actually sending real requests. ngrok makes this easy.
Wrapping Up
Serverless isn’t just for simple webhooks. With Redis for state and a bit of careful design around the stateless execution model, you can build surprisingly complex interactive applications that cost nothing to run.
The full source code is soon to be open-sourced on GitHub. If you build something with it, I’d love to hear about it.
Happy building! 🚀