KYC & 신원
구현 완료, 배포/QA 대기
SumSub 연동, 풀별 관할 게이팅(jurisdiction_whitelist + enforce_jurisdiction. KYC/KYB 레벨 게이팅은 마이그레이션 0063에서 제거됨), 크로스-VASP 재사용 KYC(v3-17), reverify-sweep, SBT 민팅 큐, 만료 대시보드 배너가 DB·핸들러·컨트랙트·프론트엔드에 모두 구현돼 있다.
SumSub 기반 KYC/KYB 검증, Soulbound Token 발급, 풀별 게이팅, 거부 기준을 다룬다.
Soulbound Token (SBT)
SBT 개요
KYC/KYB는 SumSub로 연동한다. 승인되면 전송 불가 ERC-721(PlatformKYCSoulbound)이 온체인에 민팅된다. 풀별이 아니라 전역 컨트랙트다. 투자하려면 kyc_status = APPROVED가 필요하다. KYC가 취소되면 SBT는 소각된다.
SBT가 담는 것은 KYC 레벨(INDIVIDUAL/INSTITUTION), 관할(ISO 3166-1 alpha-3 국가 코드), 발급/만료 타임스탬프, 취소 여부다. 미국인 여부는 국가 코드에서 파생되며 별도의 확인 플래그가 없다. 풀별 입금 검사는 SBT에서 관할과 유효성을 읽는다. 레벨은 저장되지만 더 이상 입금을 막지 않는다(풀 단위 KYC/KYB 레벨 게이팅은 마이그레이션 0063에서 제거됐다).
취소와 상환의 관계(v3-19에서 정리됨): 출구 게이트의 기준은 온체인 kycStateOf이고 상태는 넷이다. NONE / VALID / EXPIRED / REVOKED. EXPIRED는 신규 입금을 막지만 상환은 허용한다(canRedeem이 true를 반환). 그래서 KYC가 그냥 만료된 투자자는 언제든 나갈 수 있다. REVOKED(상시 AML 적발로 온체인 revoke() 호출)는 입금과 상환을 둘 다 막으면서, 감사를 위해 토큰은 온체인에 남긴다.
출구 게이트에서 네 상태가 모두 의미를 갖는다. canRedeem은 VALID || EXPIRED이므로(PlatformKYCSoulbound.sol:371-373), NONE은 REVOKED와 똑같이 거부된다. 예전에는 이 점이 명시돼 있지 않아 REVOKED만 출구를 막는 것처럼 읽혔다. NONE은 SBT를 한 번도 가진 적 없는 지갑과, SBT가 소각된 지갑을 모두 가리킨다. 어느 쪽이든 유효성을 읽을 토큰이 없으니 게이트를 통과시킬 수 없다. 실제로는 입금 자체가 VALID SBT를 요구하므로, 홀더가 NONE에 이르는 경로는 소각뿐이다.
거부는 경로에 따라 서로 다른 두 개의 revert로 나타난다. 오류 이름을 문구에 매핑하는 쪽에서는 이 차이가 중요하다.
| 경로 | 가드 | Revert |
|---|---|---|
즉시 정산(approveRedemption, 파트너 top-up 자동 정산) | RedemptionLib.sol:544 | RedemptionBlockedByKyc |
무권한 폴백(claimRedemptionFallback) | RedemptionLib.sol:416 | RedemptionBlockedByKyc |
회차 claim(claimRedemption) | RedemptionLib.sol:1049 | KYCRequired |
회차 요청 취소는 자기 게이트를 따로 갖지 않고 회차 claim 게이트를 물려받는다. cancelRedemption은 일부라도 정산된 상태에서 ClaimBeforeCancel로 revert하므로(:628), 막힌 홀더는 1단계를 마칠 수 없고 따라서 취소에 도달할 수 없다. 이 경우의 해소 경로는 관리자 조치다(Part A).
SBT 민팅과 복구(큐 기반)
온체인 SBT 작업(민팅, 소각, 취소)은 전부 플랫폼 서명 지갑 하나로 수행되므로, 메시지 그룹이 하나뿐인 FIFO SQS 큐 하나로 몰아넣는다. 컨슈머가 한 건씩 처리하므로 nonce 충돌이 원천적으로 불가능하다(작업별로 큐를 나누면 민팅 컨슈머와의 레이스가 다시 생긴다).
비동기 민팅(v3 큐 전환)
GREEN 심사가 나오면 SBT 민팅은 인라인으로 하지 않고 큐에 넣는다. kyc_status는 즉시 APPROVED로 바뀌고 민팅은 컨슈머에서 비동기로 끝난다. 클라이언트(투자자 카드, 어드민 KYC 페이지)는 요청 응답에서 tx 해시를 읽는 대신 sbt_status를 폴링한다(NOT_MINTED → MINTED / FAILED). 그래서 POST /kyc/mint-sbt는 **202 { status: 'queued', userId }**를 반환한다.
유저당 동시에 진행되는 민팅은 하나뿐이고, 모든 호출자에 대해 enqueueSbtMint가 이를 강제한다(엔드포인트의 일반 경로와 force 경로, GREEN 심사, reconcile sweep 전부). 이 함수는 sbt_mint_queued_at 슬롯을 선점하는 조건부 UPDATE이며 atomic하므로 동시 호출자 둘이 함께 이길 수 없다. 선점하면서 같은 쓰기 안에서 이전 결과(sbt_status/sbt_error, force 재민팅이면 토큰 미러까지)를 초기화한다. 선점에 실패하면 엔드포인트는 **409**를 반환하고 sweep은 건너뛴다. 예전에는 이 가드가 엔드포인트의 한쪽 분기에만 있어서, 일반 경로가 두 번째 민팅을 큐에 넣고 그것이 방금 민팅된 토큰을 소각해 버릴 수 있었다.
복구: webhook 유실 sweep과 큐 재시도
독립적인 안전망이 둘이다.
- 큐 재구동: 트랜잭션이 revert되거나 타임아웃된 민팅/소각/취소는 5회 재시도한다. 그다음엔 **DLQ(14일 보존)**로 들어가고, CloudWatch 알람에서 ops 이메일이 나가 수동 조사로 이어진다(서명 지갑 잔액, 체인 설정, 컨트랙트 pause 여부).
- reconcile sweep(5분 주기):
IN_REVIEW에 묶인 유저(webhook 유실)를 SumSub에 다시 조회하고, 멈춰 있는 민팅(APPROVED+NOT_MINTED/FAILED)을 다시 큐에 넣는다. 10분 동안 변경이 없던 행만 대상으로 하므로 진행 중인 webhook이나 컨슈머와 경쟁하지 않는다. 모든 경로가 멱등이다(이미APPROVED+MINTED인 유저에게 GREEN이 와도 아무 일도 하지 않는다).
진짜 webhook 유실(SumSub가 포기했거나 우리 엔드포인트가 죽어 있었던 경우)은 재시도할 메시지 자체가 없다. 그래서 SQS가 아니라 pull 방식 sweep만이 그 경우의 백스톱이다.
검증되지 않은 국가에는 민팅하지 않는다
mintKYCSBT는 countryCode를 기본값 없는 필수 파라미터로 받는다. 온체인에서 이 문자열 하나가 게이트 둘을 결정한다. 관할 화이트리스트(PlatformPool → jurisdictionAllowed[kycContract.jurisdictionHashOf(investor)])와 미국인 규칙(PlatformKYCSoulbound._isUsCountry)이다. 폴백 값을 두면 투자자가 검증받은 적 없는 관할을 주장하는 자격증명이 발급되고, 온체인 입금 게이트는 그것을 그대로 인정한다. 예전에는 기본값이 'KOR'이었다.
mintSbtForApprovedUser는 resolveInvestorCountry(business/kyc-gating.ts)로 국가를 해석한다. 오프체인 입금 게이트가 쓰는 것과 같은 헬퍼라, 양쪽이 하나의 국가를 읽는다(법인은 company_country ?? country). users의 미러가 비어 있으면 SumSub에서 신청자 정보를 한 번 가져와 저장한다. 그래도 국가를 확정할 수 없으면 추측하지 않고 닫는 쪽으로 실패한다(sbt_status = FAILED, sbt_error = 'Investor country could not be verified', 재시도 가능). 국가 해석은 재발급을 위한 소각 이전에 일어나므로, 거부가 홀더를 자격증명 없는 상태로 남기는 일은 없다.
로그인 시점 정합화: DB를 체인에 맞춘다
POST /auth/verify는 홀더의 온체인 상태를 읽어 users 행을 그에 맞춘다(auth/sbt-sync.ts → resolveSbtSync. 순수 함수이고 단위 테스트가 있으며, 쓰기는 핸들러가 한다).
여기서 읽는 것은 boolean isValidKYC가 아니라 **네 상태짜리 kycStateOf**다. 그 boolean은 EXPIRED와 REVOKED를 똑같이 false로 반환하는데, 이 둘을 "SBT 없음"으로 뭉개는 바람에 예전에는 로그인 한 번으로 두 가지 사고가 났다. 하나는 AML로 REJECTED된 홀더를 NOT_STARTED로 되돌려, isFinalRejected가 읽는 거부 기록을 지우고 KYC를 다시 열어 준 것이다. 다른 하나는 EXPIRED 홀더를 GUEST/NOT_STARTED로 떨어뜨린 것인데, 오프체인 출금 게이트가 이를 "KYC 없음"으로 읽어 온체인 canRedeem이 명시적으로 허용하는 출금을 막았다.
| 온체인 상태 | DB 쓰기 | 이유 |
|---|---|---|
읽기 실패(null) | 없음 | 모른다는 것과 없다는 것은 다르다. 노드 장애가 정상 투자자를 초기화하면 안 된다. |
VALID | 뒤처진 행을 APPROVED / MINTED / INVESTOR로 전진 | KYC의 기준은 체인이다. 단 REJECTED 행에는 절대 적용하지 않는다. 큐에 들어간 AML 취소는 잠깐 동안 여전히 VALID로 읽히는데, 이때 행을 "고치면" 컴플라이언스 결정을 되돌리게 된다. |
EXPIRED | 없음 | 자격증명은 존재한다. 만료는 신규 입금을 막을 뿐 홀더 본인의 출구를 막지 않는다(v3-28의 "무기한 가둠 금지"). 갱신은 이 쓰기가 아니라 sbt_expires_at이 유도한다. |
REVOKED | kyc_status = REJECTED만 | 아직 APPROVED를 주장하는 행을 조인다(취소가 밖에서 들어올 수 있다. 예를 들어 운영자가 체인에서 직접 revoke). sbt_status는 MINTED로 두고 토큰 id도 유지한다. 토큰은 존재하고 취소만 된 상태다. |
NONE | sbt_status = NOT_MINTED, role = GUEST(및 kyc_status = NOT_STARTED) | 확정적 부재이고, 행이 아직 토큰을 주장하고 있을 때만 적용한다. REJECTED 판정은 보존한다. 이를 "시작한 적 없음"으로 되돌리면 FINAL 거부의 재시도 금지가 풀려 버린다. |
GET /kyc/status도 kyc/self-heal.ts를 통해 같은 VALID 전용 보정 규칙(그리고 같은 REJECTED 제외)을 적용한다. 다만 행이 이미 민팅된 토큰을 기록하고 있을 때만 체인을 읽는다. 이건 10초 주기의 KYC 폴링이고, 자격증명이 없는 홀더에게는 지갑 조회도 RPC 호출도 하지 않는다. 따라서 외부에서 발급된 SBT에 뒤처진 행은 폴링이 아니라 로그인 시점(위)이나 reconcile sweep에서 보정된다.
풀별 KYC 게이팅 (v3.0)
레벨 게이팅은 제거됨(마이그레이션 0063)
풀은 더 이상 KYC/KYB 레벨(개인 대 법인)로 게이팅하지 않는다. kyc_level_required와 requires_institutional은 마이그레이션 0063에서 삭제됐고, 현재 풀 게이팅은 관할 + 유효한 SBT뿐이다. 그 위에 비미국 Reg S 적격투자자 모델이 백엔드 쪽에 구현돼 있다(마이그레이션 0072, 온체인 변경 없음). 풀마다 eligibility_mode(STATUS = Status Gate / MIN_TICKET = Ticket Gate)를 갖고, 유저마다 investor_status(RETAIL/PROFESSIONAL)와 qualification_* 필드를 갖는다. checkKycGating이 모드를 강제하고 사유 코드(NOT_PROFESSIONAL, BELOW_MIN_TICKET, KYC_EXPIRED 등)를 반환한다. 24-field-governance와 KYC/KYB·투자자 등급 스펙(v3-74) 참조.
각 풀은 pools 테이블에 자기 관할 규칙을 선언한다.
| 차원 | 타입 | 뜻 |
|---|---|---|
enforce_jurisdiction | BOOLEAN | 마스터 스위치(0064). 꺼져 있으면 국가 제한이 없고, 켜져 있으면 화이트리스트가 적용된다. |
jurisdiction_whitelist | TEXT[] | 허용 국가의 ISO 3166-1 alpha-3 코드("KR"이 아니라 "KOR". v3-60). 빈 배열이면서 enforce가 켜져 있으면 전부 차단이다. (kyc_jurisdiction_whitelist에서 이름 변경, 마이그레이션 0070) |
allows_us_persons | BOOLEAN | false(기본)면 미국 국가 코드를 가진 투자자를 거부한다(미국인 여부는 SBT 국가 코드에서 파생). |
게이트는 두 곳에서 돈다. 백엔드 checkKycGating()(pools.get.eligibility와 deposits.post.create의 사전 검사)과 온체인 _checkEntryKyc(PlatformPool)다. 후자는 취소되지 않은 유효 SBT를 요구하고, enforceJurisdiction이 켜져 있으면 투자자 국가 해시가 온체인 화이트리스트에 있어야 한다. 투자자의 SBT가 풀의 게이팅을 만족하지 않으면 입금은 명확한 오류와 함께 revert된다.
예시
- 리테일 풀, 관할 개방:
enforce_jurisdiction = false,allows_us_persons = false→ KYC를 마친 비미국 투자자면 누구나. - 한국/일본/싱가포르 풀:
enforce_jurisdiction = true,jurisdiction_whitelist = ['KOR', 'JPN', 'SGP'],allows_us_persons = false.
전체 설정 맥락은 풀 모델 → 컴플라이언스 차원 참조.
KYB (법인 온보딩)
플랫폼에서 제외됨. 개인 트랙만 운영한다(v3-98)
KYB(법인 온보딩)는 범위 밖이다. 2026-07-29 결정으로, 이 플랫폼은 개인만 온보딩한다. "나중 단계"가 아니다. 목표 시점이 없고, 이전 계획이 전제하던 2026년 8월 SumSub Enterprise KYB 구독도 하지 않는다.
이 절의 나머지를 읽는 법:
- 끝까지 동작하는 것은 INDIVIDUAL 트랙뿐이다.
VITE_KYB_ENABLED는 계속 꺼져 있고,KYCModal의 법인 옵션은 출시 가능한 경로가 아니다. kyc_levelenum(INDIVIDUAL/INSTITUTION)과SUMSUB_LEVEL_MAP은 그대로 둔다. SBT가 레벨을 저장하고, 마이그레이션 0063이 이미 레벨 기반 풀 게이팅을 없앴으므로 쓰이지 않는 INSTITUTION 분기는 무해하다. 그게 남아 있다는 사실이 KYB가 동작한다는 근거는 아니다.- 아래 정책 표는 로드맵이 아니라 조사 결과로 남긴다. 실제 관할 조사에 비용이 들었고, 법인 온보딩을 다시 검토할 때를 위해 보존한다. 여기 있는 것 중 지금 만들고 있는 것은 없다.
- 딸려 나오는 미해결 항목: 투자자 대상 문구. 없앤 기능에 "곧 제공" 메시지를 띄우는 것은 제품이 지키지 않을 약속이다. v3-98 참조.
보존한 KYB 조사 결과다. 구현 계획이 아니다(v3-75, v3-98로 대체됨. 벤치마크는 Notion "KYB 관할권 벤치마크 & 결정").
| 축 | 정책 |
|---|---|
| 대상 관할 | KYC/Reg S 허용 목록과 동일(SG·HK·EU·JP·UAE·CH·UK·TH·MY). KYB용 별도 목록은 없다. 인도네시아는 OJK 샌드박스 별도 트랙이다. |
| 법인 적격성 | 규제 대상 기관은 자동으로 PROFESSIONAL이 된다. 그 외 법인은 "대규모 기업 3중 2" 요건을 통과해야 한다(총자산 2천만 / 매출 4천만 / 자기자본 2백만. 통화는 관할별로 다르다. ADGM 자기자본 100만 달러, SG 1천만 싱가포르달러, HK 800만/4천만 홍콩달러, JP 5억 엔 또는 QII, TH 1억/2억 바트, MY 1천만 링깃). |
| UBO(실질 소유자) | 항상 자연인까지 내려간다. 지분·의결권 20% 이상인 사람과 실질 지배력을 행사하는 사람을 모두 수집한다(20%는 말레이시아 기준선이고 25% 관할도 함께 커버한다). 판정은 관할별로 다르다. 표준은 25% + 지배력 + 최고경영진 폴백이고, 일본은 캐스케이드(단독 50% 초과 → 아니면 25% 초과 → 아니면 지배력 → 아니면 대표이사), **말레이시아는 20%**다. 임계값은 관할·리스크별 설정이다(고위험 10~15%도 지원). 등기부 기반 UBO 조회는 신뢰할 수 없어(대부분 비공개) 자진 신고 + 검증 방식을 쓴다. |
| 서류 | 표준 세트(설립증명 · 정관 · 이사/주주 명부 · 등록 주소 증빙 · 이사회 결의 또는 위임장 · UBO 신고서)에 관할별 추가 서류(SG ACRA Bizfile, HK NAR1, JP 등기사항증명서)를 더한다. |
| 대표자 | 이사가 아닌 서명자는 이사회 결의나 위임장이 필요하다. 신원 확인(라이브니스)만으로는 부족하고, 그 사람이 회사 기록과 연결돼야 한다. |
| SumSub | 수집, 등기부 조회, UBO 매핑, 서류 OCR, 제재·PEP 스크리닝을 담당한다(Enterprise 등급). 최종 승인·거부와 법적 책임은 Aset에 남는다. |
미결(BD·법무 승인 필요): 관할별로 Aset이 "보고 의무 기관"에 해당하는지(어느 AML 규정집이 우리를 구속하는지), 최종 출시 국가와 정확한 임계 값(그전까지는 자리표시자), 인도네시아의 DFA 대 증권 성격 판단, SumSub 비미국 프리셋 범위.
크로스-VASP KYC (v3-17)
파트너 플랫폼(예: Tokocrypto 세컨더리 마켓, Binance.SG)에서 유저를 온보딩할 때, Aset은 규제 준수를 유지하면서 마찰을 줄이는 2트랙 전략을 쓴다.
트랙 1: SumSub 재사용 KYC(파트너가 SumSub를 쓸 때 우선)
파트너 유저 → Aset에서 "내 Tokocrypto 인증 사용하기"
↓
Aset 백엔드: SumSub에 POST /sharetokens
↓
유저 동의 모달(GDPR/PDPA 요건): "Tokocrypto KYC를 Aset과 공유하시겠습니까?"
↓
SumSub가 검증된 신청자 데이터를 Aset의 SumSub 환경으로 이전
↓
Aset의 SumSub가 수령자 레벨 검사를 다시 수행:
- jurisdiction_whitelist 일치(v3-10)
- 만료, 취소, 제재 스크리닝(항상 재수행)
↓
통과 → PlatformKYCSoulbound 민팅 → INVESTOR
↓
유저 체감 시간: 수 분요건:
- Aset과 파트너가 모두 SumSub 고객이어야 한다
- 재사용 KYC 제휴 계약이 체결돼 있어야 한다(SumSub 어드민 레벨에서 처리)
- 유저 동의(규제상 데이터 공유 동의 필요)
트랙 2: 빠른 재KYC(파트너가 SumSub를 안 쓸 때)
파트너 유저 → Aset에서 "Tokocrypto 유저이신가요?"
↓ 예
SumSub 재방문 유저 플로우(기존 SumSub 신원이 있으면 이미 올린 서류는 생략)
↓
유저가 추가 정보 제공: 관할 또는 국가 확인(미국인 여부는 국가에서 파생)
↓
표준 SumSub 심사
↓
통과 → SBT 민팅
↓
유저 체감 시간: 5~10분파트너 연동이 필요 없다. 파트너가 어떤 KYC 업체를 쓰든 동작한다.
판단 기준
| 질문 | 트랙 |
|---|---|
| 파트너가 SumSub를 쓰는가? 예 → 재사용 KYC 제휴가 가능한가? 예 | 트랙 1 |
| 파트너가 SumSub를 쓰지만 재사용 KYC 제휴가 미체결 | 트랙 2(제휴 전까지) |
| 파트너가 SumSub가 아닌 업체(Onfido, Jumio, Veriff 등) 사용 | 트랙 2 |
| 파트너 관계 없음, 신규 유저 | 표준 SumSub 플로우(약 15분) |
KYC를 아예 건너뛰지 않는 이유
"다른 곳에서 검증됐으면" KYC를 생략하려는 플랫폼이 있다. VASP에게는 허용되지 않는다.
- 싱가포르 MAS, 인도네시아 OJK, EU MiCA 모두 각 VASP가 자체 KYC를 하도록 요구한다
- 상류에서 무슨 검증을 했든 책임은 각 VASP에 남는다
- 재사용 KYC는 검사를 생략하는 게 아니라 공유받은 데이터에 우리 검사를 다시 돌려서 이 문제를 피해 간다
- "Binance가 지역 법인마다 새 KYC를 강요한다"는 방식은 마찰이 크지만 법적으로 깔끔하다. 재사용 KYC는 같은 수준의 준수를 더 낮은 마찰로 얻는다
Travel Rule(별개 사안)
FATF Travel Rule은 유저 단위 KYC가 아니라 거래 단위의 VASP 간 데이터(IVMS101 기반 송금인·수취인 정보)를 다룬다.
| 크로스-VASP KYC (v3-17) | Travel Rule | |
|---|---|---|
| 공유 대상 | 유저 신원 검증 데이터 | 거래 송신자·수신자 정보 |
| 시점 | 온보딩 시 | 임계값 초과 거래마다 |
| 임계값 | 없음 | 인도네시아 1,000달러, 싱가포르 1,500싱가포르달러 |
| 표준 | SumSub 재사용 KYC | IVMS101 (Notabene, Sygna 등) |
Aset Pool과 파트너 지갑 사이에서 임계값을 넘는 자금이 오갈 때 Travel Rule 연동이 필요하다. 별도 운영 연동으로 처리하며 v3-17 범위가 아니다.
구현 단계
| 단계 | 시점 | 내용 |
|---|---|---|
| 1단계 | 현재 | Aset SumSub KYC 단독 운영. "기존 유저" 경로에 대한 UI 힌트 제공. |
| 2단계 | Tokocrypto 계약 체결 시 | Tokocrypto의 KYC 스택 확인 후, 해당되면 재사용 KYC 제휴 활성화. |
| 3단계 | 다수 파트너 확장 | 재사용 KYC는 파트너 수 제한이 없다. Binance.SG, Coinbase 등에 같은 인프라를 쓴다. |
출시 현황 (2026-06-18, 온보딩 PRD §7)
- 재방문 유저 빠른 경로: 반영 완료.
/kyc/access-token이returning플래그를 반환하고, KYC 모달이 "기존 서류를 재사용한다"는 안내를 띄운다(트랙 2 재방문 플로우). 출시에 포함된다. - 크로스-VASP 파트너 임포트 진입점: 보류(G2). "내 파트너(예: Tokocrypto) 인증 가져오기" UI 진입점은 파트너 계약이 성사될 때까지 꺼 둔다. 백엔드(
kyc.post.reuse)는 준비된 상태로 유지한다. 파트너가 없으면 임포트할 것도 없다. - 스폰서 이벤트 재검증: 보류(G6). 민팅 이후 AML·제재 플래그에 대한 어드민 주도 재KYC UI가 없다. T-30 만료 sweep(
reverify-sweep)만 있다. 재검증 운영 정책이 확정될 때까지 보류한다.
결정 기록: Notion Decision Log의 G2 Cross-VASP 진입점, G6 Sponsor-event 재검증. (5-state 홀더 모델 보류는 21-holder-verification Phase 2.5에서 추적한다.)
KYC 상태 흐름
IN_REVIEW는 제출됐다는 뜻이지 시작했다는 뜻이 아니다
SDK를 여는 것은 심사 진입이 아니다. POST /kyc/access-token은 kyc_level만 기록한다. 상태는 SumSub 자신의 신호로 움직이고, 단일 라이터인 kyc/enter-review.ts → markApplicantInReview를 거친다. 신호는 applicantPending / applicantOnHold webhook이거나, POST /kyc/sync가 신청자의 reviewStatus를 되읽는 경우다(isApplicantSubmitted: init이 아닌 모든 값).
예전에는 토큰 발급 시점에 썼고, 그게 잠금 사고를 만들었다. 시작을 누르고 탭을 닫은 홀더는 제출한 게 없는데 IN_REVIEW가 됐다. 상태 카드의 PENDING 분기에는 CTA가 없고, 제출된 적 없는 신청자에 대해 SumSub가 심사 결과를 주지 않으니 어떤 sweep도 이를 해소할 수 없었다. 상태가 제공자가 보고한 적 없는 사실을 주장한 것이다.
제출과 기록 사이의 간극을 세 가지가 함께 막는다. 하나가 실패해도 홀더가 갇히지 않는다.
- SDK의
idCheck.onApplicantSubmitted가 모달을 닫기 전에POST /kyc/sync를 기다린다(서버가 SumSub에서reviewStatus를 다시 읽는다. 클라이언트 이벤트는 확인하라는 신호일 뿐 증거가 아니다). applicantPendingwebhook. 브라우저와 무관하게 권위 있는 신호다.- reconcile sweep의 "시작했는데 기록 안 됨" 스캔:
kyc_level IS NOT NULL AND kyc_status = 'NOT_STARTED'이면서 시작한 지 10분에서 24시간 사이인 행.kyc_level은 SDK 토큰 경로만 쓰므로, 이 조건은 정확히 "검증을 시작했고 심사 결과가 한 번도 기록되지 않았다"를 뜻한다. 24시간으로 제한하는 이유는 그 집합 대부분이 그냥 떠난 홀더들이고 SumSub는 그들에 대해 영원히init을 답하기 때문이다.
거부 기준: RETRY와 FINAL
SumSub가 거부 사유 코드에 따라 자동 판정하고, 시스템이 사유를 reject_type으로 매핑한다. FINAL 판정은 어드민이 뒤집을 수 없다(컴플라이언스 요건).
| SumSub 사유 | reject_type | 예시 |
|---|---|---|
| 서류 품질 문제 | RETRY | 사진 흐림, 빛 반사, 신분증 잘림, 만료 서류 |
| 정보 불일치 | RETRY | 이름 불일치, 잘못된 서류 종류 제출 |
| 제재 대상 국가 | FINAL | OFAC / EU 제재 목록 일치 |
| 사기 탐지 | FINAL | 위조 서류, 도용된 신원 |
| 미성년 | FINAL | 만 18세 미만 |
| 중복 신청자 | FINAL | 같은 사람이 다른 계정으로 이미 인증됨 |
규칙
KYC 규칙
- RETRY: 재시도 횟수 제한이 없다(SumSub가 내부적으로 추적). 투자자에게 재업로드를 안내한다.
- FINAL: 계정이 영구히 투자 불가가 된다. 어드민은 거부 상세를 볼 수 있지만 뒤집을 수 없다(컴플라이언스 요건).
- 어드민 KYC 상세 패널에는 SumSub 거부 사유, reject_type(RETRY/FINAL), 시도 이력이 표시된다.
- 투자자 KYC 상태 페이지에는 정제된 거부 사유와 함께 재시도 안내(RETRY) 또는 영구 차단 안내(FINAL)가 표시된다.