KYC & Identity Verification
The model
Verification is tiered and per-action, not a single verified/unverified flag. Two check levels exist — identity (government photo ID plus a selfie) and address (proof of address) — and a rules table decides which levels an action requires:
| Action | Default requirement |
|---|---|
| Deposit | None — free until a cumulative threshold |
| Deposit past $2,000 lifetime (USD equivalent) | Identity |
| Withdrawal, any amount | Identity |
| Withdrawal past $10,000 lifetime (USD equivalent) | Identity + Address |
| Casino launch, sports bet, receiving a promotion payout, reward-box opening | None — each can be given a requirement per region |
The thresholds and level sets are configuration, shaped after the AMLD5-style escalation real operators run; your compliance team sets the actual numbers. There are only two levels because a third gated nothing: an "enhanced" tier existed briefly, but no rule ever named it, so it offered players a check with no effect and was removed.
Cumulative totals are derived from the ledger's own intent records at check time, converted to USD at the live rate (or a recorded rate no more than twelve hours old) — there is no separate counter to drift. A missing exchange rate never skips the amount and is never guessed: for fiat and crypto alike the check refuses the action until a rate is available again, because a guessed rate could understate a total and skip verification entirely.
When a player attempts a gated action, the platform answers one of three ways: allow, needs verification (with the exact missing levels, so the frontend can start the right check), or blocked (banned region, VPN, a compliance-fatal rejection, a staff rejection, or a case under review). The "needs verification" answer names only the level still outstanding, so a player who has passed identity and lacks proof of address is asked for their address. A blocked player is told only to contact support — the reason (sanctions screen, VPN, open AML review) is never in the response, because telling a player which check they tripped is tipping-off.
Verification vendors and routing
Verification runs against a vendor-agnostic interface. On a vendor route the platform starts a case, the vendor reviews documents, and a signed webhook reports the decision — the platform never handles documents itself. On the upload route described below, it handles them entirely.
No vendor adapter ships today. The seam, the routing table and the webhook rails are built and tested, so adding a vendor is a new file rather than a rewrite — but out of the box, verification runs on document upload with your staff deciding.
Routing is region-aware. A table maps (player region, check level) to a provider, with wildcard fallbacks — so you can, for example, keep one provider globally but send one region's document checks elsewhere, and re-route without a deploy. Your team edits it on the backoffice Jurisdictions page, under Who verifies, next to the country's access rules (see Verification and Access Rules). Three destinations exist:
- Upload — no vendor at all: the player uploads documents and your staff decide. This is where the default route points. See below.
- A mock vendor for development and staging — full wire contract, no external calls, fabricates approvals, and unreachable in production.
- Manual — a virtual provider for staff decisions (below). Nothing routes to it; it exists so human decisions travel the same audited rails as vendor ones.
The route decides what the player sees. When a wallet action comes back "needs verification", or the player opens the Verification page, the app asks the platform to start that level and shows whatever surface the route resolves to: the document-upload flow for an upload route, or a hand-off message for a vendor route. Uploading or submitting documents for a level that is routed to a vendor is refused, so a region cannot be verified by posting files around its route. The vendor's own embedded check is not built into the web app yet; on a vendor route today the app shows a holding message, and development builds offer Approve and Reject buttons that play the mock vendor's decision through the real webhook. The Jurisdictions page names the upload route Manual review.
Running without a vendor contract
You do not need a verification vendor to go live, and by default you are not using one. Route a region — or everything — at the upload provider and the platform runs the whole flow itself: the player uploads documents and your staff decide in the same review queue vendor cases would land in.
Each level names what it accepts before the player photographs anything:
| Level | Accepts any one of | Also requires |
|---|---|---|
| Identity | Passport, driving licence, national ID card, residence permit | A selfie |
| Address | Utility bill, bank statement, tenancy agreement | — |
The selfie is an additional demand, not an alternative — a passport alone never satisfies identity. It is a document-consistency check rather than a liveness one: a reviewer can catch a face that does not match the document, but an upload has no camera and no challenge, so it cannot catch a real ID and a real face submitted together by someone else. Address freshness (issued within three months) is left to the reviewer's eye, because an issue date the player types is a field to lie in rather than a control.
It is a routing-table change, not a code change or a deploy, so you can start manual and move to a vendor later, or keep one region manual indefinitely.
What the platform does with the documents:
- Stores them privately. A separate, private bucket — never the public asset storage that serves game art and logos. There is no lasting link to a document: staff view one through a URL minted for that request and expiring in minutes.
- Checks what was actually uploaded. JPEG, PNG and PDF only, verified by inspecting the file's own bytes rather than trusting what the browser claims, and capped at 10 MB.
- Keeps the level closed while a person reviews, and nothing else. Submitting puts the case in an "on hold" state, but unlike a vendor or monitoring hold it blocks nothing by itself: the level is simply not approved yet, so an action that requires it (by default, a withdrawal) stays closed, while play and deposits stay open, no session is closed, and the player's status reads in progress. The player is told this before they submit, not after.
- Logs every staff view. Opening a player's document is recorded — who opened which document, and when — and the reviewer is told so on screen.
Uploading and submitting are two deliberate steps: attaching a file does not start the review.
A staff rejection is terminal for an upload applicant: further uploads and submits at that level are refused, and the player's other levels point them to support instead of offering Verify. Resubmitting an already-approved level changes nothing.
What the player sees. The Verification page in the player's settings lists both levels in one list, each with its state — verified, in review, action needed, unavailable, or not started — under a header that counts progress ("1 of 2 complete", "Fully verified"). Accepted documents and what a level is needed for show only while that level still needs sending, and the levels can be done in any order. Opening a level walks through it: the document type is chosen from the accepted list, the file goes in through a drop zone (drag-and-drop or browse) that states the type and size rules up front, and a checklist tracks what is on file — so a selfie already uploaded stays ticked when the player goes back to change the ID. The submit button sits beside the note the player should read first: that a person will review it, and what stays closed until they do.
Retention is a hook, not a policy: documents can be marked deleted (which hides them from every screen at once), but the platform ships no automatic destruction schedule, because the retention window is jurisdiction-specific and yours to set.
Region, geolocation and VPN blocking
Every player carries a region with a provenance:
- At registration and login, the caller's IP is geolocated and the region stored as IP-derived. Connections through VPNs, proxies or hosting providers are refused outright — at signup, at login, and again at every deposit or withdrawal attempt.
- A staff member can set the region by hand from the player page, with a written reason; the change is audited. A hand-set region replaces the IP-derived one and later logins no longer overwrite it.
- When a document is approved, its issuing country overwrites both and becomes authoritative — an IP can lie, a verified passport cannot. A disagreement with the IP-derived region is recorded as a risk signal for review. A document-confirmed region is locked: staff cannot override it by hand.
A region blocklist refuses gated actions from banned jurisdictions, and walls the whole site for visitors whose connection resolves to one (see Verification and Access Rules). Geolocation answers are cached and fail open — a lookup outage degrades to "region unknown" rather than locking players out — while blocklist enforcement stays strict wherever the region is known. The development fallback country is used to give a local player a region, never to wall a visitor.
The geolocation vendor is pluggable. Country lookup can run from a local MaxMind-format database (DB-IP Lite or GeoLite2) with no network call and no player IP leaving your servers; the default is a stub that answers one configured country for everyone, used for development. VPN and proxy detection needs an IP-intelligence subscription (a paid tier at every serious vendor), and no adapter for one ships, so out of the box VPN refusal does not fire. The seam is built; the contract is yours to sign.
Decisions, the gate, and why the mapping is asymmetric
Decisions — from a reviewer today, from a vendor webhook if you add one — land on the player's per-level case records. A display-level summary (verified / pending / unverified) is projected for the player's profile, and the projection is deliberately asymmetric:
| Case state | Effect |
|---|---|
| Approved, clean | Level counts toward requirements; money actions open. The profile's KYC status reads verified only when the identity level is approved; an approved address alone does not |
| Pending, or retry requested | Gate shut, but the player is shown "in progress", not "unverified" |
| Rejected with a compliance-fatal label | Blocked outright — resubmission cannot reopen a sanctions or fraud hit |
| On hold (vendor or monitoring) | Blocked — even if another level is approved |
| On hold (our own upload review) | Level not yet satisfied, nothing else blocked; shown as "in progress" |
| Rejected by staff | Blocked outright, like a compliance-fatal label |
Two rules are worth calling out:
On holdblocks money movement. That state is where ongoing AML monitoring parks an already-approved player. Anything else would leave withdrawals open during exactly the review meant to stop them.- A compliance-fatal reject label freezes the player regardless of the status it arrived on. A sanctions hit can technically arrive attached to a retryable decision; without this rule the gate would stay open on the grounds that the player could still resubmit.
The projection is written in the same transaction as the decision that justifies it, so the two can never disagree.
The player is told. When a level moves into approved, rejected, or retry-requested, the player gets an in-app notification naming the level (see Player Accounts & Notifications). Only a move notifies: a second approval or a manual grant on an already-approved level stays silent. The notice never carries a reason, and a compliance-fatal rejection that arrives as a retry request sends nothing, because inviting that player to resubmit would promise a path that cannot succeed.
The staff review queue
The backoffice has a KYC review queue: cases parked for human review — whether from a vendor or from a document upload — are listed, and staff approve or reject them with a mandatory reason. A rejection by staff is final for the player. The decision applies to the actual case — with the same replay and ordering protection as a webhook — and lands in the Tier-1 audit log. Opening a player's uploaded documents requires the personal-data permission, and each view is audited. Staff can also set a player's KYC status from the player page. Setting Verified grants the identity level; setting Unverified revokes it whichever route granted it, so an approved upload or vendor case cannot keep the player entitled behind the staff decision. Pending is not a status staff can set. These manual decisions are recorded as first-class cases under the "manual" provider, so the audit trail never has gaps. Everything is scoped to the staff member's operator.
Webhook safety
The webhook endpoint is authenticated by signature — the signature is the
authentication. It defends against replay through idempotency, and against
out-of-order delivery through an ordering guard, so a delayed pending retry
cannot close a gate that a newer approved already opened. Every decision —
vendor, staff, and risk signal — is recorded in an append-only event trail, so
the reasoning behind a player's status is reconstructable after the fact.
Scope & Roadmap
Implemented. Tiered per-action requirements with cumulative-amount escalation that refuses rather than guesses on a missing exchange rate; the vendor-agnostic provider seam with region/level routing edited on the Jurisdictions page and driving the player's verification flow, plus a dev-only mock adapter; IP geolocation (local database or stub) with VPN refusal wired at signup, login and money actions; document-country region confirmation with mismatch risk signals; the backoffice review queue and manual decisions with mandatory reasons and Tier-1 audit; the in-dialog document capture flow and a Verification settings page with per-level status; document upload with manual review that waits without blocking play or deposits, per-level document rules, private storage and audited staff access; terminal staff rejections; in-app notifications for verification decisions; staff-set player region with the document region taking precedence; revocation that reaches every approved case at a level; block reasons kept out of every player-facing response; webhook idempotency, ordering and signature defence; the append-only decision trail.
Needs your contracts. An IP-intelligence subscription for VPN and proxy detection — the country database adapter and the stub cannot detect VPNs on their own. An identity vendor account is optional rather than required: the upload route runs verification with no vendor contract at all, at the cost of your staff reviewing each case by hand, and with no automated AML screening (sanctions, PEP, adverse media) that a bundled vendor would supply.
Not built. Any live vendor adapter, and the vendor's embedded check in the web app (the seam is ready; no vendor ships); liveness detection — the selfie is a consistency check a human reads, not a challenge-response; payment-provider verification signals (the event schema accepts them; no ingestion endpoint yet); document expiry and periodic re-verification sweeps; an automatic document-retention schedule (the delete hook exists, the window is yours to set); a policy for freezing wagering (not just money movement) when an approved player is later flagged — deliberately left as an operator decision; standalone AML transaction monitoring; and regulator-shaped reporting artefacts. See What You Still Need.