feat: age verification with an ID document (Didit), and closed registration
Pull request / title (pull_request) Successful in 1s
CI / test (pull_request) Successful in 58s
CI / image (apps/api/Dockerfile, t5de-hub-api) (pull_request) Skipped
CI / image (apps/web/Dockerfile, t5de-hub-web) (pull_request) Skipped

Age: a Didit provider behind the vendor-neutral age interface. A person
verifies once (ID document, passive liveness, face match) and all their
linked IMVU accounts count as adult. Webhooks are checked against Didit's
signatures; only "over 18" is kept. A later answer replaces an earlier one
(an approval taken back is revoked). Checks are limited: one unfinished
check per person, 3 in 30 days, none once verified, and a monthly cap
(DIDIT_MONTHLY_LIMIT, default 30: Didit's free plan is $10 of credit a
month and a check costs $0.30). The Account page gets an Age Verification
card.

Registration: open or closed, an admin's setting in the console
(default from REGISTRATION). Closed: new accounts can only sign in until
a moderator approves them (or declines, deleting the account); the
website shows them a waiting page and the API treats them as signed out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-11-03 04:15:47 -08:00
1 parent 86ebe976ba
commit c75a4c9afc
28 files changed
+3260 -89

No files matched your search

+22 -5
View File
@@ -100,6 +100,9 @@ age, not verified) and `tagline`.
- **Switching accounts in the client**: the proxy sees which IMVU account logs in and tells the hub. Not linked:
offer to link. Linked to another hub account: no community features that session.
- **Bans** apply to the person, so all linked accounts.
- **Registration** is open or closed, an admin's setting in the console (default from `REGISTRATION`; staging is
meant to be closed, for testers only). Closed: anyone can sign up, but the account can only sign in until a
moderator approves it (or declines it, deleting it); the hub treats it as signed out everywhere else.
## Age (**Decided**: free, several providers, vendor-neutral)
@@ -112,8 +115,11 @@ No payment for verification (adult content rules out mainstream merchants of rec
2. **Phone wallets** (planned): a government ID in Google Wallet, Apple Wallet or an EU age-verification wallet,
asked for "age over 18" only, through the browser's Digital Credentials API. Free for the hub; no documents or
birth date reach it. Scope: the person.
3. **ID document and selfie** (planned): Didit's hosted check, within its free monthly allowance. Scope: the person
(the liveness step stops someone using another person's ID).
3. **ID document and selfie** (live, `didit`): Didit's hosted check: ID document $0.15, passive liveness $0.10 and
face match $0.05, so $0.30 a check, paid from the free plan's $10 of credit a month (since 2026-11-01; the 500
free checks a month are gone), so about 30 a month unless topped up. Scope: the person (liveness and face match
stop someone using another person's ID). Over 18 is worked out from the age Didit reads off the document;
nothing else is kept. With so few checks, phone wallets (free) should become the main way.
### How it's built
@@ -128,14 +134,25 @@ No payment for verification (adult content rules out mainstream merchants of rec
webhook), and optionally `recheck` (for providers whose answer can change, like IMVU). Which ones run is
configuration (`AGE_PROVIDERS`), with each provider's keys in its own settings. Adding or replacing a vendor is
one new adapter; nothing else changes.
- Webhooks arrive at `/api/v1/age/webhook/<provider>` and each provider checks its own signature.
- Webhooks arrive at `/api/v1/age/webhook/<provider>` and each provider checks its own signature. The provider's
latest answer stands: one taken back after approval (say, by a manual review) becomes `revoked`.
- **Limits** on checks of a person, for every provider: one unfinished check at a time (starting again gives the
same one), at most 3 started in 30 days, none once verified, and a provider's monthly cap (`monthlyLimit`; Didit's
`DIDIT_MONTHLY_LIMIT`, default 30), counted from the checks started this month (UTC).
- People are sent back only to the hub's website (`returnURL` is a path on it, or an address on `WEB_URL`).
- Enforced at three points: catalogs (adult files need an adult token), the hub (adult items left out of room
channels for viewers who aren't adult and opted in), and the viewer's proxy.
### Setting up Didit
In Didit's console (business.didit.me): an application; a KYC workflow with ID verification, passive liveness and
face match only (no IP analysis, AML or proof of address, which cost extra; no white label, $0.10 a session more);
an API key; and a webhook destination
`<API_URL>/api/v1/age/webhook/didit` with its secret. Then the API's environment: `AGE_PROVIDERS=imvu,didit`,
`DIDIT_API_KEY`, `DIDIT_WORKFLOW_ID`, `DIDIT_WEBHOOK_SECRET`. Use the sandbox application on staging.
### To add the planned providers
- Didit: an account, an API key and webhook secret, and a workflow set to ID document plus liveness, returning
only "over 18". Confirm its free monthly allowance covers age checks.
- Wallets: Google's Digital Credentials verifier registration (free), and an `age_over_18` request (OpenID4VP,
mdoc); Apple and the EU wallet through the same browser API where supported.