Skip to content

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 once

Ondato 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 page
  • ondato_kyc_identification_id — links to the Ondato Operator Station KYC view
  • kyc_statusawaiting, approved, rejected, or reset
  • kyc_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 approved with DuplicatedInfo, the duplicate matched a prior approved KYC for that user and the user should not be sent through KYC again.
  • If Strip shows rejected with DuplicatedInfo, the duplicate was not auto-approved; escalate for manual review instead of treating the Ondato duplicate as a pass.
  • Use Sync from Ondato when Strip looks stale and a real Ondato verification or KYC identification is available.
  • True duplicate current-account merge/linking is separate future work.
  • 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_granted makes 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