Make automated abuse expensive on your signup, login and checkout. Every attempt costs the caller real work before your server sees it, and the token it produces can be spent once, on one action, from one of your own hostnames.
Three steps. You need a site key and a secret from the console.
<script src="https://api.bolrach.io/v1/guard/sdk/guard.js"></script>
<script>
var guard = BolrachGuard.create({ key: 'YOUR_SITE_KEY' });
</script>
const result = await guard.protect('login');
if (!result.ok) {
// result.reason says why. Show the visitor something; do not submit silently.
return;
}
submitWith(result.token);
protect() runs the whole exchange: it asks for a challenge, does the proof of work,
shows a picture if Guard decided to ask for one, and resolves with the token. Most visitors never
see anything.
POST https://api.bolrach.io/v1/guard/tokens/verify
Authorization: Bearer YOUR_SECRET
Content-Type: application/json
{ "token": "<the token>", "action": "login", "min_rung": 2 }
{ "valid": true, "action": "login", "rung": 2,
"hostname": "example.com", "expires_at": "2026-08-04T12:00:00.000Z" }
valid:false with reason:"already_used" — which is what a replay looks like,
and worth treating as hostile rather than retrying.
A site key (site_<env>_…) is public and belongs in your page. A
secret (bgk_<env>_…) is your server's and verifies tokens. Never put the
secret in a browser.
Both scopes are enforced when the key asks for a challenge, not merely recorded:
example.com answering from another Origin gets
hostname_not_allowed. Leave the list empty to allow any origin.login asked for checkout gets
key_not_scoped. Leave the list empty to allow every action.
A challenge is bound to the action it was issued for, so buying a cheap challenge and spending it
on an expensive action returns nonce_action_mismatch.
Header x-guard-site-key: site_…, or Authorization: Bearer site_….
| Method | Path | What it does |
|---|---|---|
POST | /v1/guard/challenges | Ask for a challenge for one action. Returns a nonce and the work required. |
POST | /v1/guard/challenges/complete | Present the solution; receive a proof token. |
POST | /v1/guard/assessments | Report signals for a request. Observation only — it gates nothing. |
POST | /v1/guard/rescue | Open a rescue for a decision you believe was wrong. Returns a reference. |
POST | /v1/guard/rescue/email | Send a verification code to an address, if this deployment can send. |
POST | /v1/guard/rescue/redeem | Redeem that reference. Single use. |
GET | /v1/guard/sdk/build | The immutable SDK URL for this build. Reference this, not the unversioned path. |
Header Authorization: Bearer bgk_…. Never from a browser.
| Method | Path | What it does |
|---|---|---|
POST | /v1/guard/tokens/verify | Verify a proof token. This SPENDS it — call it once, when you are about to act. |
Request errors carry an error code and a message. Verification is different: it
answers { valid: false, reason: … }, because your server needs to tell "expired, ask
again" apart from "replayed, treat as hostile".
| Code | Meaning |
|---|---|
missing_site_key | No site key was presented. |
invalid_site_key | Unknown, revoked or expired key. One message for all three on purpose. |
hostname_not_allowed | The key is bound to hostnames and this Origin is not one of them. |
key_not_scoped | The key is bound to specific actions and this is not one of them. |
bad_action | The action name is missing or malformed. |
work_required | A proof of work was demanded and none was presented. |
work_invalid | The solution does not meet the required difficulty. |
visible_challenge_failed | The picture was answered incorrectly. |
nonce_invalid | That challenge is unknown, already used or expired. |
nonce_action_mismatch | That challenge was issued for a different action. |
challenge_budget_exhausted | Too many failed attempts for this visitor and action. |
rate_limited | Too many requests. Back off and retry. |
rescue_unknown | That rescue reference is not valid or has expired. |
method_not_available | That rescue method is not available here. The response lists what is. |
bad_email | That address is not shaped like an email address. |
send_failed | The code could not be sent. Your allowance was not consumed. |
code_incorrect | Wrong code. The response says how many attempts remain. |
code_expired | The code has expired, or none was ever delivered. Request another. |
too_many_attempts | Five wrong codes. Request another. |
too_many_sends | Too many codes requested for this reference or address. |
| Reason | Meaning |
|---|---|
bad_signature | Not minted by us, or altered in transit. |
expired | Past its expiry. Ask the browser for another. |
already_used | Verified once already. Treat a repeat as hostile. |
action_mismatch | Minted for a different action than the one you are verifying. |
hostname_mismatch | Minted for a different hostname than the one you asserted. |
site_key_mismatch | Minted under a different site key. |
insufficient_rung | Did not reach the rung you required via min_rung. |
unknown_key | The signing key is not one of ours. |
malformed | Not a token. |
unsupported_version | A token format this deployment does not accept. |
Every blockable workflow needs a way out, and it exists before anything blocks rather than after
the first complaint. POST /v1/guard/rescue with a decision_id returns a
short reference the visitor can quote, valid for 30 minutes and usable once.
The options in that response are the methods this deployment can actually perform,
and nothing else is ever listed. Two exist:
support_review — the visitor quotes the reference and a person decides.
Redeeming records a claim awaiting review, not a confirmed error.email_verification — POST /v1/guard/rescue/email with the
reference and an address sends a six-digit code; redeem with
{ method: "email_verification", code: "123456" }. The code lasts 10 minutes and
survives 5 wrong guesses. A wrong code never destroys the reference, and a send that fails costs
the visitor nothing.Rescues appear in your insights, so a threshold can be judged against complaints rather than against nothing.