Checkout Bouncer

Notes

Velocity checks, and choosing what to count

A velocity check counts repeated events and reacts when a threshold trips. The interesting part is not the counting, it is choosing what counts as the same thing.

6 min read

A velocity check counts how often the same thing happens, then reacts when the count crosses a line. So velocity checks are among the oldest fraud controls in payments, and the idea fits in one sentence. Deciding what counts as "the same thing" takes rather longer. Because that decision is the entire control, this note is about the counting key rather than the counting.

What a velocity check actually counts

Three parts make one up. First, a key decides whose events land in the same bucket. Then a window decides how far back the bucket reaches. Finally a threshold decides how full it gets before something happens.

Stripe's Radar names all three in its rule syntax, and its list of supported attributes reads like a catalogue of keys. For instance, total_charges_per_email_hourly holds "The total number of charges from this email in the past hour on your account." Equivalents exist per card number, per IP address and per customer.

Even the window is looser than its name suggests. Stripe's rules reference spells out that "hourly is up to 3900 seconds (5 minute buckets)". Worth knowing before you reason about an exact edge.

Not the same thing as rate limiting

Rate limiting is one specific implementation of this idea. WooCommerce core counts requests per user or per IP address on the Store API and refuses the excess, and that limiter ships with WooCommerce and stays off by default. Velocity checks are the wider question underneath it, which is what to count in the first place.

The candidate keys, and what each identifies

Most velocity checks pick their key from a short list. Each identifies something slightly different, and the differences matter more than the threshold does.

  • Card number. An instrument, and the thing actually under attack.
  • Email address. A claim rather than a person, since anyone can type anything.
  • IP address. A path through a network, not a household and not a device.
  • Billing address. A place, which several real people share.
  • Basket contents. Intent, which is why the same cheap item ordered forty times reads oddly.
  • Account record. Whoever logged in, and only where login is compulsory.

So a counter keyed badly is not a weak control. It is a control aimed at the wrong noun.

Why IP is the obvious key and the weakest one

Almost every counter starts at the IP address, because it arrives free with the request and needs nothing from the visitor. But an address describes a route. It does not describe a shopper.

Carrier-grade NAT ended the tidy version of that assumption. RFC 6269, which catalogues the consequences of address sharing, puts it plainly: "the IPv4 address no longer uniquely identifies a subscriber". It then names the exact failure a counter produces, in the RFC's own list of issues with address sharing: "one user who fails a number of login attempts may block out other users who have not made any previous attempts but who will now fail on their first attempt".

Mobile networks and large offices share addresses like this as routine. Meanwhile a CDN or a reverse proxy creates the mirror image, where every shopper wears the proxy's address unless your server digs the real one out of a header. Either way the key is wrong, and the address on a WooCommerce order is a header rather than the network, so confirm yours before counting by it.

Email costs an attacker nothing to vary

Counting by email looks tighter, and against a careless script it is. Still, an email address is a string the sender chooses. Adding one character before the @ mints a fresh key and an empty bucket, so a per-email counter measures how lazy a script is rather than how much traffic it sends.

Stripe makes the general point in its guidance on protecting yourself from card testing: "simple firewall rules or filters based on a single heuristic such as IP addresses are usually not sufficient to prevent card testing on their own." That reasoning survives whichever single key you pick.

The strongest key is the one your store never holds

Card number is the key that identifies the thing being tested. Because a stolen card costs money and cannot be edited on a whim, a bucket keyed on it survives every free variation.

Your store, though, never sees that number. Your gateway does, and it publishes a stand-in. Stripe's support documentation describes a fingerprint as "a unique identifier for a given card number or bank account in a Stripe account." Radar counts on that stand-in directly, through attributes such as declined_charges_per_card_number_hourly.

Consequently the strongest velocity checks around your checkout are usually not yours. They sit at the gateway, they run after the card leaves your server, and your own counters make do with weaker keys. Knowing which layer does what beats skipping your own. Your order notes carry the gateway's account of each attempt, which is where the two layers meet.

Windows, thresholds and the fumbled card

Once the key is settled, the window and the threshold decide who gets caught. Tighten them and you catch slower scripts, plus more of your own customers. Loosen them and the trade runs the other way.

Picture a shopper with a new card. Wrong expiry on the first go. Old CVC on the second. Missed bank prompt on the third. Three attempts in two minutes, from one address, on one email, with one basket, and every key you might have chosen has just counted an honest customer as an attacker.

Because declines are the interesting signal, the sharper velocity checks count declines rather than attempts. A fumbled card produces one or two. A script produces a run. Reading your own order evidence tells you what a normal run looks like on your store, and no published figure can.

Where velocity checks stop working

Now the honest limit. Velocity checks count per key, so an attacker who spreads the work across many keys never fills a bucket. A thousand attempts from a thousand addresses, each with its own email and card, trips nothing. Every counter reads one.

Nor is that exotic. Stripe's own recommendation is to combine mitigations rather than lean on one, listing counters alongside CAPTCHA, endpoint restrictions and session validation. A velocity check is only as good as the key it counts on. Distributed traffic simply refuses to share a key.

Where Checkout Bouncer fits

Checkout Bouncer counts nothing. Instead it scores each checkout submission with Google reCAPTCHA v3, judging how the visitor behaved on the way to the button, so a script gets judged on its first attempt rather than its fifth. Velocity checks need a history before they can act. Behaviour scoring does not.

Its limits are worth stating. The plugin guards the WooCommerce checkout only, never login, registration, comments or contact forms. Nor is it a firewall or a malware scanner. And it uses reCAPTCHA v3, never v2 or any other provider. Free and GPL, it lives on WordPress.org.

So the two belong together, because they fail in different directions. If you want to know which checkout routes on your store currently answer, the free scanner takes about two minutes.

See which doors into your checkout are standing open