Zero-wait free trials: run your abuse check in the background
Start free trials instantly and check for trial abuse in a background job. When to grant first, how to take a trial back on deny, and when not to.
Every millisecond between "Start free trial" and the product matters. The usual advice is to check for free trial abuse before you grant the trial, and for most benefits that's still right. But for a benefit you can take back in full, there's another option: start the trial instantly and run the abuse check in a background job. Real users never wait. Repeat claims lose the trial a moment later.
This guide explains when grant-first is safe, how it works with AbuseZero, and the benefits where you should never use it.
Evaluate first, or grant first?
| Evaluate before you grant | Grant now, evaluate in the background | |
|---|---|---|
| User experience | One quick server call before the trial starts | The trial starts at once |
| What a repeat claim gets | Nothing | The benefit for a short moment, then it's ended |
| Fits | Every benefit | Benefits you can take back in full |
| Default | Yes, recommended | An option, benefit by benefit |
Evaluate-before-grant stays the default, as covered in add a trial and credit abuse check to your signup flow. Grant-first is for products where even a short wait on the trial path matters and the benefit can be fully ended.
Which benefits can be granted first
The rule is simple: if you can take all of it back, you can grant it first. If it can be used up before the result arrives, check first.
| Benefit | Pattern |
|---|---|
| Trial days, plan access, a feature | Grant now, evaluate in the background. On deny, end it. |
| AI or API credits, payouts, referral cash, promotional codes | Evaluate before you grant. These can be used up at once. |
Credits are the clearest case for checking first. They're consumed the moment they land. Grant them first and a script can spend them in the seconds before the result arrives, hundreds of times over, and the bill is yours either way. See how to protect signup credits in an AI app. One team in our early beta woke up to a $6,000 OpenAI bill after someone used up the free credits on hundreds of accounts.
How grant-first works
- Grant and enqueue together. In the request that receives the claim, grant the trial and add the evaluation job in the same database transaction, so every granted claim is evaluated.
- Capture request details now. The visitor's IP address, user agent and the device token from the AbuseZero browser SDK come from this request, not from the job.
- Start the job at once. The device token is short-lived, so don't delay or batch these jobs.
- Evaluate with a stored idempotency key. Every retry of the job gets the original result back, never a second evaluation.
- Act on the result. Keep the trial on
allow. End it ondeny, in your own words.
Instant trials, still protected
The evaluation is the same call either way, with the same depth behind it. Only the timing changes. Start with Builder or see pricing.
The job, in code
import { AbuseZeroError } from "abusezero/server";
import { abusezero } from "@/lib/abusezero";
export async function evaluateTrialJob(job: TrialJob) {
let evaluation;
try {
evaluation = await abusezero.evaluate(
{
benefit: "pro_trial",
claim_id: job.claimId,
subject: { external_id: job.userId, email: job.email, email_verified: job.emailVerified },
context: { ip: job.ip, user_agent: job.userAgent },
identifiers: { device_token: job.deviceToken },
},
job.idempotencyKey,
);
} catch (error) {
if (!(error instanceof AbuseZeroError)) throw error;
if (error.retryable && !error.code.startsWith("BOT_EVIDENCE_")) return retryLater(job);
return reviewClaim(job, error.code, error.requestId);
}
return actOnResult(job, evaluation);
}TrialJob, retryLater, reviewClaim and actOnResult stand for your own job code. Use a client whose outage option is "throw": no user is waiting, so the job retries instead of deciding locally. The complete guide is evaluate in the background.
When part of the benefit was already used
Sometimes the person used part of the trial before the result arrived, and you can't take all of it back. Record it honestly: report a grant after the deny, with a short reason. The claim then counts in your claim history like any granted claim, so the next claim from the same person is checked against it.
await abusezero.outcome(
evaluationId,
{ outcome: "granted", reason: "Part of the trial was used before the result arrived." },
`outcome:${evaluationId}:granted`,
);See grants after a deny.
Or act on results from a webhook
If another system already receives AbuseZero events, let the job evaluate and leave allow and deny to the evaluation.completed webhook event. One place then applies results for every benefit. A retried job never creates a second event, and a failed delivery is retried for 12 hours. More in AbuseZero webhooks.
Frequently asked questions
Can I check for trial abuse after the trial starts?
Yes, for benefits you can take back in full, such as trial days or plan access. Grant the trial, evaluate in a background job and end the trial on deny. For credits or codes that can be used instantly, check first.
Does a background check make AbuseZero less accurate?
No. It's the same evaluation with the same evidence, as long as you capture the request details when the claim arrives and start the job right away.
What do I tell a user whose trial was ended?
A short, neutral message with a way to reach you. Never explain what was detected. See multi-accounting detection without blocking real users.
What if AbuseZero is unreachable when the job runs?
The job retries with the same idempotency key until it gets an answer. No user is waiting, so there's no need to decide the claim locally. See handle outages and retries.
Make trials instant
Start with Builder
Pay as you go. Instant trials, still protected.
Evaluate in the background
The complete step-by-step guide.
See pricing
Builder, Pro, Scale and Enterprise.