Bolrach Guard

Bolrach Guard

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.

Quickstart

Three steps. You need a site key and a secret from the console.

1. Load the SDK and create the instance on page load

<script src="https://api.bolrach.io/v1/guard/sdk/guard.js"></script>
<script>
  var guard = BolrachGuard.create({ key: 'YOUR_SITE_KEY' });
</script>
Create it on load, not inside your submit handler. The interaction collector starts listening the moment the instance exists. Created at submit time it begins after the visitor has finished interacting, and every interaction signal arrives as zero — measured, not theorised: the same page reported 0 pointer moves when created late and 8 when created on load.

2. Ask for a token before you submit

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.

3. Verify on your server

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" }
Verifying SPENDS the token. Call it once, at the moment you are about to act. A second verify of the same token returns valid:false with reason:"already_used" — which is what a replay looks like, and worth treating as hostile rather than retrying.

Keys and scope

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:

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.

Reference

Public plane — send your site key

Header x-guard-site-key: site_…, or Authorization: Bearer site_….

MethodPathWhat it does
POST/v1/guard/challengesAsk for a challenge for one action. Returns a nonce and the work required.
POST/v1/guard/challenges/completePresent the solution; receive a proof token.
POST/v1/guard/assessmentsReport signals for a request. Observation only — it gates nothing.
POST/v1/guard/rescueOpen a rescue for a decision you believe was wrong. Returns a reference.
POST/v1/guard/rescue/emailSend a verification code to an address, if this deployment can send.
POST/v1/guard/rescue/redeemRedeem that reference. Single use.
GET/v1/guard/sdk/buildThe immutable SDK URL for this build. Reference this, not the unversioned path.

Server plane — send your secret

Header Authorization: Bearer bgk_…. Never from a browser.

MethodPathWhat it does
POST/v1/guard/tokens/verifyVerify a proof token. This SPENDS it — call it once, when you are about to act.

Errors

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".

Request errors

CodeMeaning
missing_site_keyNo site key was presented.
invalid_site_keyUnknown, revoked or expired key. One message for all three on purpose.
hostname_not_allowedThe key is bound to hostnames and this Origin is not one of them.
key_not_scopedThe key is bound to specific actions and this is not one of them.
bad_actionThe action name is missing or malformed.
work_requiredA proof of work was demanded and none was presented.
work_invalidThe solution does not meet the required difficulty.
visible_challenge_failedThe picture was answered incorrectly.
nonce_invalidThat challenge is unknown, already used or expired.
nonce_action_mismatchThat challenge was issued for a different action.
challenge_budget_exhaustedToo many failed attempts for this visitor and action.
rate_limitedToo many requests. Back off and retry.
rescue_unknownThat rescue reference is not valid or has expired.
method_not_availableThat rescue method is not available here. The response lists what is.
bad_emailThat address is not shaped like an email address.
send_failedThe code could not be sent. Your allowance was not consumed.
code_incorrectWrong code. The response says how many attempts remain.
code_expiredThe code has expired, or none was ever delivered. Request another.
too_many_attemptsFive wrong codes. Request another.
too_many_sendsToo many codes requested for this reference or address.

Verification reasons

ReasonMeaning
bad_signatureNot minted by us, or altered in transit.
expiredPast its expiry. Ask the browser for another.
already_usedVerified once already. Treat a repeat as hostile.
action_mismatchMinted for a different action than the one you are verifying.
hostname_mismatchMinted for a different hostname than the one you asserted.
site_key_mismatchMinted under a different site key.
insufficient_rungDid not reach the rung you required via min_rung.
unknown_keyThe signing key is not one of ours.
malformedNot a token.
unsupported_versionA token format this deployment does not accept.

When Guard gets someone wrong

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:

Email verification proves inbox control, not humanity. It is the same shape of defence as the proof of work: it costs an automated attacker a mailbox per attempt. That is a real cost and a real bound, and it is not a person.

Rescues appear in your insights, so a threshold can be judged against complaints rather than against nothing.

What Guard is not