Most fraud does not look risky one request at a time. A clean residential IP and a normal Gmail address pass every static check, and so do the next nine signups from the same place. Velocity is how Fidro catches the pattern rather than the request.
Stateless and stateful calls
A /api/validate call is stateless unless you send event or user_id. Stateless calls run the email and IP checks and return a result; nothing about them is counted later.
A call that carries event or user_id is stateful. Fidro:
- Runs the usual checks and produces the base
risk_score. - Stores the call as an event: the type, the email and its normalised pattern, the IP and its /24 or /64 subnet, the country, the ASN, and your
user_id. - Counts velocity over your account's own events (including this one) and returns the counts in
data.velocity. - Evaluates the anomaly rules. Each rule that fires adds its weight to the score, may floor the recommendation, and is reported in
data.anomalies,data.checksand at the end ofmessage.
Events are scoped to your account. Another customer's traffic from the same IP never affects your counts. Test keys write test-mode events that are counted separately from live.
Event types
| Type | Counted by |
|---|---|
signup |
The three burst rules |
login, checkout, password_reset, check, custom:<name> |
The shared-IP rule and, with a user_id, the travel rules |
custom:<name> accepts lowercase letters, digits and underscores, up to 24 characters. A call with a user_id and no event is recorded as a check.
Velocity counts
| Key | Meaning |
|---|---|
ip_events_1h, ip_events_24h |
Events of any type from this IP |
ip_signups_1h, ip_signups_24h |
Signups from this IP |
subnet_signups_1h |
Signups from this IP's /24 (IPv4) or /64 (IPv6) |
ip_distinct_users_24h |
Different user_ids seen from this IP |
email_pattern_matches_24h |
Signups whose address reduces to the same pattern as this one |
user_distinct_ips_24h |
Different IPs this user_id has used |
user_countries_30d |
Different countries this user_id has appeared from |
Counts include the current call. A key that does not apply (no IP, no user_id) is 0.
Anomaly rules
| Code | Fires when | Severity | Score | Floor |
|---|---|---|---|---|
ip_signup_burst |
3+ signups from one IP in an hour, or 5+ in 24 hours | high | +40 | challenge |
subnet_signup_burst |
5+ signups from one subnet in an hour | medium | +20 | none |
email_pattern_burst |
3+ signups in 24 hours reducing to one email pattern | high | +40 | challenge |
ip_many_users |
5+ different user_ids from one IP in 24 hours, on a non-signup event |
medium | +20 | none |
new_country_for_user |
A user with 3+ prior events from one country appears from another | medium | +15 | none |
impossible_travel |
One user_id seen from two countries within 60 minutes |
high | +30 | challenge |
Weights are added to the base score and the total is capped at 100. A floor means the recommendation is at least that value even if the score alone would say allow. The rules and thresholds are fixed in this release; per-account tuning is on the roadmap.
Email patterns
Before matching, the local part of an address is lowercased, anything after a + is dropped, dots are removed and trailing digits are trimmed. Sam+promo@example.com, s.a.m@example.com and sam42@example.com all reduce to sam@example.com. The domain is matched exactly.
Countries
The country comes from the IP's geolocation. In test mode the country_code you send on the request stands in for it, so the travel rules can be reproduced with a test key.
Reading the response
{
"risk_score": 70,
"recommendation": "challenge",
"message": "Low risk signup detected with minor concerns: Tor network. Velocity: impossible travel.",
"data": {
"checks": { "tor": true, "impossible_travel": true },
"velocity": { "user_distinct_ips_24h": 2, "user_countries_30d": 2 },
"anomalies": [
{
"code": "impossible_travel",
"severity": "high",
"detail": "usr_42 was seen from US 12 minutes ago and is now in DE."
}
]
}
}
risk_score and recommendation already include the anomaly. Code that only reads those two fields keeps working. Read data.anomalies when you want to log the reason, show it to support, or treat a particular code differently.
Storage and retention
| Plan | Events kept |
|---|---|
| Free | 7 days |
| Starter | 90 days |
| Pro | 365 days |
| Enterprise | 365 days |
Older events are deleted nightly. Each stateful call is one request against your monthly quota, the same as a stateless one. Events and anomalies are shown next to each request in the audit log.
Where to start
Add event: "signup" to your registration call first. It costs nothing extra, needs no user_id, and turns on the three burst rules that catch the bulk of free tier abuse. Add user_id to logins when you are ready for the account takeover rules. The walkthrough is in Track Signups and Logins.