•
14 min read

Yes, My AI App's "Free Trial" Was Free Forever. Here's the Other Side.

Yes, My AI App’s “Free Trial” Was Free Forever. Here’s the Other Side.

Companion piece: my friend emptyex’s write-up from the attacker’s side, Your AI App’s “Free Trial” Might Be Free Forever. This is the defender’s side. 中文原版: AI套壳站SaaS的思路转变: 技术,市场,安全和妥协


Background

Late last year I wanted to try the “AI wrapper” game: find a keyword gap, put a model behind it, make money off the information gap. So I built and launched aibodyeditor.io, a small AI portrait editor. Upload a photo, type a prompt, and the AI reshapes the body, swaps in a swimsuit, tries on clothes.

The web version went live at the end of March 2026. iOS and Android apps followed in August.

Eight months in, the honest summary is: I paid tuition and got an education. The loss is groundwork for what comes next. And a good chunk of that tuition went to security holes that came straight out of my own naive programmer brain.

The numbers:

Count
Web signups~3,000
Web generations (signed-in + anonymous)~25,000
App users~11,000
App generation requests~21,000, about half failed, a good share of those blocked by the model’s safety filter
Paying usersa few dozen

In short: revenue covers the server bill. It does not cover the tuition.

Beyond the business lessons (finding keywords, finding demand), the biggest thing I took away is that a friend gave my security awareness the kick it needed. In the AI era both attack and defence come with AI attached, so human experience and expertise end up being the variable that matters most. Along the way I got a close look at every flavour of freeloader and grey-market trick that shows up the moment you run a SaaS with anything free in it. Lessons learned, eyes opened.

Thanks again to my friend emptyex for the time and effort. emptyex is one of the smartest and most relentless people I know. He earned the heavyweight cyber security certs on his own steam, pretty much the whole set. Getting him to use my little site as a practice target, and write up what he found, was pure luck on my part.

He wrote the whole test up: Your AI App’s “Free Trial” Might Be Free Forever. Same story from the attacker’s side. Read the two together. If you have cyber security questions, or work you’d like to talk to him about, get in touch with emptyex.


1. Why I picked a template and a framework to move fast

For an indie product the first rule is speed. So every technical decision looked reasonable at the time. But they pile up, turn into security debt, and the debt gets collected by freeloaders.

The web app started from an off-the-shelf SaaS template. Login, Stripe, credits, admin panel, all included; two weeks to launch. The template itself wasn’t the problem. The problem was that we never understood it before throwing it at an AI to modify, and treated it as “something already thought through”. Only later did we find out the template passed the model name and parameters from the frontend straight through to Replicate, including the parameter that turns off the safety filter. Edit one field in the browser and you could run a model that doesn’t exist in the UI, with the filter off while you’re at it. Someone actually did this in production, 9 times. You can see it in emptyex’s Burp screenshots too: provider and model sitting right there in the request body.

Image models on Replicate, nothing self-hosted. Pay per call, swap models by changing one line of config. We swapped a few times: Seedream → Grok → a model half the price and six times faster (but you handle NSFW yourself) → back to Grok three days later.

So why not just stick with the fast, good, cheap one? Because its safety guardrail was moody: some days it wouldn’t pass a gym tank top, other days it let genuinely sensitive images through. And when it did block a request it didn’t return an error. It returned a placeholder image with “NSFW” written on it, status “success”, and the user’s credits were deducted anyway.

In the portrait editing business, a user uploads a real person’s photo, the model generates something it should not, and the consequence lands directly on your payouts. Every payment provider has strict policy here: Stripe freezes the account, Apple and Google pull the app, and at the far end there are legal problems. So when the choice is “cheap and fast” versus “a filter you can actually rely on”, I’d rather pay double per image.

There is probably a more elegant engineering answer. That’s for later.

No login in the app. RevenueCat gives you an anonymous ID, Apple and Google accounts already are a user system, and a login screen only costs you conversions. This decision saved a lot of engineering and planted half the landmines that went off later: identity was a string the user could swap at will.

Anonymous trial on the web. Three free edits, no signup. A great acquisition hook, and the early signup conversion looked lovely.

Every one of these was a compromise. Put them together and look from the security side, and the debt came due all at once. Under emptyex’s test the whole site leaked from every seam, like a high-schooler’s toy project with zero thought given to failure modes.


2. Where the money leaked out

1. One subscription, eleven accounts

No login in the app, so a user’s identity is RevenueCat’s anonymous ID. A clever user noticed that uninstall + reinstall = new ID.

After the reinstall the app hits our server, the Worker sees a “new user” and checks with RevenueCat: is this person a subscriber? RevenueCat looks at the store receipt: yes, same person. So the Worker hands out another month of credits.

Result: one Play subscription with 11 accounts hanging off it. The user paid for one month and got about ten times the credits.

Lesson: credits belong to the subscription, not to the account ID.

2. Sandbox purchases: the zero-dollar checkout

Apple and Google both have sandbox environments where test purchases cost nothing. Our webhook read the environment field, printed it to the logs, and never once used it in the decision logic.

One sandbox purchase = a month of real credits + Pro membership.

14 accounts in the database marked “subscribed”, with not a single real IAP transaction behind them.

Lesson: when you’re taking access away, just do it. When you’re granting it, check ID first.

3. A pirated APK was 70% of new signups

It never crossed my mind that an app this small would get cracked and repackaged. In South America someone unpacked the Android build and redistributed it. 70% of new signups came from the pirated package, each device collecting 3 free credits, zero paying.

We added Google’s Play Integrity: the package has to be installed from Play, the account has to be licensed, the device has to pass the integrity check. Launch night, I tested it myself: uninstall, reinstall, Integrity passes, another free allowance collected. Because Integrity proves “the package and the account are real”. It does not prove “this device hasn’t collected before”. So we added a second cut: dedupe by device.

After the fix, weekly signup bonuses handed out: 2,484 → 364 → 88.

Lesson: one gate answers one question. Be clear which question you’re asking.

4. Refund on failure, so failure became a free retry

Model blocks the request, we refund the credits. Sounds fair.

But someone who keeps getting blocked now has unlimited free retries. In 14 days, 1,788 free users were blocked 2,207 times, one person got refunded 67 times. What were they doing, what was in those prompts… I’ll let you guess.

Lesson: now a user who has never paid gets at most two failure refunds. Paying users are unaffected.

5. Anonymous trial: it cost more than the whole site made

From day one I didn’t trust email signups, so Google login was the only option. But for the anonymous trial, I naively assumed a browser fingerprint would stop most non-technical people.

The idea: lean on the fingerprint, 3 credits per fingerprint; if the same IP keeps rotating fingerprints, after 10 attempts temporarily block the whole IP range.

Clever users figured it out fast: the fingerprint is computed client-side, change it and you’re a new person. Block the IP, they switch VPN. Block the range, they switch to a VPS exit. In the Cloudflare data, over 90% of the traffic from the Netherlands, Japan, Singapore and Switzerland came from a handful of heavy subnets.

The record: the same prompt run 540 times.

By September the anonymous trial was producing close to ten thousand images a month and costing more than the whole site earned in the same period. In August I had written in my own docs that “the anonymous trial is our most cost-effective acquisition channel”. Two months later I switched it off with my own hands.

emptyex’s report put it plainly: a fingerprint is only ever a gentleman’s agreement, you can never trust anything that comes from the frontend. As he shows, change _abe_fp and you are someone else; and an IP limit means nothing to anyone on a VPN or proxy.

So the choice became a dilemma: Keep the anonymous trial and accept that the fingerprint can be swapped at any time. Stacking an IP limit on top only raises the bar a little; the problem is still there. Or switch it off and accept the drop in conversion. Put yourself in the user’s shoes: nobody hesitates at the Costco checkout after the free sample.

Lesson: the final call was to switch it off. The people who were going to buy will buy. The people who weren’t, tried it, and still didn’t.

6. If a Worker can replace the server, don’t run a VPS

This one isn’t a security incident, but it scared me the most.

The whole VPS carried a pile of containers to maintain, and the Tailscale mesh kept fighting with the route table. Along the way came a RackNerd host upgrade, then a Tailscale auto-update that logged itself out and took the machine’s DNS with it. Every container lost outbound resolution. The web app went 12 full days with zero signups and zero generations.

Meanwhile my own monitoring was watching the homepage, which Cloudflare had cached, so it returned 200 the whole time. Green lights across the board, not a single alert.

Lesson: a 200 on the homepage proves one thing: the CDN is alive.


3. A more professional pair of eyes

Everything above we found ourselves, by reconciling the books. Constant firefighting. Every time it started with “why is this month’s bill so high”, and only then: oh, so that’s another hole.

In October I mentioned my SaaS to emptyex. He was curious and spent an afternoon on it. His report (details in his article) called out three things:

  1. Change one local value and the free trial renews forever. And along the way he noticed that the credit count on the page reads from localStorage while the upload endpoint reads from the cookie, two systems minding their own business. That’s a seam we had never once noticed ourselves.
  2. Prompt injection gets the AI to print its system prompt onto the image. What leaked was the upstream prompt rewriter’s instructions, nothing secret of ours, but the path itself is open.
  3. The watermark is applied on the frontend. The API hands the browser the original image URL; right-click, open in new tab, and there’s the clean original.

With his report in hand we went back and did a white-box pass, which pulled out a few more: the “client picks the model” issue above, the app’s failure-refund loop, and “N concurrent generations, one credit charged” all came out of that round.

We had already run AI over the codebase before, and it found plenty. But some things an AI scan won’t find: it looks at whether the code has bugs, an attacker looks at who the system trusts.

And the script kiddie of the AI era doesn’t need to understand the details anymore; cheap tokens let them just keep trying. What matters is having the security mindset and perspective, not just thinking like a programmer or a business owner.


4. The fixes

Shipped on October 23:

ProblemFix
Client picks model and parametersModel decided server-side, parameter whitelist, images only accepted from our own storage domain
Watermark on the frontendWatermark composited at the Cloudflare edge, anonymous users never hold an original URL
Anonymous trial farmedSwitched off; one free generation after Google login instead
Concurrent generations charged onceIf the deduction can’t be fully covered, roll back the whole thing and don’t persist the task
App failure-refund loopFree users limited to two refunds
iOS collecting the signup bonus without a device IDNo device ID, no bonus

Prompt injection we left alone for now: what leaks is the upstream provider’s prompt, there’s nothing secret to leak on our side. Properly closing it means a content gate, and that’s next phase.

Switching off the anonymous trial was the hardest call. It used to be our best-converting entry point. But the arithmetic is simple: close to ten thousand free images a month, in exchange for a signup conversion that slid from 17.6% to 3.9%. Switching it off was worth it, because it had stopped being an acquisition channel and become free GPU for the farmers.


5. Every hole came from the same blind spot

It really comes down to one sentence:

We trusted what the client said about itself.

The model name came from the client, the fingerprint came from the client, the device ID is the client’s, “I’m a new user” is the client’s word, even the IP is a request header that can be forged.

Now, for any endpoint that spends money, we ask three questions first:

  1. This identity: who vouched for it?
  2. The price and the model: who decided them?
  3. Skip the frontend and hit the API directly. What happens?

If you can’t answer, don’t rush to ship.


6. Last thing

Running an AI product is not an easy business right now. Model costs are real, users are willing to pay very little, and the people trying to get it for free are far more diligent than the people paying. A small team carries product, growth, compliance and security all at once, and security is permanently filed under “when there’s time”.

AI can write your code and AI can scan your code, but it stands on your side of the table and sees what you see. Get a real attacker to take one look, even if it’s just one afternoon. Worth it.

Thanks again to emptyex. His article tells the same story from the attacker’s side, with a far more complete chain of evidence than mine: Your AI App’s “Free Trial” Might Be Free Forever. If you’re building an AI product, read his first, then come back and hold it up against your own site.