발급 API
데모 웹에 내장된 발급 서비스의 API입니다. 발급 키는 서버에만 존재하고, 트랜잭션 가스는 서비스가 부담합니다.
/api/screen이 실제 심사를 돌려 케이스를 만들고, /api/issue는 통과한 케이스에만 붙습니다. 심사를 건너뛰는 경로는 없습니다.POST /api/screen
발급 전 심사입니다. 검사 7종을 순서대로 돌리고 케이스를 만들어 케이스 번호를 돌려줍니다. 이름은 명단 대사에만 쓰고 저장하지 않습니다 — 케이스에는 솔티드 해시만 남습니다.
요청
{
"address": "0x…", // 필수 — 심사 대상 지갑
"name": "Gildong Hong", // 필수 — 제재 명단 대사용. 저장하지 않음
"dob": "1990-05-05", // 선택 — 동명이인 판별에 쓰임
"nationality": "KR", // 선택 — ISO 3166-1 alpha-2
"residency": "KR"
}검사 순서
| 검사 | 내용 | 실패 시 |
|---|---|---|
| OWNERSHIP | 지갑 소유권 서명 (발급 단계에서 검증) | FAIL |
| IDENTITY | 선언 정보의 형식·범위 정합성 | FAIL |
| ADDRESS | OFAC 디지털자산 주소 정확 일치 대사 | FAIL |
| SANCTIONS | OFAC·UN 이름 대사 (95점 이상 차단 / 85점 이상 검토) | FAIL 또는 HIT |
| EXPOSURE | 제재 지정 주소와의 온체인 접촉 | HIT |
| JURISDICTION | FATF 조치 촉구 관할 | FAIL |
| RISK | 위험평가 종합 — 등급이 만료·재심사 주기를 결정 | 등급 4 이상이면 HIT |
응답
{
"caseNo": "ONB-20260729-0001",
"decision": "ALLOW", // ALLOW | REVIEW | BLOCK
"status": "AUTO_CLEARED",
"checks": [ { "kind": "SANCTIONS", "status": "PASS", "summary": "…", "ms": 107 }, … ],
"risk": { "score": 22, "band": 1, "bandLabel": "낮음", "ttlDays": 365, "rescreenDays": 365, … },
"screening": { "topScore": 0, "candidates": 84, "scanned": 20168, "listVersion": "0x…" },
"screenedAt": "2026-07-29T…"
}FAIL 하나면 BLOCK, HIT 하나면 REVIEW입니다. 자동 승인은 전 검사가 통과했을 때만이고, BLOCK·REVIEW 케이스로는 발급이 나가지 않습니다.
POST /api/issue
지정한 지갑에 KYC 어테스테이션을 발급(또는 승급)합니다.
요청
{
"address": "0x…", // 필수 — 발급 대상 지갑 주소
"level": 1, // 선택 — 1(본인 선언)만 허용. 벤더 등급은 연동 후 개방
"force": false, // 선택 — 이미 활성이어도 강제 재발급
"caseNo": "ONB-…", // 필수 — /api/screen이 준 통과 케이스. 15분 내, 1회만 사용 가능
"claimsRoot": "0x…", // 필수 — 대상자 브라우저가 만든 클레임 커밋먼트(bytes32).
// 신원 원본은 서버로 오지 않는다
"issuedAt": "…", // 필수 — 서명 시각(ISO 8601), 10분 내 유효
"signature": "0x…" // 필수 — 대상 지갑의 EIP-191 서명. 원문은 아래 참조
}서명 원문 (EIP-191 personal_sign)
호패 발급 요청
이 서명은 지갑 소유권 증명이에요. 가스가 들지 않아요.
wallet: <address 소문자>
level: L<level>
claimsRoot: <claimsRoot 소문자>
issued-at: <issuedAt ISO 8601>서버가 같은 원문을 재구성해 서명을 검증하고, 복구된 서명자가 address와 일치할 때만 발급합니다. 원문에 level·claimsRoot·시각이 바인딩되어 있어 다른 요청에 재사용할 수 없고, issuedAt이 10분을 지나면 거절됩니다.
동작
| 조건 | 결과 |
|---|---|
| 요청 등급 이상으로 이미 활성 | 트랜잭션 없이 { "alreadyActive": true } 반환 (force로 우회 가능). |
| force 재발급 | 기존 어테스테이션을 같은 트랜잭션에서 폐기(supersede)하고 새로 발급. |
| 가스 잔고가 낮은 지갑 | 소량의 가스 지원(stipend)을 먼저 전송. |
응답 (성공)
{
"ok": true,
"txHash": "0x…", // 발급 트랜잭션
"uid": "0x…", // EAS 어테스테이션 UID
"level": 1,
"supersededUid": "0x…", // 재발급으로 폐기된 구 UID (없으면 null)
"evidence": { … }, // 증적 레코드 원문 (심사 체크·케이스 ID·타임스탬프)
"evidenceHash": "0x…", // 위 레코드의 keccak256 — 온체인 기록과 일치
"stipendTxHash": "0x…", // 가스 지원 tx (없으면 null)
"claimsRoot": "0x…", // 온체인에 기록된 커밋먼트 루트 (스키마 v2)
"auditRef": "0x…" // 감사 봉투 해시 (스키마 v2)
}evidence는 앞 레코드의 해시(prevHash)를 품은 증적 체인의 한 항목입니다. 키 순서를 정렬한 정규 JSON을 keccak256하면 온체인 evidenceHash와 일치하며, POST /api/evidence가 그 계산을 대신 해 줍니다.
POST /api/revoke
연결한 지갑이 자기 호패를 철회하는 경로입니다. 발급과 동일한 EIP-191 소유권 서명을 요구하고, 사유 코드는 USER_REQUEST로 기록됩니다. 제재 적중에 의한 폐기는 이 경로가 아니라 재대사 데몬이 발급 키로 실행합니다.
요청
{
"address": "0x…", // 필수 — 폐기 대상 지갑 (서명 주체와 동일해야 함)
"requestedAt": "…", // 필수 — ISO 8601, 서명 원문에 포함 (10분 내 유효)
"signature": "0x…" // 필수 — revokeMessage()의 EIP-191 서명
}응답 (성공)
{
"ok": true,
"txHash": "0x…", // 폐기 트랜잭션
"record": { … }, // 폐기 사유 레코드 원문
"reasonHash": "0x…" // 레코드의 keccak256 — 온체인 기록과 일치
}공통 사항
| 항목 | 내용 |
|---|---|
| 레이트리밋 | 클라이언트당 5분에 10회 (엔드포인트별). 초과 시 429. |
| 에러 응답 | { "error": "메시지" } — 400(형식·주소·등급·서명 만료 오류), 401(소유권 서명 불일치), 404(폐기 대상 없음), 429(레이트리밋), 500(트랜잭션 실패). |
| 트랜잭션 직렬화 | 발급 키의 nonce 경합을 막기 위해 서버에서 발급·폐기 tx를 직렬 처리합니다. |
| 증적 영속화 | 증적 체인은 현재 서버 인스턴스 메모리에 있습니다. 사본이 발급 응답으로 대상자에게 전달되므로 저장소 없이도 검증은 성립합니다. WORM 저장소는 다음 단계입니다. |
발급 권한 모델: 온체인 발급은 레지스트리에 등록된 issuer 키만 가능하고, 그 키는 이 서버에만 존재합니다. SDK는 읽기 전용이며 발급 능력이 없습니다.
POST /api/evidence · GET /api/evidence?hash=
증적 검증기입니다. GET은 저장소에서 레코드를 찾아 주고, POST는 건네받은 사본의 해시를 다시 계산합니다. 후자는 저장소가 없어도, 우리를 믿지 않아도 성립하는 경로입니다 — 발급 때 받은 사본과 온체인 evidenceHash만 있으면 됩니다.
// 요청
{ "record": { … }, "expected": "0x…" }
// 응답
{ "computed": "0x…", "expected": "0x…", "match": true }GET /api/cron/rescreen · POST
일일 재대사입니다. 활성 로스터를 온체인 이벤트에서 재구성해 제재 지정 주소·온체인 접촉으로 다시 대사하고, 적중하면 증적을 만들어 폐기 트랜잭션을 냅니다. GET은 Vercel Cron이 매일 00:00 KST에 호출하며 CRON_SECRET으로 보호됩니다. POST는 콘솔의 즉시 실행 버튼이 쓰는 같은 코드입니다.
{
"runId": "RSC-…",
"scanned": 12, // 대사한 활성 어테스테이션 수
"hits": [ { "subject": "0x…", "reason": "OFAC 제재 지정 주소 — …" } ],
"revoked": [ "0x…" ],
"listVersion": "0x…",
"trigger": "cron"
}GET /api/aml
콘솔이 읽는 운영 상태입니다. 명단 소스·신선도, 재대사 이력, 케이스, 증적 체인 무결성, KPI가 들어 있습니다. 화면의 숫자는 전부 이 응답에서 나옵니다.