본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 05. 30. 10:49

솔로 OSS의 npm 공개 직전: mainnet 실전 투입 + 5계층 위협 모델을 Claude CTO 리뷰어 역할로 통과하기까지

요약

솔로 OSS 프로젝트인 kawasekit의 npm 배포 직전 단계와 Polygon 메인넷 실전 투입 과정을 다룹니다. 특히 Claude에게 CTO 리뷰어 역할을 부여하여 위협 모델(Threat Model)을 검증하고 기술적 승인을 받는 AI 활용 워크플로우를 소개합니다.

핵심 포인트

  • Claude를 CTO 리뷰어로 설정하여 위협 모델의 24건 지적 사항 해결
  • Polygon 메인넷 실전 트랜잭션 및 npm alpha 버전 배포 마일스톤 달성
  • AI를 단순 코딩 도구가 아닌 기술적 의사결정 및 검증 도구로 활용
  • AI 에이전트의 스테이블코인 기반 종량제 API 결제 SDK 구현

0. 도입부 후크

Claude에게 「CTO급 외부 리뷰어」 역할을 명시적으로 부여하여 자신의 docs/THREAT_MODEL.md를 리뷰하게 하고, 돌아온 24건의 지적 사항을 모두 closure(종결)한 아침, 마지막 출력에는 이렇게 적혀 있었습니다.

kawasekit@0.1.0의 GA technical sign-off는 문제 없음. 남은 것은 5건의 minor follow-up뿐이며, 모두 blocker(차단 요소)는 아님.

솔로로 build in public(공개 빌드)을 진행하며, Polygon mainnet에서 실전 트랜잭션을 날리고, npm에 0.1.0-alpha.0을 배포하고, docs/THREAT_MODEL.md를 작성하며, 자신이 평소에 놓치기 쉬운 관점으로 다시 읽기 위한 장치로서 Claude에게 CTO 리뷰어 역할을 부여하여, 자신의 문서를 sign-off(승인) 단계로 통과시킬 수 있는 상태에 마침내 도달한 순간이었습니다. 제삼자의 audit(감사)은 1.0을 향한 goal(목표)로 두되, 지속적인 리뷰는 공개 리포지토리와 기사라는 open channel(개방형 채널)에 맡긴다 — 그 전제하에 "기술적으로는 GA ready(정식 출시 준비 완료)"에 가까운 상태(남은 것은 §6.1 idempotency(멱등성)와 amount ceiling(금액 한도)의 gap-closure)까지 스스로 판단할 수 있는 지점에 도달했다는 마일스톤입니다.

본 기사는 M4 마일스톤 (mainnet + 관측성 + CLI + docs 사이트 + 위협 모델 + npm 0.1.0-alpha → beta)의 구현 기록입니다. 이에 더해, 최종장에서는 Claude에게 CTO 리뷰어 역할을 부여하여 docs/THREAT_MODEL.md를 리뷰하게 하고, 19건 + 5건의 follow-up을 closure하며, 그 reviewer(리뷰어)를 재사용 가능한 skill(기술)로 formalize(정형화)하기까지의 프로세스를 기록합니다. Polygon mainnet 상의 3건의 실전 tx(트랜잭션), docs/THREAT_MODEL.md5계층 + 48개 위협, 그리고 24건의 review closure가 결과물입니다.

단순한 스탠드얼론(standalone) 구현 해설이 아니라, **「솔로 OSS가 "타인이 사용할 수 있는" 상태가 되기까지 무엇을 했는가"**를 중심축으로 작성합니다. 설계 판단 → 구현 → 난관(ハマりどころ) → 수정 → 검증, 여기에 더해 → AI에게 CTO 리뷰어 역할을 부여하여 자신에게 sign-off를 내리고, 그 reviewer를 도구화하는 순서로 기록합니다.

1. 지난 회차와의 연결

kawasekit은 제가 build in public으로 진행하고 있는 솔로 OSS 프로젝트로, AI 에이전트가 일본 엔화 스테이블코인 (JPYC)으로 종량제 API를 결제하기 위한 TypeScript SDK입니다.

지난 회차 (#3)에서는 M3 마일스톤의 구현을 작성했습니다. x402 v2 server / client / facilitator를 구현하고, ZeroDev Kernel v3.1 + 자체 제작 envelope로 session-key lifecycle (issue / restore / revoke / rotate)을 구성하였으며, Mastra Agent × Hono × kawasekit 통합 예시에서 Anthropic Claude Sonnet 4.5에게 "Tokyo, Osaka, Kyoto의 날씨를 비교해줘"라고 물으면, 3개 도시분의 JPYC paywall(결제 장벽)을 자율적으로 모두 결제하는 상태까지 구현했습니다. Polygon Amoy 상에 7건의 실전 tx를 남겼고, SDK의 export 수는 113까지 성장했습니다.

M3가 끝난 시점에서의 kawasekit의 위치는 다음과 같이 표현할 수 있었습니다 — 「외부에서 접할 수 있는 SDK」. clean checkout 후 pnpm install && pnpm dev라는 2개의 터미널만으로, 실제 Polygon Amoy 상의 JPYC paywall을 실제 LLM이 결제함. 동작하는 예시가 있고, 난관은 기사 #3에 기록함. 하지만 아직 npm에 출시되지 않음. pnpm add kawasekit으로 누군가가 바로 사용할 수 있는 상태는 아님.

이번 M4에서 할 일은, 그 「접할 수 있는 SDK」를 「타인이 운영 환경(production)에 도입할 수 있는 SDK」로 만드는 것입니다. 구체적으로는:

  • 공개 계층 (Public): Polygon mainnet에서 실전 투입(実弾) → npm에 0.1.0-alpha → beta를 publish
  • 운영 계층 (Operations): 관측성 (Observability (Prometheus / OTLP))을 출하 → 위협 모델 (THREAT_MODEL.md) 작성
  • DX 계층 (Developer Experience): CLI (kawasekit init / account / transfer / policy / session-key) 및 docs 사이트 (Astro Starlight) 구축

M3의 연장선이 아니라, 성격이 다른 페이즈(Phase)입니다. M3까지가 "동작하는 것"을 만드는 기간이었다면, M4는 **"타인이 읽을 수 있고 / 만질 수 있고 / 신뢰할 수 있는 것"**을 만드는 기간입니다. 코드 이외의 산출물 (THREAT_MODEL.md / docs 사이트 / npm provenance)의 비중이 단번에 높아졌습니다.

2. M4의 목표

"타인이 pnpm add kawasekit을 하고 문서를 읽으며, 실제 JPYC를 mainnet에서 구동하고, 무엇이 SDK에 의해 보호되며 무엇이 운영자(operator)의 책임인지를 THREAT_MODEL.md를 통해 확인할 수 있다."

이를 7개의 서브 마일스톤(Sub-milestone)으로 분해했습니다.

내용주요 산출물
M4-1Polygon mainnet 대응
M4-2관측성 레이어 (Observability Layer)
M4-3위협 모델 (Threat Model)
M4-4CLI
M4-5docs 사이트
M4-6npm v0.1.0-alpha → beta
M4-7API 리네임 (Rename)

M3와 마찬가지로, 스코프(Scope) 확대보다 기간 준수를 우선했습니다. 완료 조건은 7개 서브 마일스톤이 모두 완료되는 것과 더불어, Polygon mainnet에서 서로 다른 시나리오 (self-facilitator + session-key)로 각각 1건 이상, 총 2건 이상의 실전 settlement가 성립하는 것입니다.

결론을 먼저 말씀드리면, 7개 서브 마일스톤 모두 당초 예정된 기능을 충족하며 종료되었습니다. Polygon mainnet 상에 3건의 실전 tx를 남겼고, kawasekit@0.1.0-alpha.0가 npm 상에 SLSA v1 provenance를 포함하여 공개되었으며, docs 사이트가 https://kawasekit.k0yote.dev에서 동작하는 상태까지 도달할 수 있었습니다.

그리고, M4를 종료한 후의 추가 장으로서, THREAT_MODEL.md를 Claude에게 CTO 리뷰어 역할로 리뷰하게 하여 19건의 지적 사항 + 5건의 후속 조치(follow-up)를 종결(closure)하고, 그 리뷰어를 스킬화(skill)하는 프로세스가 있었습니다. 이것이 최종장 (Section 12)의 내용입니다.

3. M4의 7개 계통 + 병렬 구조

3. M4의 7개 계통 + 병렬 구조

M4는 M3까지와 달리, 병렬 구현이 가능한 계통이 많은 마일스톤이었습니다. mainnet 실탄 (M4-1)과 THREAT_MODEL (M4-3)은 완전히 독립적이며, 관측성 (Observability, M4-2)과 CLI (M4-4)도 독립적이고, docs 사이트 (M4-5)와 API rename (M4-7) 또한 독립적이었습니다. 이를 다음과 같은 의존 구조로 병렬 진행했습니다 (가로 나열은 병렬화가 가능한 계통, 는 의존 관계).

Phase A (독립 병렬):
M4-1 Mainnet M4-3 THREAT_MODEL
(1.1 → 1.10) (3.1 → 3.6, 병렬)
...

크리티컬 패스(Critical Path)는 **M4-1.8 (mainnet 첫 번째 tx) → M4-6.5 (alpha 공개) → Claude CTO 리뷰 (Section 12)**였습니다. M4-1이 지연되면 하류(downstream) 작업이 전부 중단됩니다. 실제로 M4-1의 mainnet 실탄은 "내가 POL과 JPYC를 입금한 후에야 동작하는" 성질을 가지고 있어, 조작 대기 시간이 발생하기 쉬운 단계였으므로 이곳을 최우선으로 착수했습니다.

이하에는 서브 마일스톤 순서가 아니라, "막혔던 부분"과 "설계 판단"이 집약된 부분부터 순서대로 작성하겠습니다.

4. M4-1: Polygon mainnet 대응

network 인수를 필수화하기

4.1 설계 판단 D1 — M3까지 kawasekit은 Polygon Amoy 전용에 가까운 구현이었습니다. polygonAmoysrc/chains/polygon.ts에서 export 하고, createSelfFacilitatorcreateX402PaymentSigner 모두 "호출 측이 walletClient.chain을 Amoy로 설정하고 있다"는 전제하에 동작했습니다.

mainnet 대응을 시작하면서 가장 먼저 직면한 질문은: 어떻게 "testnet PK를 mainnet에서 실수로 사용하는 사고"를 방지할 것인가였습니다.

선택지는 3가지가 있었습니다.

  • (a) chain object로만 구분하기: walletClient.chain.id만 확인한다. 가장 라이브러리다운 방식이다. 하지만 사용자가 chain: polygonAmoy 상태 그대로 RPC URL만 mainnet으로 향하게 하면 조용히(silently) 실패한다.
  • (b) ENV 가드만 사용하기: KAWASEKIT_ALLOW_MAINNET=1이 없으면 mainnet 조작 불가. 명시적(loud)이지만, 오퍼레이터가 라이브러리 내에서 network를 전환하는 애플리케이션(예: dashboards)에서는 다루기 불편하다.
  • (c) 하이브리드: API에 network: "mainnet" | "testnet"을 필수 인수로 추가하고, walletClient.chain.isTestnet과의 불일치를 construction-time check를 통해 fail-fast 시킨다. 동시에 스크립트 레벨에서는 ENV 가드도 남겨둔다.

채택된 방식은 (c)였습니다. 이유는 **"타입 레벨에서 필수성(requireness)을 표현할 수 있다"**는 점 때문입니다. network를 optional로 설정하면 IDE가 자동 완성에서 아무것도 보여주지 않아 잊어버리기 쉽습니다. required로 설정하면 호출 측이 반드시 의식하게 됩니다.

구현은 src/x402/facilitator.ts (createSelfFacilitator)와 src/x402/client.ts (createX402PaymentSigner) 양쪽 모두에 network: "mainnet" | "testnet"을 추가하고 runtime check를 수행하는 방식입니다.

export interface CreateSelfFacilitatorParams {
readonly network: "mainnet" | "testnet"; // required
readonly walletClient: WalletClient;
...
}

signer 측 (createX402PaymentSigner)은 추가로, 각 sign() 호출 시 paymentRequirements.network

를 chain identity (체인 ID)에 대해 재확인합니다. 서버 측에서 polygon-mainnet을 요구했는데 signer (서명자)가 testnet (테스트넷)으로 구축되어 있다면, 서명 전에 throw (예외 발생) 합니다.

이를 통해 "testnet config (테스트넷 설정)으로 mainnet (메인넷)을 공격"하거나 "mainnet config (메인넷 설정)으로 testnet (테스트넷)을 공격"하는 양방향 사고를 타입 (Type) + 런타임 (Runtime)의 이중 가드로 방지할 수 있습니다. Threat 1.6 (Network / chainId mismatch silently accepted, 네트워크/체인 ID 불일치가 조용히 수용됨)을 ✅ Mitigated (완화됨) 상태로 가져갈 수 있었던 것도 이 결정의 연장선상에 있었습니다.

scripts/11

scripts/12

4.2 실전의 방식 — mainnet (메인넷)에서 실전을 치르기 위해, scripts/07 (Amoy self-settle)과 scripts/09-10 (Amoy session lifecycle)을 mainnet 버전으로 복제했습니다. scripts/11-mainnet-self-settle.tsscripts/12-mainnet-session-flow.ts입니다.

실전을 치르기 전 checklist (체크리스트):

  • mainnet wallet (메인넷 지갑) 3개 (payer EOA / smart account owner / facilitator EOA)에 대해 0.001 JPYC + 필요한 POL 입금
  • KAWASEKIT_X402_CHAIN=polygonKAWASEKIT_ALLOW_MAINNET=1을 env (환경 변수)에 설정
  • dry-run (드라이 런)에 해당하는 --simulate 플래그로 먼저 signature validity (서명 유효성) 확인
  • 실행
  • tx hash (트랜잭션 해시)를 polygonscan에서 확인 + CHANGELOG.md에 기록

Polygon Amoy에서 0.1 POL 가스 (gas)를 예상했던 것과 달리, mainnet에서는 gas tracker (가스 트래커)를 통해 30 gwei 범위 + 0.03 POL을 확인했으므로, facilitator EOA에 0.5 POL을 넣어 여유를 두었습니다.

scripts/11 (M4-1.8)의 출력 로그에서 일부를 인용:

$ pnpm m4:mainnet-self-settle
[chain] Polygon mainnet (137)
[network gate] KAWASEKIT_ALLOW_MAINNET=1 ✓
...

0x6feacc71...은 polygonscan 상의 실제 mainnet tx (메인넷 트랜잭션)입니다: polygonscan에서 확인하기. 로그를 읽어보면 Transfer(payer→recipient, 0.001 JPYC)AuthorizationUsed(payer, nonce)라는 2개의 event (이벤트)가 동일한 tx (트랜잭션)에서 emit (발생)되었습니다. 이것이 EIP-3009를 통한 mainnet settlement (메인넷 결제)가 실제로 성립했다는 움직일 수 없는 증거입니다.

scripts/12 (M4-1.9)는 session-key (세션 키) lifecycle (라이프사이클) 전 단계를 mainnet에서 수행합니다:

  • Pre-revoke transfer (권한 취소 전 전송):
    0x44a91433...

  • Revoke (uninstallValidation, owner sudo client, 권한 취소):
    0x8017f711...

  • Post-revoke transfer (권한 취소 후 전송):
    AA validation phase (AA 검증 단계)에서 revert (되돌리기, 온체인 tx 남지 않음)

세 번째인 "Post-revoke transfer"는 0x 형태의 tx hash (트랜잭션 해시)를 가지지 않습니다. Kernel v3.1의 permission validator (권한 검증기)가 revoke (권한 취소) 이후의 UserOp (사용자 작업)를 validation (검증) 단계에서 reject (거부)하기 때문입니다. 이는 AA (계정 추상화) 세계다운 "온체인에 남지 않는 실패의 증명"이었습니다.

nonceManager를 sudo client에도 attach (연결)

4.3 빠지기 쉬운 함정 — M3에서 "facilitator EOA에 viem의 nonceManager

를 attach(연결)하지 않으면 병렬 settle(결제) 시점에 실패한다"라는 함정에 빠져 있었기에, mainnet 실전에서도 당연히 attach했습니다. 하지만 scripts/12에서 owner 측 sudo client에도 attach하는 것을 잊어버려, 첫 번째 trial(시도)에서 revoke(철회)가 pending(대기) 상태로 돌아오지 않는 문제가 발생했습니다.

// Before (고장 남)
const ownerWallet = createWalletClient({
chain: polygon,
...

bundler를 경유하는 UserOp는 EntryPoint의 nonce key를 사용하므로 언뜻 보면 viem의 nonceManager와 관계가 없어 보이지만, owner client가 동일한 프로세스 내에서 **여러 개의 sponsorship-related RPC call(스폰서십 관련 RPC 호출)**을 던질 때, viem 측(EOA 레벨)의 nonce 관리(nonce handling)가 필요하게 됩니다.

Threat 2.2 (Concurrent settle nonce race)는 M4 self-review(자기 검토) 단계에서 구조체 체크(createSelfFacilitatorwalletClient.account.nonceManager를 구축 시점에 검사) 단계까지 발전시켰습니다. 이에 대해서는 Section 10에서 다루겠습니다.

5. M4-2: 관측성 레이어 (Observability Layer)

5.1 설계 결정 D2 — hooks default + adapter optional

관측성을 어떻게 배포할 것인가에 대해서도 여러 선택지가 있었습니다.

  • (a) prom-client를 직접 prod dep(운영 의존성)으로 설정: 가장 빠르게 진행할 수 있습니다. 하지만 operator(운영자)가 "OTLP exporter를 사용하고 싶다"거나 "어떤 exporter도 사용하고 싶지 않다"고 생각할 때, 불필요한 의존성이 항상 설치됩니다.
  • (b) callback만 제공: SDK는 어떠한 exporter도 갖지 않고, onSettle(event)와 같은 callback만 제공합니다. operator가 원하는 exporter를 직접 작성합니다. 유연하지만 scaffold(뼈대)가 없기 때문에 채택 비용이 높습니다.
  • (c) hooks default + adapter optional subpath: SDK의 core(핵심)는 callback (ObservabilityHooks)만 가지며, kawasekit/observability/prometheuskawasekit/observability/otlpsubpath로서 optional(선택적)하게 제공합니다. peer dependency로 prom-client / @opentelemetry/api를 요구하며, import 하지 않으면 dep tree(의존성 트리)에 포함되지 않습니다.

채택된 방식은 (c)입니다. src/observability/hooks.tsObservabilityHooks interface(인터페이스)를 두고, createSelfFacilitator / createX402Handler / wrapFetch 세 가지 함수가 hooks?: ObservabilityHooks를 받도록 했습니다.

// src/observability/hooks.ts
export interface ObservabilityHooks {
readonly onPaymentRequired?: HookCallback<PaymentRequiredEvent>;
...

invokeHookSafely에서 try/catch를 거는 이유는 operator가 작성한 hook이 SDK의 정상적인 동작을 방해하지 않도록 하기 위해서입니다. observability(관측성)는 최우선적으로 "관측 대상에 영향을 주지 않음"을 보장해야 하며, 그렇지 않으면 본말전도이기 때문입니다.

5.2 Prometheus / OTLP adapter

kawasekit/observability/prometheus subpath에서는 prom-clientRegistry를 하나 전달받고, 각 hook이 해당 Registry에 대해 Counter / Histogram을 increment(증가) 또는 observe(관측)하는 형태로 구현했습니다.

// 조작 예시
import { Registry } from "prom-client";
import { createPrometheusMetrics } from "kawasekit/observability/prometheus";
...

createPrometheusMetrics가 반환하는 metrics.hooks를 facilitator에 그대로 흘려보내면, 내부적으로 kawasekit_facilitator_settle_total{result="success"}와 같은 메트릭 (metrics)이 자동으로 생성됩니다. OTLP 어댑터 (kawasekit/observability/otlp) 도 동일한 인터페이스로, @opentelemetry/sdk-metricsMeter를 받아 Counter / Histogram을 생성하는 형태로 구현되어 있습니다.

peerDependencies를 다음과 같이 설정했으므로, subpath를 사용하지 않는 사용자에게는 prom-client@opentelemetry/api가 설치되지 않습니다.

"peerDependencies": {
"prom-client": ">=15.0.0",
"@opentelemetry/api": ">=1.9.0",
...

examples/observability/ — Grafana에서 보이는 형태까지

5.3 채택 판단을 강화하기 위해, examples/observability/라는 새로운 workspace를 구축하여, docker-compose up 한 번으로 Prometheus + Grafana + kawasekit example server가 실행되고, localhost:3000의 Grafana에서 즉시 대시보드 (dashboard)를 볼 수 있는 상태로 만들었습니다.

examples/observability/
├── docker-compose.yml # Prometheus + Grafana + kawasekit example
├── prometheus.yml # scrape config (kawasekit example의 /metrics)
...

Grafana 대시보드는 6개의 패널(panel)로 구성됩니다: facilitator settle rate, verify failure reason breakdown, client payment p99 latency, facilitator gas spend per minute, paywall acceptance rate, 그리고 알람 (alert) 발생률. 알람 규칙 세트 (ruleset)에는 onPayment_declined가 초당 1건을 초과하면 "Client-side onPayment guard rejected"를 발생시키는 규칙 등을 정의했습니다.

이는 M3의 examples/agent-x402-jpyc/와 동일한 "작동하는 데모 (demo)" 패턴의 관측성 (observability) 버전으로, 운영자 (operator)가 "Prometheus가 작동 중이라고 들었는데, 구체적으로 무엇을 볼 수 있지?"라는 질문에 대해 5분 이내에 확인할 수 있는 상태로 구성했습니다.

6. M4-4: CLI

6.1 commander vs citty

scripts/01-10에서 대체해 온 운영자 (operator) 흐름을 CLI로 승격하는 단계에서, 프레임워크 (framework) 선택이 논점이 되었습니다.

  • commander: 13 버전대에서 안정적 (stable), TS 타입 (types) 양호, 생태계 (ecosystem) 넓음, 다소 평범함
  • citty: Nuxt 계열의 신흥 프레임워크, ESM-first, 합성성 (composability) 높음, 아직 실험적 (experimental) 성격이 강함

M4 단계에서 실험적인 선택은 피하고 싶었기에, commander를 채택했습니다. citty로 전환할 미래를 부정하지는 않지만, 첫 M4 alpha에서 검증된 기반을 선택하는 것은 "타인이 다루기 쉽다"라는 DX (Developer Experience) 목표와 일치합니다.

5개의 명령어를 갖추었습니다:

명령어대체 스크립트내용
kawasekit init(신규).env.example 템플릿 + 필요 파일 스캐폴딩
kawasekit account createscripts/01스마트 계정의 반사실적 주소(counterfactual address) 획득
kawasekit transferscripts/03JPYC 전송 (UserOp 경유)
kawasekit policy create(신규)일일 한도 정책 구축을 위한 대화형 UI
`kawasekit session-key issuerestorerevoke

scripts/05

(verify EIP-3009 스마트 계정)과 scripts/06

(정책 위반(policy violation))과 scripts/07

(x402 자체 정산(self-settle))은 CLI화하지 않고 scripts/에 남겼습니다. 이는 E2E 테스트 성격이 강하여, CLI로서 인체공학적으로(ergonomic) 만드는 의미보다 '재현 가능한 스크립트 검증'으로 유지하는 의미가 더 크다고 판단했기 때문입니다.

6.2 mainnet 가드(guard)를 CLI 레이어에도

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0