Product 7 min read Markdown

Fidro Now Remembers: Velocity Counts and Anomaly Detection on Every Validate Call

Matt King
Matt King

October 2, 2026

Fidro Now Remembers: Velocity Counts and Anomaly Detection on Every Validate Call

Until today every call to Fidro was a question with no memory. You sent an email and an IP, Fidro scored them against disposable domains, VPN and proxy lists, Tor exit nodes and your blocklists, and forgot the call had ever happened. That is fine for the first signup from an address. It is useless for the fifth.

From today, POST /api/validate accepts two optional fields, event and user_id. Send them and Fidro records the call, counts related activity over the last hour, day and month, and adds the anomalies it finds to the score and the response. It is on every plan, including Free, and the Track signups and logins guide has the three-line change.

The problem with stateless checks

Most signup abuse does not arrive from a Tor exit node with a mailinator address. It arrives from an ordinary residential IP with a plausible Gmail address, then another, then another. Each one is clean on its own. The pattern is only visible across calls, and until now the only place that pattern could be seen was your own database, after the accounts already existed.

The same applies to logins. A user who has signed in from the UK three hundred times and suddenly appears from a datacenter in another country is not a bad IP problem, because the IP may be fine. It is a "this is not how this person behaves" problem, and answering it needs history.

Fidro now keeps that history for you.

What you send

Two fields, both optional:

{
  "email": "alice@example.com",
  "ip": "203.0.113.1",
  "event": "signup",
  "user_id": "usr_42"
}

event is one of signup, login, checkout, password_reset, check, or custom:<name> for anything else you want counted. user_id is your own identifier for the person, so Fidro can follow them across IPs and countries. Send user_id on its own and the event defaults to check.

A call without either field is stateless and behaves exactly as it did yesterday.

What you get back

Two new objects under data:

"velocity": {
  "ip_events_1h": 3,
  "ip_events_24h": 3,
  "ip_signups_1h": 3,
  "ip_signups_24h": 3,
  "subnet_signups_1h": 3,
  "ip_distinct_users_24h": 0,
  "email_pattern_matches_24h": 1,
  "user_distinct_ips_24h": 1,
  "user_countries_30d": 1
},
"anomalies": [
  {
    "code": "ip_signup_burst",
    "severity": "high",
    "detail": "3 signups from 203.0.113.1 in the last hour."
  }
]

The velocity counts include the current call. The anomalies are the rules that fired, each with a severity and a sentence you can show to a reviewer. Every anomaly code also appears in data.checks as true, so code that already loops over the checks picks them up without changes, and the message field gains a "Velocity: ip signup burst." suffix.

The six rules

Code Fires when Effect on the score
ip_signup_burst 3 or more signups from one IP in an hour, or 5 in 24 hours +40, recommendation at least challenge
email_pattern_burst 3 or more signups in 24 hours whose addresses reduce to one pattern (a.lice+1@, alice2@, al.ice@) +40, at least challenge
impossible_travel One user_id seen from two countries within 60 minutes +30, at least challenge
subnet_signup_burst 5 or more signups from one /24 (or /64) in an hour +20
ip_many_users 5 or more different user_id values from one IP in 24 hours +20
new_country_for_user A user with 3 or more prior events from one country appears from another +15

These add to the existing email and IP scoring, so a clean address from a clean IP that happens to be the third signup from that IP this hour scores 40 and comes back as challenge. A disposable address in the same burst goes straight to block.

The thresholds are fixed in this release. We would rather ship predictable rules now and add per-account tuning once we have seen how real traffic behaves against them.

Recommendation names

While we were in here we tidied the vocabulary. The /validate recommendation is allow (score 0 to 49), challenge (50 to 79) or block (80 and up). The Stripe chargeback side keeps its own allow, review, refund because the actions there are different. The docs, the SDKs and the MCP tool all use the real names now.

Where you see it

The audit log shows the event type, the user_id and a badge for each anomaly on every stateful call, and the request detail page has a Memory panel with the counts and the anomaly sentences. If you use the MCP server, validate_signup accepts event and user_id too, so "record a login for user 4821 from 203.0.113.9 and tell me if anything looks off" is a complete request.

Storage and privacy

One row per stateful call, scoped to your account and never shared across customers. Rows are deleted when they fall outside your plan's retention window: 7 days on Free, 90 on Starter, 365 on Pro and Enterprise. Test keys write to a separate history, so playing with the API from the playground or a test suite never affects your live counts.

Try it

With a test key, send three signup events from 1.1.1.1 with three different addresses. The third one comes back with ip_signup_burst. The test mode guide has a scenario for every rule, and the Velocity and Anomalies reference explains each count and threshold.

If you are not a customer yet, the free plan includes 200 validations a month, velocity and anomaly detection included, no card required.

Frequently Asked Questions

What changes in my integration?

Two optional fields on POST /api/validate. Send event (signup, login, checkout, password_reset or custom:name) to say what is happening, and user_id to say who it is. Nothing else changes: the score, the recommendation and the checks come back in the same shape, with data.velocity and data.anomalies added. Calls without those fields behave exactly as before.

Which anomalies does Fidro detect?

Six in this release: ip_signup_burst (3 or more signups from one IP in an hour, or 5 in a day), subnet_signup_burst (5 or more from one /24 in an hour), email_pattern_burst (3 or more signups whose addresses reduce to one pattern once plus-tags, dots and trailing digits are removed), ip_many_users (5 or more different user_ids from one IP in a day), new_country_for_user and impossible_travel (one user seen from two countries within an hour). Each one adds to the risk score and the three high-severity ones floor the recommendation at challenge.

Does this cost extra?

No. Velocity and anomaly detection are on every plan, including Free, and a stateful call counts against your quota exactly like a stateless one. What differs by plan is how long events are kept: 7 days on Free, 90 on Starter and 365 on Pro and Enterprise.

What does Fidro store?

One row per stateful call: the event type, the lowercased email and a normalised pattern of its local part, the IP and its subnet, the country, the user_id and session_id you sent, the score and any anomalies. Events are scoped to your account, never shared across customers, and deleted when they fall outside your plan retention window. Test keys keep a separate history so playing with the API never affects live counts.

Can I tune the thresholds?

Not yet. The rules and thresholds are fixed in this release so the behaviour is predictable across accounts. Per-account tuning and alerting are on the roadmap.