KYC
KYC
Purpose
Document identity verification behavior for NSFW character creation.
Participants
- Brisket initiates KYC-related user action and displays verification status.
- Sirloin owns account-level KYC state relevant to NSFW access.
- Ondato handles external identity verification.
- S3 stores KYC image artifacts used for NSFW character verification.
- Clerk supplies the user email that Sirloin passes to Ondato for the verification record.
- Strip displays account-level KYC status with links to the Ondato IDV page and Operator Station.
Sequence
sequenceDiagram participant Brisket participant Sirloin participant Clerk participant Ondato participant S3 Brisket->>Sirloin: Start KYC verification Sirloin->>Clerk: Look up user email Sirloin->>Ondato: Create verification (with email) Sirloin-->>Brisket: verification URL / ID Brisket->>Ondato: User completes verification Ondato-->>Sirloin: Verification callback/status Sirloin->>S3: Store KYC image artifact Sirloin->>Sirloin: Gift draft NSFW real character onceOndato Email Propagation
When Sirloin starts a KYC verification (startkycverification.go), it fetches
the user’s primary email from Clerk and sends it in the email field of
Ondato’s POST /v1/identity-verifications request. This makes the user email
visible in the Ondato dashboard for identity management. If the Clerk lookup
fails (e.g. missing API key in test environments), the email is omitted from
the request and verification continues normally.
Strip Operator Visibility
Strip surfaces account-level KYC details on the user details modal
(userdetails.templ). The StripUser message includes four fields populated
from users.kyc_verifications (the current row, is_current=TRUE):
ondato_verification_id— links to the Ondato IDV verification pageondato_kyc_identification_id— links to the Ondato Operator Station KYC viewkyc_status—awaiting,approved,rejected, orresetkyc_status_reason— free-text reason (e.g.DuplicatedInfo)
These are populated via scalar subqueries in StripListUsers so they add no
additional round-trips. The operator UI shows a status badge and conditional
links to Ondato IDV and OS.
Strip KYC bypass is account-level. Eligible operators can approve the current
user KYC row from the user details modal; Sirloin records a synthetic
strip-... Ondato verification ID with status=approved. Character-local
Ondato columns remain for compatibility with in-flight sessions and historical
fallbacks only. Strip account-level sync lives on the user details modal; the
old character-level Ondato sync route and control have been removed.
Account KYC is authoritative for current KYC actions and NSFW gates. Approved account KYC does not cascade verification state to character records; it does idempotently create one draft NSFW real character for users without an existing NSFW character. The KYC handler can reuse a verified character’s Ondato verification ID only when the user lacks their own current account KYC record.
Duplicate KYC Outcomes
Ondato can reject a KYC identification with status_reason=DuplicatedInfo.
That reason alone is not enough to approve local account KYC. Sirloin only
promotes Rejected + DuplicatedInfo to approved account KYC when the same
user_id already has an approved, real Ondato-backed KYC row in
users.kyc_verifications. Without that same-user prior approval, Sirloin stores
the result as rejected and keeps kyc_status_reason=DuplicatedInfo visible in
Strip.
CS checklist for duplicate reports:
- Check Strip user details first; account KYC is the source of truth.
- If Strip shows
approvedwithDuplicatedInfo, the duplicate matched a prior approved KYC for that user and the user should not be sent through KYC again. - If Strip shows
rejectedwithDuplicatedInfo, the duplicate was not auto-approved; escalate for manual review instead of treating the Ondato duplicate as a pass. - Use
Sync from Ondatowhen Strip looks stale and a real Ondato verification or KYC identification is available. - True duplicate current-account merge/linking is separate future work.
Source Links
- apps/brisket/src/server/api/routers/kyc.ts
- apps/brisket/src/app/verification/page.tsx
- apps/sirloin/internal/app/services/characters/createcharacter.go
- apps/sirloin/internal/app/services/kyc/startkycverification.go
- apps/sirloin/internal/pkg/storage/characters.go
- apps/sirloin/internal/pkg/ondato/client.go
State Transitions
Users start verification from Brisket’s api.kyc router. Sirloin records the Ondato verification ID and KYC state in users.kyc_verifications, with one current row per user. Character-local Ondato columns remain for compatibility with in-flight sessions and legacy fallback reads, but account KYC is authoritative for new NSFW gates.
Users may reset approved account KYC only before any successful 18+ generation on an active 18+ character. Eligibility and the reset RPC both count non-deleted NSFW media with MEDIA_STATUS_AVAILABLE on non-deleted 18+ characters; deleted media and media on deleted 18+ characters do not block reset. There is no manual-photo marker requirement.
Invariants
- NSFW character creation requires approved account KYC.
- Sirloin owns the account KYC state that gates NSFW character creation and generation.
- Strip test bypass approves account KYC, not a single character.
- Approved account KYC creates at most one draft NSFW real character;
users.credits.kyc_fac_bonus_grantedmakes this idempotent across webhook and poller retries. - Ondato is the external verifier.
- User email is propagated to Ondato on verification creation for dashboard visibility.
Error Paths
Missing approved account KYC blocks NSFW character creation and generation with FailedPrecondition. Reset is denied with FailedPrecondition after any successful 18+ generation.
Tests And Verification
- cd apps/sirloin && make run-tests
- cd apps/brisket && pnpm test