2
2 Comments

The first signup my anti-abuse system blocked was my own test account

I give away 2 free credits on signup. Enough for one design plus its print-ready file. Generous enough to be useful, cheap enough that I wouldn't cry if someone farmed it.

Then I had to actually stop people from farming it.

Here's what shipped, and the one decision I'd defend hardest.

Four layers, checked in order

  1. A small geo denylist (a couple of markets where the abuse rate made a free tier unviable)
  2. No visitor cookie, no grant
  3. Same visitor cookie already signed up, no grant
  4. Same IP already signed up, no grant

Every attempt writes a row to a signup_attempts table with the outcome and the rejection reason. Not just the blocked ones. All of them.

The decision I'd defend hardest: the account is always created

None of those four layers stop anyone from signing up.

They gate the free credit grant and nothing else. You still get an account. You can still pay. The only thing you don't get is the 2 free credits.

Writing that felt wrong. It felt like leaving the door open.

But look at who actually trips these rules. A family behind one IP. Someone on a corporate NAT. A privacy-conscious user who blocks cookies. Anyone at a coworking space. If I had blocked signups instead of grants, every one of those people would hit a wall with no explanation, and I would never hear about it, because they would just leave.

A withheld credit is recoverable. I can read the row, see the reason, and grant it by hand. A blocked account is not recoverable, because the person is already gone.

Fail open, not closed

The geo check reads an edge header for country. Locally that header doesn't exist at all.

My first instinct was to treat a missing country as suspicious. That is exactly backwards. A missing header is not a signal about the user, it is a signal about my own infrastructure. If that header ever stops arriving in production, failing closed means every new user on earth silently gets zero credits and I find out from a support email three weeks later.

So a missing country grants. Same for a missing IP. The comment I left in the code says it plainly: callers fail open, so a missing header never blocks everyone.

Log the grants, not just the rejections

signup_attempts records successful grants too. I almost skipped that to save a write.

That table is now the only way I can answer "is this working, or is it silently rejecting everybody?" A table that only contains failures cannot tell you your failure rate.

And then it caught me

The first thing it ever blocked in production was my own test account. Same machine, same IP as the operator account I had signed up with an hour earlier. granted = 0, reason ip_dupe.

I had to go and grant myself 2 credits by hand.

Which was the best possible outcome. The rules fired correctly, the reason was sitting right there in the table, and the fix took thirty seconds, because the account existed and only the grant was missing.

If I had built it to block signups, I would have spent that afternoon debugging a broken OAuth flow instead.


Building T-Shirt Design AI (https://tshirtdesignai.com) - you type a sentence, it hands back print-ready t-shirt artwork at 300 DPI.

on August 4, 2026
  1. 1

    The fact that your own account became the first false positive makes the example unusually convincing.

    Not because the rule was perfect, but because you could see exactly what happened without losing the signup.

  2. 1

    The part that stayed with me wasn't the anti-abuse logic but the emphasis on recoverability.

    "A withheld credit is recoverable. A blocked account is not." That feels like a much broader systems principle. Perfect prevention is rarely possible, but designing so mistakes can be understood and corrected changes the cost of getting them wrong.

    I also liked that the logs weren't just there for debugging, they became a way to understand whether the system itself was behaving as intended.

Trending on Indie Hackers
How to rank #1 on ChatGPT? User Avatar 112 comments I built a startup-idea scanner. It just told me none of my 3,400 ideas are easy wins. User Avatar 76 comments “I’ll just post on Upwork” is not a client strategy. Here’s what I built instead. User Avatar 58 comments Building a Shopify bundles app for stores with real fulfillment: here's the wedge User Avatar 42 comments I recorded myself using 200+ indie SaaS products cold. Here are the 7 conversion killers that keep showing up. User Avatar 33 comments The Capture Trap User Avatar 32 comments