Show HN: I built a message board where you pay to be the homepage (saythat.sh)
I kept thinking about what would happen if a message board only had one slot. One message, front and center, until someone pays to replace it.
That's the entire product. You pay the current message's decayed value plus a penny to take the homepage. Message values drop over time using a gravity-based formula (same concept HN uses for ranking), so a $10 message might only cost a few bucks to replace a day later. Likes slow the decay, dislikes speed it up.
The whole thing runs on three mini PCs in my house (k3s cluster, PostgreSQL, Redis Sentinel). Is it overengineered for a message board? Absolutely.
I genuinely don't know where this goes. Curious what HN thinks.
Archive of past messages: https://saythat.sh/history
8 comments
[ 4.2 ms ] story [ 29.5 ms ] threadI'm most curious about your development process. This has a very strong feel of being vibe coded, which I have nothing against. And your replies here also sound heavily LLM influenced. I als have no issues with that. I am more interested to know if my instinct about these things is accurate. Has developed in this way, over engineered as you say, essentially because the cost to do so is practically nothing with the advent of LLM coding?
Also, has anything ever broken at 3am?
TL;DR:
Concurrent bids: First payment to complete wins; everyone else gets auto-refunded and notified. Uses version checking in the database so no one pays for a post that's already been replaced.
Vibe coding: Started from a half-finished project by a dev team that ghosted. Took over and built the rest with Claude Code, but spent roughly half the time on security, performance, and privacy compliance — not just generating code.
LLM content: Uses LLMs to draft technical/promotional writing, then edits and fact-checks. Natural writing style is similar to LLM output anyway.
3AM breakages: Mostly from experimental automation tooling, nothing production-critical.
Now for the full answers:
1. Two people bidding to replace the same message:
- Simple explanation: Optimistic locking with automatic refunds.
- A bit more detail: The first payment to fully process wins. The second (third, fourth, etc.) person gets an automatic full refund and a notification explaining that someone else beat them to it. Under the hood it's a simple "check before you activate" on the actual message acceptance. When you start paying, the system notes which message is currently live. When the payment completes, it checks whether that same message is still there. If someone else already replaced it, the payment gets refunded instead of going through. Realistically, this shouldn't be happening in any normal situation as the backend is quite responsive. But if I got heavy enough traffic (specifically paid message submissions), I'd likely tune the behavior to refresh frontend values even more frequently, same with optimizing the backend to reduce latency.
- Techno-babble: When you initiate payment, the system snapshots the current post's version number and stores it in the Stripe PaymentIntent metadata. When the webhook comes back confirming payment succeeded, it does an atomic PostgreSQL UPDATE ... WHERE id = $1 AND version = $2 with an increment. If updated.count === 0, someone else already won. The payment gets automatically refunded via the Stripe API, and the user gets a real-time WebSocket notification explaining what happened ("Someone else posted while your payment was processing. Your payment has been refunded."). There's also a per-user Redis distributed lock (SETNX with TTL) to prevent the same person from double-submitting, and post numbers use a PostgreSQL sequence that's only consumed after winning the activation race, so there are no gaps in the numbering. The whole activation path is idempotent, so Stripe webhook retries are handled gracefully.
2. Vibe coding: The initial project referenced to build the site was hand-built, unfortunately by a dev team I paid who basically ghosted me halfway through. I got my money back and there's a whole story behind that but basically I got sick of waiting for their bug fixes and was spending so much time troubleshooting, I figured I may as well take full charge. Claude Code got me from there to here. But frankly, about half the time I spent building the site was spent on performance optimization, security hardening, and aligning with GDPR (and California's) privacy standards.
3. LLM influenced responses: Your instinct is fairly good on that! For more technical breakdowns and generic 'promotion' stuff, I draft with LLMs and then adjust based on my own preferences (and after fact checking where relevant). To that end, my natural writing style is a bit stiff/serious so it ends up sounding about the same. You'd be right about it being overengineered due to the low barrier to entry as well. I'm far from new to containerization, but stepping into the K8S world and configuring everything manually was outside my scope when the ends were more important than the means. Not that I don't make significant efforts to learn about the stack I'm using every day I work on i...
I'm always a bit worried about making super low priced services myself with Stripe, mainly cause I've heard that's a common target for credit card testing fraud, but interested to hear your thoughts.
I've got lots of features on both the frontend and backend to prevent automated behavior that could be problematic. But ultimately nothing is stopping someone from throwing a random number into the payment modal to see if it works (though they'd still have to enter an email, zip, expiration, and CVV too).