Most freemium abuse costs you a seat or some storage. AI image and video generation is different: every free job burns GPU time you pay for by the second. A person who opens twenty accounts to collect twenty sets of welcome credits is not a rounding error; on a video model they can cost more than a paying customer brings in.
The usual fix, “verify the email,” does not help. Inboxes are free and plus-addressing makes one inbox look like many. The fix that works is to stop counting credits per account and start counting them per device as well.
Why per-account quotas leak
A typical setup gives each new account N free generations and resets monthly. The abuser’s math is simple: one more account is one more N. Every control that sits on the account, such as email verification, CAPTCHA or a username blocklist, adds a few seconds of friction per account and does not change the multiplier.
A quota keyed on the device breaks the multiplier. If a device gets the same allowance whether it holds one account or ten, the eleventh account is worthless.
Identify on session start, not on every job
Prynt gives each browser a stable visitorId that survives cleared cookies and incognito. Identify when the session starts and again on sensitive actions, then verify the result on your server. Do not trust a visitorId posted by the browser.
// browser: on app load
const agent = await Prynt.load({ apiKey: 'pk_live_…' });
const { requestId } = await agent.identify({ tag: { action: 'session' }, linkedId: currentUser.id });
await fetch('/api/session/device', { method: 'POST', body: JSON.stringify({ requestId }) });
// server
import { PryntServer } from '@prynt/node';
const prynt = new PryntServer({ secretKey: process.env.PRYNT_SECRET_KEY });
app.post('/api/session/device', async (req, res) => {
const ev = await prynt.getEvent(req.body.requestId);
req.session.device = {
visitorId: ev.visitorId,
decision: ev.decision,
riskScore: ev.riskScore,
accounts: ev.accountsOnDevice.count,
};
res.sendStatus(204);
});
Binding the verified visitorId to the server session means the generation endpoint never needs a fresh identification per job. Each identification counts against your plan’s monthly volume, so one per session, plus one at sign-up and login, is usually enough. Re-identify if the session is long-lived or the user starts a high-cost job such as a long video.
Charge two counters on every job
On each generation, debit both the account and the device, and refuse when either is empty:
async function reserveCredits(userId, visitorId, cost) {
const month = new Date().toISOString().slice(0, 7);
const userKey = `gen:user:${userId}:${month}`;
const devKey = `gen:dev:${visitorId}:${month}`;
const [u, d] = await redis.multi()
.incrby(userKey, cost).expire(userKey, 40 * 86400)
.incrby(devKey, cost).expire(devKey, 40 * 86400)
.exec()
.then((r) => [r[0][1], r[2][1]]);
if (u > USER_FREE_LIMIT || d > DEVICE_FREE_LIMIT) {
await redis.multi().decrby(userKey, cost).decrby(devKey, cost).exec();
return false;
}
return true;
}
Set DEVICE_FREE_LIMIT somewhat above USER_FREE_LIMIT, perhaps one and a half times. A household where two people share a laptop keeps working; a farm of ten accounts on one machine shares one allowance. Weight cost by what the job actually consumes: a 1024px image, an upscale and a ten-second clip should not cost the same number of credits.
Paid accounts skip the device counter entirely. The whole point is protecting the free tier, not metering customers. The same keying works for short-window burst limits too; see rate limiting by device.
Close the front door at sign-up
Quotas limit the damage per device; a sign-up check stops the account inflation that makes your metrics lie. At registration, read accountsOnDevice from the event and decide before you grant welcome credits:
const ev = await prynt.getEvent(req.body.requestId);
const others = ev.accountsOnDevice.count;
let welcomeCredits = WELCOME_CREDITS;
if (ev.decision === 'block') return res.status(403).json({ error: 'signup_unavailable' });
if (others >= 2) welcomeCredits = 0; // account allowed, no free credits
else if (others === 1) welcomeCredits = WELCOME_CREDITS / 2;
const user = await createUser(req.body, { welcomeCredits });
await prynt.updateEvent(req.body.requestId, { linkedId: user.id });
Granting zero credits instead of refusing the account is a useful middle path. The person can still pay, and you have not handed a farmer anything worth farming. updateEvent links the new user to the device so the next sign-up sees it. The full pattern is covered in preventing AI credit abuse on freemium plans.
Degrade, don’t slam the door
Device identification is probabilistic, and some flagged devices belong to real people. For devices that are suspicious but not certain, make free generation less valuable instead of unavailable:
- Lower output quality: smaller resolution, no upscaling, watermark on free outputs.
- Shorter jobs: cap video length or frame count for flagged devices.
- Slower queue: route free jobs from flagged devices to a lower-priority pool.
- Step-up before the next job: a phone check, a card on file, or a proof-of-work challenge via the agent’s
challenge()for automated bursts.
Use the signals to choose the response. decision and riskScore summarise the verdict; the reason codes say why. MULTI_ACCOUNT and ACCOUNT_SHARING point to farming. DATACENTER, TLS_AUTOMATION or BOT point to a script driving your generation API, which deserves a hard stop rather than a slower queue. VPN alone is weak evidence: plenty of paying users run one.
Watch for API-side abuse
Farmers who get blocked in the browser often move to your API. If generation is reachable with a session cookie or token, the device check has to happen where the token was issued, and the per-device counter has to be keyed on the device bound to that session. A token minted on one device and replayed from a server farm will show up as a different ip and, if you re-identify, a different visitorId. Treat that mismatch as a reason to revoke the token. More patterns for compute-heavy free tiers are in preventing free-tier compute abuse.
What to measure
Track three numbers before and after the change: free GPU-seconds per device per month, the share of free generations coming from devices with two or more accounts, and conversion from free to paid. If the device quota is set well, the first two drop sharply and the third does not move, or goes up because heavy free users now have a reason to pay.
Start in log-only mode: compute the device counter and write it alongside each job without enforcing it. A week of data will tell you where to put DEVICE_FREE_LIMIT. The Smart Signals behind the flags are described on the device fingerprinting page, and the free plan on pricing is enough to run that measurement.
Try it free
Prynt is device intelligence with a free tier — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics, powered by a cross-site network. Start free.