A recorded decision, from the live API

A signup arrives 

A new address, a browser with no history, an IP you have not sold to before. It looks like any other Tuesday.

email d.mercer+t7@mailinator.comip 45.83.220.7visitorToken signed, valid for 5 minutes

The browser has been here 

This device already finished a free trial, under a different name. That alone proves little, because people share machines.

The address is new, the person is not 

Canonicalized, stripped of the plus tag and the dots, it resolves to an account you already have.

The mailbox is temporary 

It is a disposable domain. Nobody opens a renewal notice at an address built to expire.

Same block, different door 

The subnet has already been used to consume a trial. A VPN moves the address, not the neighbourhood.

And always at this hour 

The accounts this one links to started their trials in the same part of the day. People have habits. So do their scripts.

Five signals. Four families. One answer. 

deny

out of 100
{
"decision": "deny",
"score": 81,
"reasons": [
{ "code": "D_DEVICE_REPEAT", "message": "Device previously used for a consumed trial" },
{ "code": "E_EMAIL_REPEAT", "message": "Email matches a prior account" },
{ "code": "E_DISPOSABLE_DOMAIN","message": "Disposable email domain" },
{ "code": "N_IP24_REPEAT", "message": "IP subnet previously used for a consumed trial" },
{ "code": "B_ABUSE_HOUR_MATCH", "message": "Signup hour matches prior abuse in linked cluster" }
]
}
The real response carries a weight on every reason. Those numbers, and the two lines that turn a score into a verdict, belong to your dashboard and your API, not to this page.
Device previously used for a consumed trialDevice signal · D_DEVICE_REPEAT
Email matches a prior accountEmail signal · E_EMAIL_REPEAT
Disposable email domainEmail signal · E_DISPOSABLE_DOMAIN
IP subnet previously used for a consumed trialNetwork signal · N_IP24_REPEAT
Signup hour matches prior abuse in linked clusterBehaviour signal · B_ABUSE_HOUR_MATCH

One tag. One call.

The tag mints a short-lived token in the browser and shows nothing. Your server sends that token with the signup and gets the answer back. No decision is ever made in the browser, because the answer carries the score and the reasons and those are yours alone.

Card numbers never reach us. Emails are stored canonicalized and hashed, with the address encrypted.

On your signup page
<script src="https://onetrial.dev/v1.js" data-key="YOUR_PUBLIC_KEY" async></script>
In your signup handler
const res = await fetch('https://onetrial.dev/api/v1/decisions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ONETRIAL_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ visitorToken, email, ip, userId }),
});
const d = await res.json();
if (d.decision === 'deny') return showPaidPlanOrSupport();
if (d.decision === 'challenge') return redirect(d.challengeUrl); // trial unlocks on the challenge.completed webhook
createTrial(userId);

Three answers, and what each one costs you

deny

Send them to a paid plan

The evidence is strong enough that a trial would be a repeat. Offer the paid plan or a way to reach you. The visitor never sees a score or a reason, so nobody learns what to change next time.

challenge

Ask for one proof

A card, an email link, or a captcha. We host the page and tell your server the outcome, or you run it inside your own UI. A real customer clears it and carries on.

allow

Create the trial

Nothing about this signup looks like a repeat. This is most of your traffic, and it should never notice that any of this happened.

Nobody gets turned away on your first day.

New workspaces start in shadow mode. Every signup is scored and recorded, and your app is always told allow. You spend a week reading what would have happened, decide whether the answers match your own judgement, and only then turn enforcement on. It is the cheapest way to find out whether a fraud tool suits your traffic, and most of them will not let you do it.

json
{ "decision": "allow", "shadow": true, "wouldBe": "deny", "score": 81 }

Four plans, priced on decisions

A decision is one answered signup. The free plan stops at its cap rather than billing you by surprise.

Free

$0

per month
500 decisions a monthHard cap. No card.
Starter

$49

per month
2,500 decisions a month90 days of history.
Growth

$199

per month
15,000 decisions a month3 seats, a year of history.
Scale

$499

per month
75,000 decisions a month10 seats, a year of history.

Or let the agent do it

The CLI detects your framework, writes the env vars, injects the tag and self-tests. The dashboard also hands you a prompt carrying your real key, so Cursor or Claude Code has nothing left to guess.

bash
npx onetrial init
npx @onetrial/mcp # the decision feed, the sandbox and the rules, as agent tools