Korzen
Bot Protection

Protecting a checkout

Payment, login and sign-up routes: require the tag's token in Node, Go, Python, PHP, Java, .NET or a Cloudflare Worker, start in monitor mode, and know who it stops.

Card testing is a script that tries stolen cards with small payments, one after another, to find which still work. The script usually never loads your page. The tag never runs, so the decision has no signal from a browser, and its class is unknown, which is allowed. A missing signal is never evidence of a bot, so Bot Protection does not call it one. On the actions such a script goes after, ask for proof of a real visit instead: the tag's token.

What requireToken does

With "require_token": true in the call (the SDKs' requireToken option, in Go RequireToken), the action goes on only with a valid token the tag earned in that browser. Without one, the answer is challenge with token_required true, whatever the class; the class itself does not change. The form's post spends the token, once per session and path an hour; up to three tries within two minutes are the same attempt, so a declined card and a second card still go through. A page load (GET, HEAD) spends no token, and the SDKs send the option with actions only. The rules in full: an action that needs the tag.

Put the tag on the page each action starts from: the checkout page, the login page, the sign-up page. The tag

Which routes

  • Payment: the post that charges the card or places the order, and any that adds a card or redeems a gift card. Card testing.
  • Login: the post that checks the password. Credential stuffing.
  • Sign-up: the post that creates the account. Fake accounts.

Leave it off the routes no page of yours leads to: your payment provider's webhooks, your mobile app's API, a partner's server. No tag runs there, so every request would be challenged.

In your code

Each tab adds it to the call its framework's guide sets up. Node and Go take the option of the client file Korzen serves; the others send the field in the call's body.

Node

// Express, with korzen.js as the Express guide sets it up
const guard = protect({ client: korzen, ip: (_get, req) => req.ip, requireToken: true });

app.get("/checkout", guard, showCheckout); // the page: a page load spends no token
app.post("/checkout", guard, placeOrder); // the action: it needs the tag's token

// Next.js, Hono or any handler of a Web Request
export const POST = withKorzen(placeOrder, { client, ip: ipFrom.vercel, requireToken: true });

Go

mux.Handle("POST /checkout", bot.Protect(http.HandlerFunc(placeOrder), korzen.Options{
	IP:           korzen.RemoteIP,
	RequireToken: true, // the action needs the tag's token
}))

Python

# In decide() of the Django and Flask or FastAPI guide: one more field in the body.
json={
    # ...the fields the guide sends
    "path": path,
    "method": "POST",
    "require_token": True,  # on payment, login and sign-up
},

PHP

// In decide() of the PHP or Laravel guide: one more field in the body.
'path' => $path,
'method' => 'POST',
'require_token' => true, // on payment, login and sign-up

Java

// In decide() of the Spring Boot guide: one more field in the body.
body.put("path", path);
body.put("method", "POST");
body.put("require_token", true); // on payment, login and sign-up

.NET

// In Decide() of the ASP.NET Core guide: one more field in the body.
["path"] = path,
["method"] = "POST",
["require_token"] = true, // on payment, login and sign-up

Ruby

# In decide of the Rails guide: one more field in the body.
path: path,
method: "POST",
require_token: true # on payment, login and sign-up

Cloudflare Worker

# wrangler.toml: the paths whose actions need the tag's token, by commas;
# one ending in * is a prefix.
[vars]
KORZEN_REQUIRE_TOKEN = "/checkout,/login,/signup"

When the same function asks for every action, give it the choice: pass true from the payment, login and sign-up handlers and false from the rest. A misspelt field, such as requireToken in the body, is refused with 400 and lets the visitor through, as every error does: check the first answers.

Monitor mode first

Turn it on while the account is in monitor mode. Every answer's action stays allow, so nobody is stopped; an action that came without a token answers intended_action challenge with token_required true. Watch two numbers for a few days:

  • the decisions on these routes with token_required true, in the answers your server logs;
  • the share Without a token in the integration health on the Set-up page. A high share there usually means a page that leads to the action has no tag, or a form sent before the tag has run: fix those first.

What is left is mostly scripts, and some people whose browser does not run the tag (below). Turn on block mode when that share is one you accept. Monitor and block mode

The actions for each class

The defaults suit a checkout. The class's action and the token are separate: with a valid token, the action is the class's; without one, it is at least challenge.

ClassActionOn a checkout
human highallowgo on
humanallowgo on
unknownallowgo on: with a token required, a script that skipped your page is challenge instead
botchallengeyour own check, such as 3-D Secure or an email code, or our challenge page from the checkout page
bot highblockrefuse with the message a declined card shows, so the script learns nothing

With the decoy turned on for bot and bot_high on the Set-up page, the SDKs answer a decoyed action on their own, with what looks like success, so the script learns nothing. A decoy answer

After the payment, send your payment provider's outcome as feedback: a chargeback or a confirmed fraud lowers the next decisions for that browser, a good customer lifts them.

Who it stops

A visitor whose browser does not run the tag holds no token: an ad blocker or a privacy extension that blocks it, a DNS filter that blocks Bot Protection's API host, JavaScript turned off. On a route with require_token, in block mode, that visitor is challenged on the action. Our challenge page asks for a small proof of work and for our tag to run there. With JavaScript off it has no way past, and a DNS filter that blocks our host blocks the page too. That person cannot pay unless you give them another way.

  • Send it on the routes where a script costs you more than a stopped person: payment, login, sign-up. Never on every route.
  • Read the share in monitor mode before you turn on block mode, as above.
  • Under challenge, offer a way on that needs no tag of ours, if you have one: 3-D Secure, an email or phone code, a support contact. The SDKs' onChallenge hook runs it instead of the redirect.

When Bot Protection is slow or down, your server lets the visitor through, token or not: the call fails open.