Skip to content

Chapter 10: Identity Verification

Strategic Decision: Capxul Owns Identity Verification

Section titled “Strategic Decision: Capxul Owns Identity Verification”

This is the most important decision in this chapter and it is already made.

HoneyCoin operates as a B2B payment infrastructure provider. Their model works like Paystack or Flutterwave: HoneyCoin verifies Capxul as a business customer, and Capxul is contractually responsible for knowing its end users. If a payout is later flagged as suspicious, HoneyCoin points to their agreement with Capxul. Without its own KYC records, Capxul has no defense.

If identity verification is delegated to each ramp provider, the facade pattern breaks at the most user-visible layer. Adding a new provider would require every user to re-verify. Swapping a provider mid-transaction is impossible. When Capxul owns verification, users verify once. The provider never needs to know about user identity.

Owning verification data gives Capxul leverage with providers. Pre-verified users may unlock higher limits, lower fees, or faster settlement.

Open Question: HoneyCoin’s KYC Requirements

Section titled “Open Question: HoneyCoin’s KYC Requirements”

If HoneyCoin independently requires individual-level verification for certain operations (e.g., virtual account creation), the architecture supports dual verification: Capxul’s KYC via Shufti Pro for platform-level access, plus a HoneyCoin-specific step if needed. This must be resolved on the HoneyCoin call. Most likely, HoneyCoin’s B2B API requires no individual KYC — Capxul’s own verification is sufficient.

Verification Tiers and Progressive Disclosure

Section titled “Verification Tiers and Progressive Disclosure”

Capxul’s verification framework is jurisdiction-agnostic and tiered. Verification is triggered by fiat involvement and scales with transaction volume, not by platform participation. Each jurisdiction’s thresholds are configured in the jurisdictionThresholds table in Convex.

Tier 0: No Verification. Required for progressive disclosure Levels 0-2 when the user does not touch fiat. Employees, vendors, and contractors who only interact with crypto need zero verification. Data collected: email and org membership.

Tier 1: Basic Verification. Required for first fiat off-ramp at low volumes. Data collected: full legal name, date of birth, phone number, country. Verification method: database lookup (seconds, no document upload). Jurisdictional thresholds define per-transaction and cumulative limits per country — see Appendix C for launch market values.

Tier 2: Standard Verification. Required when off-ramp volumes exceed Tier 1 thresholds. This is where most regular employees land. Data collected: everything from Tier 1 plus government-issued photo ID and selfie with liveness check. Verification: document OCR + liveness detection via Shufti Pro, cross-referenced against government databases (Nigeria BVN/NIN, Kenya IPRS, Ghana GhanaCard system).

Tier 3: Enhanced Verification. Required for high-volume transactions, specific jurisdictions, or PEP/sanctions flags. Data collected: everything from Tier 2 plus proof of address, source of funds declaration, AML screening. Exists in the data model but not expected to be common at launch.

Trigger: when the org touches fiat. An org that only operates in USDC needs no KYB.

Data collected: business registration number (varies by jurisdiction), company name and address, director/UBO information, business type, certificate of incorporation. See Appendix C for registry details per launch market.

Verification: Shufti Pro’s KYB module checks against corporate registries across 250+ jurisdictions. UBO verification triggers individual KYC on identified beneficial owners.

LevelVerification Required
0 (No account / Added by org)None
1 (Basic account / Claim-ready)None
2 (Payment-ready / Off-ramp configured)Tier 1 individual KYC, or KYB for orgs enabling fiat
3 (Full compliance)Tier 2/3 triggered when volume approaches Tier 1 threshold

Key UX principle: Verification is never a wall. It is a gate that appears only when needed, proportional to the action. An employee off-ramping $20/month may never see a document upload screen.

Same facade pattern as fiat ramp and bridge. Shufti Pro is the sole provider at launch.

  • initiateIndividualVerification(params) — starts KYC session. Returns session URL or widget config.
  • initiateBusinessVerification(params) — starts KYB session.
  • getVerificationStatus(sessionId) — polls status: pending, in_review, approved, rejected, expired.
  • parseWebhook(payload, signature) — normalizes provider webhook.
  • getVerificationDetails(sessionId) — retrieves detailed results for record-keeping.

Convex table verificationProviders: provider ID, supported types (kyc_tier_1/2/3, kyb), supported countries, API config, active flag, priority. At launch: Shufti Pro, all types, covering the initial launch markets. See Appendix C for country-specific coverage.

Source of truth for all verification state. Provider records are secondary.

Record ID, actor type (individual/organization), actor ID, verification type (kyc_tier_1/2/3, kyb), status, provider ID, provider session ID, country code, initiated/completed/expires timestamps, rejection reason/code, verified data (structured object), attempt count, previous record ID (for re-verification chains).

not_started -> pending -> in_review -> approved
| | |
v v v
expired rejected expired
|
v
not_started (retry)

Tier 1 database lookups may skip in_review (near-instant results). Approved verifications expire after 12-24 months and trigger re-verification.

Every fiat-touching operation calls getActorVerificationLevel(actorType, actorId, countryCode):

  1. Query verificationRecords for the actor
  2. Find highest tier where status is approved and expires_at is in the future
  3. Return effective tier (0/1/2/3)

If insufficient: reject with { code: "VERIFICATION_REQUIRED", requiredTier, currentTier, reason }. The frontend routes the user to the appropriate flow.

The verification check runs before any provider interaction:

  • Employee off-ramp: Check individual KYC tier against jurisdictional thresholds
  • Org off-ramp/on-ramp: Check org KYB status
  • Auto-routing: Check on every execution (verifications expire between grant and execution)

No quote is requested, no provider deposit address is generated, until the user is verified.

The indexer does not directly interact with verification. Cumulative volume per user per jurisdiction is computed from fiatTransactions records in Convex. When a new off-ramp is requested, the backend sums completed off-ramps for the current period and checks against thresholds.

Receipts generated on off-ramp completion can include verification metadata for audit: “This payout was made to a user verified at Tier 2 (record ID: xyz, verified: date, provider: Shufti Pro).”

KYC: document verification across 10,000+ document types in 230+ countries, face verification with 3D liveness, address verification, AML screening.

KYB: business verification against 300+ data sources in 250+ jurisdictions, UBO identification, company officer verification.

Tier 1: Shufti Pro’s eIDV endpoint (database lookup without document upload). If eIDV doesn’t cover all four countries, fall back to self-declaration with verification deferred to Tier 2.

Tier 2: Shufti Pro’s Journey Builder — hosted verification flow (iframe or redirect) with government ID + face verification + 3D liveness.

Tier 3: Same as Tier 2 plus address verification and AML screening modules.

KYB: Shufti Pro’s API. Org admin provides registration number and country. Server-side verification, no user-facing UI beyond the input form.

Shufti Pro provides KYC coverage across 230+ countries and KYB verification against 300+ data sources in 250+ jurisdictions. For specific ID types and registry details per launch market, see Appendix C.

Single Convex HTTP action endpoint. Validates signature, normalizes payload, looks up verification record by provider session ID, transitions status. Idempotent: if already in target state, no-op.

Polling fallback: Scheduled function every 5 minutes checks records stuck in pending or in_review longer than expected. Calls provider’s getVerificationStatus. Same pattern as fiat ramp webhook reliability.

Convex table jurisdictionThresholds: country code, currency, Tier 1/2 per-transaction limits, cumulative limits and periods, Tier 3 required flag, KYB required flag, legal reference text.

At launch: conservative defaults (require Tier 2 for any fiat transaction). Relax to Tier 1 for small amounts once thresholds are legally confirmed. Designed to be permissive later, not tightened — better to require more verification initially and relax.

  1. Look up user’s country in jurisdictionThresholds
  2. Sum completed off-ramp volume for current period from fiatTransactions
  3. Determine minimum tier required based on existing + new transaction volume
  4. Compare against user’s current verified tier
  5. Proceed if sufficient; reject with VERIFICATION_REQUIRED if not

“To withdraw to your bank account, we need to verify your identity. This takes less than a minute.” Collects name, DOB, phone, country. No document upload. If database check passes: proceed to withdrawal. If fails: prompt for Tier 2 immediately.

“Your withdrawal volume requires additional verification. Please verify your ID. This takes about 2 minutes.” Opens Shufti Pro hosted flow. User uploads ID and takes selfie. Typically 30-60 seconds for automated check.

“To enable fiat features, please provide your business registration details.” Collects country, registration number, company name. Backend calls Shufti Pro. Results in seconds for companies with digital registry records.

30-day advance prompt. On expiry, next fiat operation triggers re-verification with pre-filled data from previous verification.