Core Concepts

Velocity and Anomalies

How Fidro counts what it has seen, the six anomaly rules, what each one adds to the score, and how events are stored and kept.

6 min read Last updated October 2, 2026 View as Markdown

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:

  1. Runs the usual checks and produces the base risk_score.
  2. 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.
  3. Counts velocity over your account's own events (including this one) and returns the counts in data.velocity.
  4. 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.checks and at the end of message.

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.