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

> A clean email from a clean IP is still a problem when it is the fifth signup from that IP this hour. Tell Fidro what is happening and who it is, and it counts repeat activity, spots bursts and impossible travel, and raises the score before the account exists.

**Author:** Matt King | **Published:** October 2, 2026 | **Category:** Product

---

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](/docs/guides/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:

```json
{
  "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`:

```json
"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](/mcp), `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](/docs/guides/test-mode-and-test-data) has a scenario for every rule, and the [Velocity and Anomalies](/docs/guides/velocity-and-anomalies) reference explains each count and threshold.

If you are not a customer yet, the [free plan](/register) 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.

