Integration docs

FanVerify follows the reCAPTCHA v2 integration contract on purpose. If your checkout already has a CAPTCHA, swapping it in is a find-and-replace.

Quickstart

  1. Get a site key and secret. The demo pair below works against this deployment.
  2. Add the script and a container to your checkout or presale page.
  3. On form submit, post the fanverify-response field to your server.
  4. Your server calls /api/v1/siteverify and gates inventory on the result.
Site key (public):   fv_pk_demo_6LcDemo
Secret (server-only): fv_sk_demo_change_me

Client widget

Auto-render: any element with class fanverify and a data-sitekey becomes a badge. Clicking it opens the challenge in a modal iframe. On success the widget writes a hidden input named fanverify-response into the container and calls your callback.

html
<script src="https://YOUR-FANVERIFY-HOST/fanverify.js" async defer></script>

<form action="/checkout" method="POST">
  <div class="fanverify"
       data-sitekey="fv_pk_demo_6LcDemo"
       data-context="taylor-swift"
       data-theme="dark"
       data-callback="onFanVerified"
       data-expired-callback="onFanExpired"></div>
  <button type="submit" id="buy" disabled>Buy tickets</button>
</form>

<script>
  function onFanVerified(token) { document.getElementById("buy").disabled = false; }
  function onFanExpired() { document.getElementById("buy").disabled = true; }
</script>

Attributes

data-sitekeyRequired. Your public site key.
data-contextKnowledge pack id, e.g. oasis. Determines what the fan is quizzed on.
data-themelight (default) or dark. Affects the badge only.
data-difficultyeasy | medium | hardcore. Demo-only: production sites pin one difficulty per event in site settings and the server ignores the client value.
data-label / data-sublabelBadge copy. Defaults: “I'm a real fan” / “Verify to continue”.
data-callbackGlobal function name, called with the token.
data-expired-callbackCalled when a token ages out (110 s) before you used it.
data-error-callbackCalled when the fan fails the challenge.

Server verification

Never trust the token in the browser. Exchange it server-to-server. Tokens are signed, expire two minutes after issue, and can be redeemed once.

sh
curl -X POST https://YOUR-FANVERIFY-HOST/api/v1/siteverify \
  -d secret=fv_sk_demo_change_me \
  -d response=<fanverify-response>

Response fields

{
  "success": true,
  "score": 1.0,                      // 0–1. fraction of rounds correct
  "context": "oasis",                // pack the fan was verified against
  "difficulty": "medium",            // preset the challenge was built with
  "lane": "fast",                    // "fast" = passed; "slow" = failed, then waited out the slow lane
  "hostname": "tickets.example.com", // page that embedded the widget
  "challenge_ts": "2026-09-10T14:02:11.392Z",
  "rounds": { "total": 3, "correct": 3 },
  "flags": []                        // e.g. ["r1:low_pointer_activity"]
}

// failure
{ "success": false, "error-codes": ["timeout-or-duplicate"] }
missing-input-secret / invalid-input-secretSecret absent or unknown.
missing-input-response / invalid-input-responseToken absent, malformed, or signature failed.
site-key-mismatchToken was issued for a different site key than this secret.
timeout-or-duplicateToken expired (2 min) or already redeemed.
rate-limitedToo many calls from this IP.

Recommended policy: score ≥ 0.67 to buy, and cap quantity lower when flags is non-empty. For high-demand on-sales, route score = 1.0 to the fast lane.

Contexts (knowledge packs)

Promoters and artists set the questions. A pack is owned by the artist, label or promoter. You can add any fact you like, remove anything you don't want asked, write questions for a single tour or on-sale, and choose the difficulty and round mix per event. The packs in this demo are just starting points.

A context is a JSON knowledge pack: albums with release dates and running orders, non-album tracks used as distractors, free-text facts with accepted spellings, and year facts. Six generators compose rounds from it. Adding an artist means adding a pack; no new code.

idnamealbumsfacts
oasis🎸 Oasis726
taylor-swift Taylor Swift1225
maisie-peters🪄 Maisie Peters524
coldplay🪐 Coldplay1032
// src/lib/fanverify/packs/<artist>.ts
export const artist: KnowledgePack = {
  id: "artist", name: "Artist", tagline: "World Tour", glyph: "🎤", hue: 300,
  yearRange: [1990, 2026],
  albums: [{ id: "debut", title: "Debut", released: "1999-05-04", tracks: ["…"], artwork: "https://…/debut.jpg" /* optional */ }],
  nonAlbumTracks: ["B-side", "Standalone single"],
  facts: [{ id: "drummer", prompt: "Who drummed on Debut?", answers: ["Name", "Surname"], difficulty: 2 }],
  yearFacts: [{ id: "formed", prompt: "Year formed?", year: 1995, difficulty: 2 }],
};

Content policy: titles, dates, personnel, running orders and venue facts by default. No lyrics, no audio. Album artwork is optional and only for rights holders: set artwork on an album and the sort and match rounds show your real sleeves; leave it out and the widget draws a generated one. FanVerify never ships third-party covers itself.

Security model

  • Stateless, sealed challenges. Solutions travel inside an AES-256-GCM sealed challenge token; progress travels in a sealed progress token. Both are opaque to the client and bound to each other.
  • One-shot rounds, strict sequence. A round can be answered once, in order. Out-of-sequence or replayed answers are rejected.
  • Server-side timing. Time limits are enforced from server timestamps, with a 5 s network grace. Implausibly fast answers are flagged for the vendor, never marked wrong: a fan who knows the answer instantly is not a bot.
  • Behavioural signals. Drag rounds require a real drag gesture (pointer or keyboard). Low pointer activity, no typing and very fast answers are reported as flags in the siteverify response for the vendor's own policy. Counts only, never coordinates or content, and nothing is stored.
  • Signed, single-use, short-lived result tokens. HMAC-SHA256, 2-minute TTL, redeemed once at siteverify.
  • Rate limits on challenge creation, answers, and siteverify per IP.
  • Slow lane. A fan who fails can wait (5 minutes by default, per site) and is then verified with lane: "slow" and the threshold score. The wait is enforced server-side via a sealed ticket that cannot be redeemed early. Vendors can cap quantity or skip presale perks for slow-lane buyers.

What this does not claim: that a determined human with a search engine can never pass. That is by design. The goal is to make automation slow, flaky and expensive enough that bulk scalping stops paying, while a real fan finishes in under a minute.

Self-hosting

pnpm install
FANVERIFY_SECRET=$(openssl rand -hex 32) pnpm dev   # http://localhost:3000

# production
vercel env add FANVERIFY_SECRET   # long random string; rotating it invalidates in-flight tokens
vercel --prod

Site keys live in src/lib/fanverify/sitekeys.ts for the demo; move them to a database for multi-tenant use. Rate limiting and single-use tracking are in-memory per instance; swap store.ts for Redis to make them global.