WHITEPAPER · TECHNICAL SPECIFICATION

실내외 공기질 데이터 생태계와
온체인 데이터 거래 인프라

전국 단위 IoT 공기질 측정망 — 실내(IAQ)실외(OAQ) 두 계층 — 으로 생활 환경 데이터를 수집해 가전 제조사의 R&D·마케팅과 환경 서비스에 공급하고, 기여자에게는 온체인 보상을 돌려주는 선순환 생태계. Part 1은 사업 백서, Part 2 이후는 이를 구현하는 XRPL DePIN · x402 기술 사양이다.

IAQ PM · CO₂ · TVOC · 온습도 OAQ PM · O₃ · NO₂ · SO₂ · CO XLS-20 기기 라이선스 IOU WLBN 보상 토큰 XLS-30 AMM 유동성 HTTP 402 데이터 API 과금

백서 1 개요

현대인은 하루의 90% 이상을 실내에서 생활하며, 실내 공기질과 기후 환경은 개인의 건강과 직결된다. 그러나 대부분의 실내 공조 가전(공기청정기, 제습기, 에어컨 등)은 출하 시 설정된 표준 알고리즘에 의존해 작동한다.

본 프로젝트는 전국 단위의 IoT 실내 공기질 측정망을 통해 사용자의 실제 거주 환경 데이터를 실시간으로 수집하고, 이를 가전 제조사에 제공하여 기술 발전과 초개인화 마케팅을 지원하는 '실내 기후 데이터 플랫폼 생태계'를 구축하는 것을 목적으로 한다.

90%+
현대인의 하루 중 실내 체류 비중
0
제조사가 확보한 출하 후 실거주 환경 데이터
6+7종
노드가 상시 측정하는 지표 — 실내 IAQ 6종 · 실외 OAQ 7종
문서 구성

Part 1(백서)은 생태계의 사업 구조를 기술한다. Part 2~5(기술 사양)는 이 구조를 XRPL 위에 구현하기 위한 데이터 스키마, 라이선스·보상 설계, 비식별화 파이프라인, API 과금 프로토콜, 그리고 단계별 구현 지침을 다룬다. 백서의 각 장은 대응하는 기술 섹션으로 연결된다.

백서 2 문제 제기 및 시장의 한계

Problem 01
제조사의 실데이터 부재
가전 제조사는 제품 판매 이후 실제 소비자의 가정 내 공기질 변화, 계절별 기기 사용 패턴, 주거 환경(평형, 위치, 채광)에 따른 공조 효율 데이터를 확보하기 어렵다.
Problem 02
획일화된 제품 개발 및 마케팅
지역별·주거 형태별로 요구되는 제습량이나 공기 정화 능력이 다름에도 불구하고, 천편일률적인 스펙 경쟁과 광범위한 마케팅에 의존하고 있다.
데이터 공백이 만드는 비용

제조사는 실환경 데이터가 없어 최악 조건 기준으로 과설계하거나, 반대로 특정 주거 형태에서 성능이 미달하는 제품을 출하한다. 마케팅 측면에서는 "제습이 필요한 가구"를 식별할 수단이 없어 전 고객 대상 브로드캐스트에 의존하며, 이는 곧 리드 획득 단가 상승으로 이어진다. 본 생태계는 이 두 공백을 각각 R&D 데이터코호트 API로 메운다.

백서 3 플랫폼 생태계 구조

본 생태계는 세 주체로 구성되며, 데이터의 수집–정제–활용이 선순환을 이룬다.

DATA PROVIDER 데이터 기여자 가정 · 사무실 · 상업시설 IAQ 측정 IoT 디바이스 설치 기여도 기반 리워드 획득 PLATFORM HUB 플랫폼 허브 원시 데이터 온체인 무결성 보장 개인정보 비식별화 처리 메타데이터 가공 · 품질 평가 XRPL + 클라우드 인프라 DATA CONSUMER 가전 제조사 에어컨 · 제습기 · 공기청정기 환기 시스템 · 스마트홈 플랫폼 구매 또는 구독형 API 원시 측정값 정제 메타데이터 데이터 이용료 기여 보상 실선 = 데이터 흐름 · 점선 = 가치 흐름 보상은 가전 구매 할인 · 필터 구독 결제로 재유입되어 생태계 내에서 순환
Fig 0. 3주체 선순환 구조

3.1 데이터 기여자 (Data Provider)

  • 가정·사무실·상업시설에 실내(IAQ) 측정기를, 옥상·가로변·학교 등에 실외(OAQ) 측정기를 설치한 사용자.
  • 실시간 환경 데이터를 플랫폼에 제공하고, 기여도에 따라 리워드(보상)를 획득한다.

3.2 플랫폼 허브 (Data Platform Hub)

  • 수집된 원시 데이터(Raw Data)를 블록체인 및 클라우드 인프라를 통해 위변조 없이 안전하게 저장한다.
  • 개인정보를 비식별화(익명화) 처리한 후, 제조사가 활용할 수 있는 형태의 메타데이터로 가공한다.

3.3 데이터 수요자 (Appliance Manufacturers)

  • 에어컨, 제습기, 공기청정기, 환기 시스템 등을 제조하는 기업.
  • 정제된 데이터를 구매하거나 구독형 API 형태로 제공받아 R&D 및 마케팅에 활용한다.
기술 대응

기여자 자격 증명 → XLS-20 라이선스 NFT · 원시 데이터 무결성 → 오라클 배치 머클루트 · 비식별화 → 5단계 파이프라인 · 수요자 과금 → x402 데이터 상품.

백서 4 수집 데이터 및 핵심 활용 방안

수집된 데이터는 수요기업의 목적에 맞게 기술적 데이터(R&D)마케팅 데이터(CRM/Sales)로 분류되어 제공된다.

4.1 실내(IAQ) 지표 — 가전 R&D · 마케팅

수집 지표연관 가전제품기술적 활용 방안 (R&D)마케팅 활용 방안 (Sales)
절대습도 / 상대습도제습기, 에어컨 주거 형태별 최적 제습 알고리즘 개발, 결로 방지 예측 모델링 구축 장마철 고습도 유지 가구 타겟팅 제습기 프로모션, 건조 가구 대상 가습기 교체 시기 알림
초미세먼지 (PM2.5/10)공기청정기, 청소기 필터 수명 예측 모델 고도화, 오염도 급증 시 팬 속도 자동 제어 로직 설계 대기오염도와 실내 오염도가 비례하는 가구 추출, 대용량 프리미엄 공기청정기 타겟 광고
이산화탄소 (CO2)환기 청정기 (전열교환기) 밀폐 공간 내 CO2 농도 변화율 기반 AI 환기 시점 예측 기술 개발 환기 부족 다중이용시설 및 영유아 가구 대상 스마트 환기 시스템 도입 제안
VOCs (유기화합물)새집증후군 제거기 건축 자재별 배출 가스 패턴 분석, 센서 감도 최적화 신축 아파트 입주 및 인테리어 시공 가구 대상 특화 필터 마케팅

4.2 실외(OAQ) 지표 — 환경 서비스 · 외기 연동 제어

실외 지표는 수요자 구성이 다르다. 실내 지표가 가전 제조사 한 축이라면, 실외는 지자체·환경 서비스가 1차 수요자이고 가전 제조사는 "외기 조건에 반응하는 제어"라는 형태로 2차 수요자가 된다.

수집 지표주 활용 주체기술적 활용 방안 (R&D)서비스 활용 방안
초미세먼지 (PM2.5/10)가전 제조사 · 지자체 격자별 침투율(I/O ratio) 산출, 외기 연동 선제 급기·차단 제어 알고리즘 개발 외기 악화 예보 기반 창문 닫기·공기청정기 예약 가동 알림
오존 (O₃)지자체 · 보건 광화학 생성 패턴 분석, 환기 금지 시간대 판정 모델 구축 오존주의보 구간 환기 억제 권고 — 환기형 청정기에 제어 신호 전달
이산화질소 (NO₂)지자체 · 교통 도로변 배출 기여도 분리, 가로변 노출 지도 작성 통학로·보행로 저노출 경로 안내
아황산가스 (SO₂)산업단지 · 환경 산업 배출원 영향 반경 추정, 풍향별 확산 모델 검증 인근 주거지 대상 배출 이벤트 알림
일산화탄소 (CO)지자체 · 안전 교통 정체 구간 축적 패턴 분석 밀집 구역 단기 노출 경보
풍속 · 풍향전 주체 오염물질 이동·확산 방향 추정, 배출원 역추적 인접 격자로부터의 오염 유입 예측
두 표를 잇는 값은 PM2.5다

4.1과 4.2에서 PM2.5만 양쪽에 등장한다. 같은 물질을 같은 광산란 방식으로 실내외에서 동시에 측정하기 때문이다. 나머지 지표는 한쪽에만 존재하므로 단독 상품이지만, PM2.5는 두 관측망을 곱해 침투율이라는 파생 지표를 만든다. 이 파생 지표가 Phase 3에서 가장 비싼 데이터가 되는 이유이며, 실외를 기상 관측으로 두었다면 존재하지 않았을 값이다.

마케팅 활용의 전제 조건

위 표의 마케팅 항목은 모두 "특정 조건을 만족하는 가구를 추출"하는 형태다. 이는 개인을 직접 식별하지 않고 코호트 단위 카운트만 반환하는 API로 구현해야 하며, 실제 광고 도달은 플랫폼이 중개하고 제조사에는 대상자 목록을 넘기지 않는다. 구체적 설계는 비식별화 파이프라인코호트 API 절에서 다룬다.

백서 5 데이터 보상 메커니즘

광범위하고 유의미한 데이터를 지속적으로 확보하려면 사용자의 자발적인 IoT 디바이스 운영이 필수적이다.

Earn
기여 보상
24시간 끊김 없이 양질의 데이터를 전송하는 노드(측정기)에 데이터 포인트 또는 유틸리티 토큰을 지급한다.
Spend
생태계 소비
획득한 보상은 가전제품 구매 할인, 필터 교체 구독 서비스 결제, 혹은 플랫폼 내 제휴 마켓에서 사용 가능하도록 설계하여 이탈을 방지한다.
"양질의 데이터"를 정량화해야 하는 이유

제출 건수에만 비례해 보상하면 센서를 방치하거나 조작해 건수만 채우는 것이 최적 전략이 된다. 본 사양은 가동률·이상치율·인근 노드 상관·캘리브레이션 경과일을 합산한 품질 점수(Quality Score)를 도입하고, 기간 보상 예산을 고정한 뒤 점수 지분으로 배분하여 노드 수 증가가 곧 발행량 증가로 이어지지 않도록 설계한다.

백서 6 비즈니스 수익 모델

Revenue 01
B2B 데이터 API 구독료
가전 제조사 및 스마트홈 플랫폼(SmartThings, ThinQ 등)을 대상으로 지역별 실내 공기질 통계 API를 월간/연간 구독 형태로 제공.
Revenue 02
타겟팅 광고 수수료
가전 제조사가 플랫폼의 데이터를 기반으로 특정 환경 요건을 갖춘 사용자에게 맞춤형 제품을 광고할 때 발생하는 리드(Lead) 당 수수료 부과.
Revenue 03
인사이트 리포트 판매
건설사 및 인테리어 기업을 대상으로 평면 및 자재에 따른 실내 공조 효율성 분석 리포트 판매.

세 수익원은 모두 x402 데이터 상품으로 매핑된다 — 구독료는 선불 크레딧, 리드 수수료는 코호트 API 건당 과금, 리포트는 LLM 추론 건당 과금이다. 과금 수단이 하나의 프로토콜로 통일되므로 별도 빌링 시스템 없이 정산이 가능하다.

백서 7 향후 발전 로드맵

PHASE 1
초기 IoT 인프라 구축 및 얼리 어답터 대상 데이터 수집망 확보
디바이스 라이선스 NFT 발급 체계, 텔레메트리 수집 파이프라인, 품질 점수 기반 보상 엔진 가동. 기술 사양의 Step 1~4에 해당한다.
PHASE 2
메이저 가전 제조사와의 MOU 체결 및 시범 데이터 API 연동 (PoC)
비식별화 파이프라인 검증, 코호트 API·집계 API 개설, x402 과금 게이트웨이 연동. Step 5~6B2B 상품 설계에 해당한다.
PHASE 3
실외 OAQ 관측망과 결합해 실내외 연동 스마트홈 제어 솔루션으로 확장
같은 격자의 실외 공기질과 실내 공기질을 함께 확보해 예측 기반 선제 공조 제어를 제공. 두 관측망은 동일한 라이선스·보상·과금 인프라를 공유하며 NFTokenTaxon으로만 구분된다.
Phase 3의 기술적 의미

실외 OAQ는 선행 지표, 실내 IAQ는 결과 지표다. 특히 PM2.5는 두 관측망이 같은 물질을 측정하므로, 같은 격자에서 두 시계열을 함께 확보하면 "외기 PM이 X일 때 이 주거 형태의 실내 PM이 Y로 변한다"는 침투율(I/O ratio) 전이 함수를 직접 학습할 수 있다. 이는 실외를 기상 관측으로 두었을 때는 얻을 수 없는 값이며, 제조사가 살 수 있는 가장 비싼 형태의 데이터가 된다. 본 기술 사양이 두 관측망을 하나의 원장·하나의 과금 체계 위에 올린 이유다.

기술 개요 플랫폼 개요

백서의 3주체 구조를 온체인으로 구현하면 두 개의 경제 루프가 하나의 원장 위에서 연결된다. 공급 측은 IAQ 노드가 실내 환경 데이터를 올리고 토큰 보상을 받는 DePIN 루프이고, 수요 측은 가전 제조사와 AI 에이전트가 데이터 API를 호출하며 토큰을 지불하는 x402 루프다. 두 루프는 AMM 유동성 풀을 통해 가격이 연결된다.

Supply Loop · DePIN
데이터를 올리면 토큰을 받는다
인가된 IoT 기기(NFT 라이선스 보유)가 IAQ 측정값을 전송하면, 품질 점수를 산출해 Operational Wallet이 WLBN을 자동 지급한다. 노드 확산 = 관측망 밀도 증가.
Demand Loop · x402
데이터를 쓰면 토큰을 낸다
제조사·AI 에이전트가 집계·코호트·리포트 API를 호출하면 HTTP 402로 결제를 요구하고, 온체인 트랜잭션 해시 검증 후 응답을 반환한다. 호출량 = 토큰 수요.

두 관측망, 하나의 인프라

실내 IAQ 관측망과 실외 OAQ 관측망은 센서 구성과 데이터 스키마만 다를 뿐, 라이선스 발급·보상 지급·데이터 과금 경로는 완전히 동일하다. 구현상으로는 NFTokenTaxon과 텔레메트리 스키마 버전으로만 구분되며, 지갑·토큰·AMM·x402 게이트웨이는 단일 인스턴스를 공유한다.

구분실내 IAQ 관측망실외 OAQ 관측망
측정 지표PM1.0/2.5/10, CO₂, TVOC, 온·습도, 기압PM2.5/10, O₃, NO₂, SO₂, CO, 온·습도, 풍속·풍향
설치 주체일반 가구 · 사무실 · 상업시설건물 옥상 · 가로변 · 학교 · 산업단지 인근
NFTokenTaxon30262026
주 수요자가전 제조사 · 스마트홈 플랫폼지자체 · 환경 서비스 · AI 에이전트
공유 인프라Issuer/Operational 지갑 · WLBN·RLUSD · AMM 풀 · x402 게이트웨이 · 소각 배치동일 (단일 인스턴스 공유)

PoC가 증명하려는 4가지 명제

#명제검증 수단XRPL 기능
1기기 신원을 온체인으로 위조 불가능하게 증명할 수 있다 verifyDeviceLicense() 온체인 조회XLS-20 NFT
2데이터 기여에 대한 마이크로 보상을 무인 자동화할 수 있다 오라클 수신 → 즉시 PaymentIssued Asset (IOU)
3토큰과 법정화폐(스테이블) 간 가격 발견이 원장 내에서 가능하다 RLUSD → WLBN 스왑 시뮬레이션XLS-30 AMM
4AI 에이전트가 사람 개입 없이 API 사용료를 결제할 수 있다 402 응답 → 결제 → 재요청 → 200Payment + tx 조회
5개인을 식별하지 않고도 제조사가 쓸 만한 코호트를 추출할 수 있다 k-익명성 미달 시 응답 거부, 카운트만 반환오프체인 (온체인 미기록)
왜 XRPL인가

마이크로페이먼트가 성립하려면 수수료가 결제 금액보다 압도적으로 작아야 한다. XRPL의 기본 트랜잭션 비용은 10 drops(0.00001 XRP)이고 확정(finality)까지 3~5초다. 0.002 RLUSD 단위의 집계 API 과금과, 수만 개 노드에 대한 에폭 배치 보상 지급이 수수료에 잡아먹히지 않고 성립하는 이유이며, NFT·IOU·AMM이 스마트컨트랙트 없이 네이티브 트랜잭션 타입으로 제공되어 감사 대상 코드 표면이 작다는 점도 PoC 속도에 유리하다.

기술 개요 시스템 아키텍처

오프체인 3계층(기기 · 게이트웨이 · 소비자)과 온체인 1계층(XRPL 원장)으로 구성된다.

PHYSICAL BACKEND CONSUMER LEDGER 실내 IAQ 노드 PM·CO₂·TVOC·온습도 실외 OAQ 노드 PM·O₃·NO₂·SO₂·CO Device Wallet NFT 라이선스 보유 DePIN Oracle API POST /api/iaq/telemetry license → quality → reward x402 Gateway /api/iaq/* · /api/llm/* 402 → verify tx → 200 집계 · 코호트 · LLM 비식별화 · k-익명성 게이트 인사이트 리포트 생성 가전 제조사 · AI Agent x402 자율 결제 클라이언트 스마트홈 플랫폼 RLUSD → WLBN 스왑 Operational Wallet (Hot) — 보상 지급 · 결제 수취 · 소각 배치 XRP LEDGER XLS-20 NFT 기기 라이선스 WLBN / RLUSD Issued Asset + TrustLine XLS-30 AMM WLBN ↔ RLUSD 풀 Issuer (Cold) 발행 · 소각 수취 기기 서명 verify NFT reward / burn swap payment
Fig 1. 4계층 아키텍처 — 파란선 = DePIN 공급 루프, 녹색선 = x402 수요 루프

지갑 역할 분리

지갑역할키 보관주요 트랜잭션
Issuer (Cold)WLBN·RLUSD 발행 주체, NFT 발행자, 소각 수취처 오프라인 / HSM 가정AccountSet, NFTokenMint, 최초 Payment
Operational (Hot)오라클 보상 지급, x402 결제 수취, 소각 배치 실행 서버 환경변수 (KMS 권장)Payment, AMMDeposit
IoT Device (Mock)테스트용 기기 지갑, NFT 라이선스 보유자 기기 시큐어 엘리먼트 가정TrustSet, NFTokenAcceptOffer
AI Agent (Mock)x402 클라이언트, API 사용료 지불자 에이전트 로컬TrustSet, Payment
Cold / Hot 분리 원칙

Issuer 키가 유출되면 무제한 발행이 가능하므로, 런타임 서버는 절대 Issuer 시드를 로드하지 않는다. 발행은 1회성 부트스트랩 스크립트에서만 수행하고, 이후 모든 상시 트랜잭션은 Operational Wallet이 담당한다. PoC 단계에서도 이 경계를 코드 레벨(모듈 분리 + 별도 .env 키)로 강제해 둔다.

기술 개요 기술 스택 & 네트워크

Language
TypeScript / Node.js 20+
Python 3.10+ (xrpl-py) 대안 가능. 본 문서는 TS 기준으로 기술한다.
XRPL SDK
xrpl.js v3+
WebSocket Client, Wallet, autofill/sign/submitAndWait 사용.
API
Express 또는 NestJS
FastAPI 대안. x402 게이트웨이는 미들웨어 형태로 구현.

네트워크 파라미터

항목비고
WebSocketwss://s.altnet.rippletest.net:51233Testnet 공용 노드
JSON-RPChttps://s.altnet.rippletest.net:51234단발 조회용
Faucetclient.fundWallet()SDK 내장 호출, 계정당 테스트 XRP 지급
Explorertestnet.xrpl.orgtx / account 시각 검증
Base Reserve1 XRP계정 유지 최소 잔액 (Testnet 기준)
Owner Reserve0.2 XRP / 오브젝트TrustLine · NFT Offer · AMM 등 각각 차감
Finality3~5초비동기 대기 및 폴링 설계 필수

통화 코드 인코딩

XRPL의 currency 필드는 3자리 ASCII이거나 40자리 hex(160-bit)여야 한다. WLBN·RLUSD 모두 3자를 초과하므로 hex 인코딩이 필수다. 이 값을 상수로 고정해두지 않으면 TrustSet과 Payment의 통화가 불일치해 tecNO_LINE / tecPATH_DRY가 발생한다.

src/config/currency.tstypescript

// ASCII → 20-byte hex (right-padded with zeros)
export const toCurrencyHex = (code: string): string => {
  if (code.length <= 3) return code;              // 3 chars or fewer pass through
  return Buffer.from(code, 'ascii').toString('hex').toUpperCase().padEnd(40, '0');
};

export const WLBN  = toCurrencyHex('WLBN');   // 574C424E00000000000000000000000000000000
export const RLUSD = toCurrencyHex('RLUSD');  // 524C555344000000000000000000000000000000

환경 변수

.env.exampledotenv

XRPL_ENDPOINT=wss://s.altnet.rippletest.net:51233
XRPL_NETWORK=testnet

# Cold — bootstrap script only. Never inject into the runtime server
ISSUER_SEED=sEd..............................

# Hot — API server runtime
OPERATIONAL_SEED=sEd..............................
OPERATIONAL_ADDRESS=r..............................

# Mock wallets (testing only)
DEVICE_SEED=sEd..............................
AGENT_SEED=sEd..............................

# Pricing policy
X402_PRICE_WLBN=0.1
X402_PRICE_RLUSD=0.001
X402_DEST_TAG=402001
DEPIN_REWARD_WLBN=1
BURN_RATIO=0.5
시드 취급

.env는 반드시 .gitignore에 등록한다. PoC라도 Testnet 시드를 커밋하면 같은 코드가 Mainnet 전환 시 그대로 답습된다. 운영 전환 시에는 .env 대신 KMS/Vault에서 런타임 주입하고, 서명은 Operational 키에 한정한다.

기술 개요 모듈 구조

각 XRPL 기능을 독립 모듈로 분리해 단계별 테스트와 교체가 가능하도록 한다.

src/ ├─ config/ │ ├─ env.ts # env loading + validation (zod) │ └─ currency.ts # WLBN / RLUSD hex constants ├─ xrpl/ │ ├─ client.ts # Client singleton · reconnect · graceful shutdown │ ├─ wallet.ts # Step 1 — wallet create / load / fund │ ├─ token.ts # Step 2 — AccountSet · TrustSet · IOU Payment │ ├─ nft.ts # Step 3 — Mint / Offer / Accept / verifyDeviceLicense │ ├─ amm.ts # Step 5 — AMMCreate · AMMDeposit · path swap │ └─ submit.ts # shared autofill→sign→submitAndWait + tes check ├─ services/ │ ├─ oracle.ts # Step 4 — telemetry intake · validation · reward │ ├─ x402.ts # Step 6 — 402 challenge · tx verification │ ├─ llm.ts # weather/IAQ LLM inference (mock) │ └─ burn.ts # Step 6 — 50% burn batch job ├─ api/ │ ├─ routes.iaq.ts # POST /api/iaq/telemetry │ ├─ routes.llm.ts # POST /api/llm/inference │ └─ middleware.x402.ts# 402 gate middleware ├─ scripts/ │ ├─ 01-bootstrap-wallets.ts │ ├─ 02-issue-tokens.ts │ ├─ 03-mint-license.ts │ ├─ 05-create-amm.ts │ └─ mock-agent.ts # x402 client simulator └─ server.ts

공통 제출 헬퍼

모든 트랜잭션이 tesSUCCESS인지, 그리고 validated: true인지 한 곳에서 검증한다. 이 래퍼를 강제하지 않으면 "제출은 됐지만 원장에 반영되지 않은" 상태를 성공으로 오인하게 된다.

src/xrpl/submit.tstypescript

import { Client, Wallet, SubmittableTransaction, TxResponse } from 'xrpl';

export class TxError extends Error {
  constructor(public code: string, public hash?: string) {
    super(`XRPL tx failed: ${code}${hash ? ` (${hash})` : ''}`);
  }
}

/** autofill → sign → submitAndWait → verify tesSUCCESS + validated */
export async function submit(
  client: Client, wallet: Wallet, tx: SubmittableTransaction,
): Promise<TxResponse> {
  const prepared = await client.autofill(tx);
  const signed = wallet.sign(prepared);
  const res = await client.submitAndWait(signed.tx_blob);

  const meta = res.result.meta;
  const code = typeof meta === 'object' && meta !== null
    ? (meta as { TransactionResult: string }).TransactionResult
    : 'UNKNOWN';

  if (code !== 'tesSUCCESS') throw new TxError(code, res.result.hash);
  if (res.result.validated !== true) throw new TxError('NOT_VALIDATED', res.result.hash);
  return res;
}

공기질 사양 센서 · 텔레메트리 스키마

백서 4장의 지표를 실제 노드가 측정 가능한 물리량과 페이로드 형식으로 확정한다. 실내(IAQ)와 실외(OAQ) 스키마는 지표 목록만 다르고 동일한 봉투(envelope) 구조를 공유하며, 각각 버전 필드를 갖는다.

측정 지표 사양

지표필드센서 방식측정 범위목표 정확도
미세먼지pm1 pm25 pm10 광산란 (PMS)0 ~ 1000 µg/m³±10% (≥100 구간)
이산화탄소co2 NDIR400 ~ 5000 ppm±(50 ppm + 3%)
총휘발성유기화합물tvoc MOX 반도체식0 ~ 60000 ppb상대 지수 (절대값 비보증)
온도tempC 디지털 온습도 복합−10 ~ 60 °C±0.3 °C
상대습도rh 정전용량식0 ~ 100 %RH±2 %RH
기압hpa MEMS 기압식850 ~ 1100 hPa±1 hPa
절대습도 유도ah 온도 + 상대습도에서 계산0 ~ 60 g/m³입력 오차 전파

실외 OAQ 센서 사양

실외 노드는 대기환경기준 물질을 측정한다. PM2.5·PM10은 실내 노드와 같은 물질을 같은 방식으로 측정하므로, 같은 격자의 실내외 값을 직접 비교해 침투율(I/O ratio)을 산출할 수 있다.

지표필드센서 방식측정 범위tier
미세먼지pm25 pm10광산란 (PMS)0 ~ 1000 µg/m³oaq-standard
오존o3전기화학식0 ~ 500 ppboaq-standard
이산화질소no2전기화학식0 ~ 500 ppboaq-standard
온도 · 상대습도tempC rh디지털 복합−30 ~ 60 °C / 0 ~ 100 %RHoaq-standard
아황산가스so2전기화학식0 ~ 200 ppboaq-pro
일산화탄소co전기화학식0 ~ 50 ppmoaq-pro
풍속 · 풍향wind windDir초음파식0 ~ 60 m/s / 0 ~ 360°oaq-pro
실외 센서는 보정 부담이 훨씬 크다

전기화학식 가스 센서(O₃·NO₂·SO₂·CO)는 온도·습도에 강하게 교차 반응하고 수명이 1~2년으로 짧다. 실내 노드보다 드리프트가 빠르므로 품질 점수의 driftDays 가중을 실외에 더 크게 적용하고, 인근 국가 측정망(에어코리아) 값과의 상관을 보정 계수 산출에 함께 쓴다.

이 때문에 oaq-pro 보상 배수를 ×2.4로 두어 실내 iaq-pro(×2.2)보다 높게 잡았다.

절대습도는 왜 유도해야 하는가

백서 4장이 첫 번째 지표로 절대습도를 명시한 것은 정확한 선택이다. 상대습도는 온도 종속량이라 제습 부하 계산에 직접 쓸 수 없다. 같은 60 %RH라도 20 °C에서는 약 10.3 g/m³, 30 °C에서는 약 18.2 g/m³로 실제 수분량이 1.8배 차이난다. 제습기가 뽑아내야 할 물의 양은 후자가 압도적으로 많다. 따라서 노드는 상대습도를 전송하되, 서버는 수신 즉시 절대습도를 유도해 저장한다.

src/services/iaq/humidity.tstypescript

/**
 * Absolute humidity (g/m³) via the Magnus approximation.
 * Dehumidification algorithms and condensation prediction take this, not relative humidity.
 */
export function absoluteHumidity(tempC: number, rh: number): number {
  const es = 6.112 * Math.exp((17.67 * tempC) / (tempC + 243.5));  // saturation vapor pressure, hPa
  const e = es * (rh / 100);                                        // actual vapor pressure, hPa
  return (216.7 * e) / (tempC + 273.15);                            // g/m³
}

/** Dew point (°C) — direct input to the condensation-prevention model */
export function dewPoint(tempC: number, rh: number): number {
  const gamma = (17.67 * tempC) / (tempC + 243.5) + Math.log(rh / 100);
  return (243.5 * gamma) / (17.67 - gamma);
}

// absoluteHumidity(20, 60) → 10.35    absoluteHumidity(30, 60) → 18.21

텔레메트리 페이로드

노드는 측정은 1분 주기, 전송은 5분 주기로 배치한다. 전송 단위마다 기기 키로 서명하며, 서버는 서명자 주소와 deviceAddress가 일치하는지 확인한 뒤 라이선스 NFT를 조회한다. 서명이 없으면 NFT를 보유한 주소를 사칭한 임의 페이로드를 막을 수 없다.

POST /api/iaq/telemetryjson

{
  "schema": "kweather-iaq-telemetry/v1",
  "deviceAddress": "rDeviceWalletAddress...",
  "signature": "3045022100...",
  "batch": [
    {
      "ts": 1786195200,
      "pm1": 6.2, "pm25": 11.4, "pm10": 15.8,
      "co2": 812, "tvoc": 143,
      "tempC": 23.4, "rh": 58.1, "hpa": 1008.3
    },
    {
      "ts": 1786195260,
      "pm1": 6.0, "pm25": 11.1, "pm10": 15.2,
      "co2": 845, "tvoc": 139,
      "tempC": 23.4, "rh": 58.4, "hpa": 1008.2
    }
  ]
}

수신 검증 순서

#검증실패 시비고
1서명이 deviceAddress의 공개키와 일치401 BAD_SIGNATURE온체인 조회 없음 — 가장 싼 검사를 먼저
2배치 내 ts가 단조 증가, 서버 시각 ±10분 이내422 TIMESTAMP_SKEW과거 데이터 재전송(리플레이) 차단
3각 지표가 물리적 범위 이내422 OUT_OF_RANGE센서 고장·조작 1차 필터
4직전 값 대비 급변율이 임계 이내이상치 플래그거부하지 않고 품질 점수에 반영
5라이선스 NFT 보유 (캐시 60초)403 NO_VALID_LICENSE가장 비싼 검사를 마지막에
6제출 주기 하한 준수429 RATE_LIMITED보상 파밍 방지
원시 데이터는 온체인에 올리지 않는다

1분 해상도 × 노드 수만큼의 측정값을 원장에 기록하는 것은 비용·성능 모두 불가능하다. 원시 데이터는 오프체인 저장소에 두고, 정산 주기마다 배치 머클루트만 XRPL 트랜잭션의 Memos 필드에 기록한다. 이후 특정 측정값의 무결성이 다투어지면 머클 증명으로 "그 시점 배치에 이 값이 포함되어 있었다"를 검증할 수 있다. 백서 3.2의 "위변조 없이 저장"은 이 방식으로 달성된다.

공기질 사양 라이선스 계층 · 품질 점수

백서 5장의 "양질의 데이터"를 정량화하고, 노드 확산이 곧 발행량 증가로 이어지지 않도록 보상 예산을 고정한다.

Taxon 및 티어 체계

라이선스 NFT는 NFTokenTaxon으로 관측망을 구분하고, 메타데이터의 license.tier로 센서 구성 등급을 표현한다. 티어는 보상 배수접근 가능한 데이터 상품을 동시에 결정한다.

Taxon관측망Tier필수 센서보상 배수
3026실내 IAQiaq-litePM2.5/10, 온·습도×1.0
3026실내 IAQiaq-standard+ CO₂ (NDIR)×1.6
3026실내 IAQiaq-pro+ TVOC, 기압, PM1.0×2.2
2026실외 OAQoaq-standardPM2.5/10, O₃, NO₂, 온·습도×1.4
2026실외 OAQoaq-pro+ SO₂, CO, 풍속·풍향×2.4

디바이스 메타데이터 (IPFS)

주거 환경 속성은 백서 2장이 지적한 "평형·위치·채광에 따른 공조 효율" 분석에 필수적이다. 다만 이 속성들의 조합은 그 자체로 가구를 특정할 수 있으므로, 메타데이터 단계에서부터 연속값이 아닌 버킷으로만 기록한다.

ipfs://Qm…/iaq-device.jsonjson

{
  "schema": "kweather-iaq-device/v1",
  "name": "Kweather IAQ Node #10427",
  "description": "Indoor Air Quality license",
  "device": {
    "serial": "KW-IAQ-10427",
    "model": "KW-AIR-2",
    "sensors": ["pm1", "pm25", "pm10", "co2", "tvoc", "temp", "rh", "hpa"],
    "installed_at": "2026-05-02"
  },
  "context": {
    "geohash5": "wydm9",
    "space_type": "residential",
    "area_bucket": "60-85sqm",
    "floor_bucket": "5-10F",
    "build_year_bucket": "2010-2019",
    "orientation": "S"
  },
  "license": { "tier": "iaq-pro", "reward_multiplier": 2.2, "valid_until": "2029-05-02" }
}
메타데이터는 공개된다

NFT의 URI는 온체인에 기록되고 IPFS 콘텐츠는 누구나 조회할 수 있다. 따라서 정확한 주소·동호수·연속 좌표를 여기에 넣으면 영구히 공개된다. geohash5(약 4.9 km 격자)와 버킷 값만 기록하고, 정밀 위치는 플랫폼 오프체인 DB에 암호화 보관한다.

품질 점수 (Quality Score)

백서 5장의 "24시간 끊김 없이 양질의 데이터"를 4개 관측 가능한 지표로 분해한다. 특히 인근 노드 상관계수는 센서를 실외에 방치하거나 값을 합성하는 조작을 잡아내는 핵심 항목이다 — 같은 지역의 다른 노드와 온도·PM 추세가 전혀 맞지 않으면 그 노드의 데이터는 신뢰할 수 없다.

src/services/iaq/quality.tstypescript

export interface QualityInputs {
  uptimeRatio: number;    // actual submissions / expected submissions, last 24h
  outlierRatio: number;   // share of out-of-range + sudden-jump outliers
  neighborCorr: number;   // temp/PM trend correlation with nodes within 2km (-1 to 1)
  driftDays: number;      // days since last calibration
}

const clamp01 = (x: number) => Math.min(1, Math.max(0, x));

/** 0 to 1. Weights sum to 1.0 */
export function qualityScore(q: QualityInputs): number {
  const uptime = clamp01(q.uptimeRatio);
  const clean  = clamp01(1 - q.outlierRatio * 4);        // 25% outliers scores zero
  const agree  = clamp01((q.neighborCorr - 0.3) / 0.6);  // zero below 0.3, full marks at 0.9
  const fresh  = clamp01(1 - q.driftDays / 365);         // zero after a year without calibration

  const score = 0.40 * uptime + 0.25 * clean + 0.20 * agree + 0.15 * fresh;
  return Math.round(score * 1000) / 1000;
}

고정 예산 배분 (Emission Cap)

노드 1대당 고정 금액을 지급하면 노드 수에 비례해 발행량이 무한히 늘어난다. 대신 기간 보상 예산을 먼저 고정하고, 각 노드의 티어 배수 × 품질 점수를 지분으로 삼아 나눈다. 노드가 늘어나면 1대당 보상이 줄어들 뿐 총 발행량은 변하지 않는다.

src/services/iaq/reward.tstypescript

import { qualityScore, QualityInputs } from './quality';

export const TIER_MULTIPLIER: Record<string, number> = {
  'iaq-lite': 1.0, 'iaq-standard': 1.6, 'iaq-pro': 2.2,
  'oaq-standard': 1.4, 'oaq-pro': 2.4,
};

export interface NodeEpochStats { address: string; tier: string; quality: QualityInputs; }

/**
 * Reward distribution for one settlement epoch.
 * Total payout never exceeds EPOCH_BUDGET — fixed regardless of node count.
 */
export function distribute(nodes: NodeEpochStats[], epochBudgetWlbn: number) {
  const weighted = nodes.map((n) => ({
    address: n.address,
    weight: (TIER_MULTIPLIER[n.tier] ?? 1) * qualityScore(n.quality),
  })).filter((n) => n.weight > 0);

  const total = weighted.reduce((s, n) => s + n.weight, 0);
  if (total === 0) return [];

  // prevent a single node dominating — per-payout cap is 0.5% of budget
  const perNodeCap = epochBudgetWlbn * 0.005;

  return weighted.map((n) => ({
    address: n.address,
    amount: Math.min((n.weight / total) * epochBudgetWlbn, perNodeCap).toFixed(6),
  }));
}
잔여 리스크 "보상 인플레이션" 해소

이 설계로 잔여 리스크 표의 보상 인플레이션 항목이 구조적으로 닫힌다. 총 발행량은 EPOCH_BUDGET × 에폭 수로 사전에 확정되고, 노드 증가는 개별 보상 희석으로만 반영된다. 다만 희석이 지나치면 참여 유인이 사라지므로, B2B 매출에 연동해 예산을 조정하는 거버넌스 규칙이 별도로 필요하다 (PoC 범위 밖).

배치 지급으로 전환됨

고정 예산 배분은 에폭 종료 시점에 전체 노드의 점수를 알아야 계산할 수 있다. 따라서 Step 4의 요청당 즉시 지급 방식은 IAQ 관측망에서는 사용하지 않는다. 텔레메트리 수신은 202 Accepted로 즉시 응답하고, 보상은 에폭(예: 24시간) 배치로 distribute() 결과를 순차 지급한다. 이는 Sequence 경합 대응과도 자연스럽게 맞물린다.

공기질 사양 비식별화 파이프라인

백서 3.2의 "개인정보를 비식별화 처리한 후 메타데이터로 가공"을 5단계 변환으로 구체화한다. 실내 데이터는 재실 여부·생활 패턴이 그대로 드러나는 민감 정보이므로, 실외 공기질 데이터보다 훨씬 강한 처리가 필요하다.

실내 데이터가 노출하는 것

CO₂ 농도 시계열 하나만으로 재실 인원수, 취침·기상 시각, 부재 기간이 상당히 정확하게 추정된다. PM2.5 급등은 조리 시각을, TVOC 패턴은 청소·인테리어 시공을 드러낸다. 이 데이터가 정확한 위치와 결합되면 사실상 특정 가구의 생활 로그가 된다. 따라서 정밀 위치·원시 시간해상도·개별 노드 식별자가 동시에 외부로 나가는 경로는 존재해서는 안 된다.

5단계 변환

#단계원시공개
P1공간 일반화위경도 (소수 6자리)geohash5 — 약 4.9 km 격자
P2시간 집계1분 해상도 측정값1시간 단위 평균·최대·분산
P3식별자 치환rDeviceAddress…HMAC(NFTokenID, epochSalt) — 에폭마다 회전
P4k-익명성 게이트버킷 내 노드 < 5이면 상위 격자로 롤업, 그래도 미달이면 응답 거부
P5속성 일반화평형 84.3 m², 7층60-85sqm, 5-10F

P3의 에폭마다 회전하는 salt가 중요하다. 고정 salt로 해시하면 가명 ID가 영구 식별자가 되어 장기 추적이 가능해진다. 에폭마다 salt를 교체하면 에폭 간 연결이 끊긴다. 다만 이 경우 장기 시계열 분석이 불가능해지므로, 장기 분석은 가명 ID가 아닌 버킷 단위 집계로만 제공한다.

src/services/iaq/anonymize.tstypescript

export const K_MIN = 5;

export interface NodeSummary { geohash5: string; tier: string; hourly: HourlyStats; }
export interface Bucket { geohash: string; precision: number; n: number; stats: HourlyStats; }

/**
 * P4 — roll up to coarser cells until k-anonymity is satisfied.
 * If geohash3 (~156km) is still short, return null and the API refuses to respond.
 */
export function rollupToKAnonymous(nodes: NodeSummary[], geohash: string): Bucket | null {
  for (let precision = Math.min(5, geohash.length); precision >= 3; precision--) {
    const prefix = geohash.slice(0, precision);
    const members = nodes.filter((n) => n.geohash5.startsWith(prefix));

    if (members.length >= K_MIN) {
      return { geohash: prefix, precision, n: members.length, stats: aggregate(members) };
    }
  }
  return null;
}

/** P3 — derive a pseudonymous ID from a salt rotated each epoch */
export function pseudonym(nftokenId: string, epochSalt: string): string {
  return createHmac('sha256', epochSalt).update(nftokenId).digest('hex').slice(0, 16);
}

차분 공격 방어

k-익명성만으로는 부족하다. 필터를 조금씩 바꿔가며 반복 질의하면 두 결과의 차이로 개별 가구를 분리해낼 수 있다. 예를 들어 "평형 60-85 & 5-10층" 코호트와 "평형 60-85 & 5-10층 & 남향" 코호트의 차이가 1이면 그 1가구의 속성이 그대로 드러난다.

방어규칙효과
카운트 반올림응답 코호트 크기를 5 단위로 반올림차이가 1인 질의쌍이 같은 값을 반환
필터 개수 상한동시 적용 가능한 속성 필터 최대 3개조합 폭발로 인한 좁은 코호트 생성 차단
질의 로그 · 예산구매자별 일일 질의 수 상한, 유사 질의 반복 탐지반복 차분 시도 자체를 제한
최소 코호트반올림 후에도 25 미만이면 거부k=5보다 보수적인 실질 하한

데이터 접근 권한 매트릭스

데이터기여자 본인가전 제조사공개 API
자기 노드 1분 해상도 원시값전체불가불가
정밀 좌표 · 주소전체불가불가
격자 1시간 집계전체구독제한 공개 (지연 24h)
코호트 카운트건당 과금불가
대상자 연락처 · 노드 목록불가불가
광고 도달은 플랫폼이 중개한다

백서 6장의 "타겟팅 광고 수수료" 모델에서 제조사에 넘어가는 것은 코호트 크기와 광고 소재뿐이다. 대상자 목록은 절대 이전되지 않는다. 제조사는 "조건 X를 만족하는 가구 1,240곳에 이 소재를 노출" 요청을 넣고, 플랫폼이 자체 앱·알림 채널로 도달시킨 뒤 리드 수만 정산한다. 이 경계가 무너지면 생태계 전체가 개인정보 처리 리스크에 직접 노출된다.

법적 검토 필요

본 절은 기술적 설계이며 법률 자문이 아니다. 국내 서비스 시 실내 환경 데이터의 개인정보 해당 여부, 가명정보 처리 요건, 수집·이용 동의 문구, 제3자 제공과 위탁의 구분, 그리고 보상 지급이 대가성 있는 데이터 판매로 해석될 여지에 대해 착수 전 개인정보 전문 법률 검토가 반드시 선행되어야 한다.

공기질 사양 B2B 데이터 상품 · 과금 설계

백서 6장의 세 수익원을 x402 프로토콜 위의 세 가지 상품으로 매핑한다. 과금 수단이 하나로 통일되므로 별도 빌링 시스템 없이 온체인 정산이 성립한다.

수익원 (백서 6장)엔드포인트응답과금참고 단가
API 구독료GET /api/iaq/aggregate 격자·시간대별 지표 집계선불 크레딧 차감0.002 RLUSD / call
타겟팅 광고 수수료POST /api/iaq/cohort 조건 만족 코호트 크기만건당 결제0.05 RLUSD / query
인사이트 리포트POST /api/llm/inference LLM 분석 리포트건당 결제0.5 RLUSD 또는 50 WLBN
(R&D 데이터셋)POST /api/iaq/dataset 비식별 학습셋 스냅샷 URL건당 결제 (고액)협의 · 계약 기반

코호트 API — 개인을 노출하지 않는 타겟팅

백서 4장의 마케팅 활용은 전부 이 엔드포인트 하나로 수렴한다. 핵심은 응답에 노드 목록이 없다는 것이다. 크기와 대표 통계만 반환한다.

POST /api/iaq/cohortjson

{
  "region": "wydm",
  "window": { "from": "2026-06-20", "to": "2026-07-20" },
  "filters": [
    { "metric": "ah", "op": "gte", "value": 14.0, "duty": 0.6 },
    { "metric": "area_bucket", "op": "in", "value": ["60-85sqm", "85-135sqm"] }
  ]
}
200 OK — monsoon high-humidity households (WP ch.4 dehumidifier scenario)json

{
  "cohortSize": 1240,
  "rounding": 5,
  "kAnonymity": { "min": 5, "satisfied": true, "effectiveMin": 25 },
  "profile": {
    "ah_p50": 15.8, "ah_p90": 19.2,
    "hours_above_threshold_p50": 431,
    "top_area_bucket": "60-85sqm"
  },
  "reachable": true,
  "note": "No subject list is provided. Ad delivery is brokered by the platform."
}
422 — cohort too narrowjson

{
  "error": "COHORT_TOO_NARROW",
  "minimum": 25,
  "hint": "Relax the filters or reduce region precision. The actual size is not returned."
}

구독형을 x402 위에 올리는 법 — 선불 크레딧

원 x402는 호출 1건 = 결제 1건이다. 집계 API처럼 초당 수십 회 호출되는 상품에 이를 그대로 적용하면 매 호출마다 3~5초의 원장 확정을 기다려야 해 성립하지 않는다. 따라서 402 응답에 건당 결제와 선불 크레딧 두 가지 scheme을 동시에 제시하고, 구독 고객은 후자를 택하게 한다.

HTTP/1.1 402 — two payment schemes offeredjson

{
  "x402Version": 1,
  "resource": "/api/iaq/aggregate",
  "accepts": [
    {
      "scheme": "xrpl-payment",
      "network": "xrpl-testnet",
      "payTo": "rOperationalWalletAddress...",
      "asset": { "currency": "RLUSD", "issuer": "rIssuerAddress...", "value": "0.002" },
      "destinationTag": 402010,
      "maxTimeoutSeconds": 120
    },
    {
      "scheme": "xrpl-credit",
      "network": "xrpl-testnet",
      "payTo": "rOperationalWalletAddress...",
      "topUpMinimum": { "currency": "RLUSD", "issuer": "rIssuerAddress...", "value": "100" },
      "destinationTag": 402011,
      "unitPrice": "0.002",
      "authScheme": "x402-credit .."
    }
  ]
}

크레딧 충전은 온체인 Payment 1건으로 이뤄지고, 이후 호출은 서명으로만 인증된다. 서명 대상에 nonce를 포함시켜 재전송을 막고, nonce는 단조 증가를 강제한다.

src/services/x402.credit.tstypescript

import { verify } from 'ripple-keypairs';
import { verifyPayment } from './x402';

interface CreditAccount { balance: number; currency: 'RLUSD' | 'WLBN'; lastNonce: number; publicKey: string; }
const credits = new Map<string, CreditAccount>();   // use a persistent store in production

/** Top-up — reuses the existing on-chain verification logic as-is */
export async function topUp(txHash: string) {
  const res = await verifyPayment(txHash);          // replay + delivered-amount checks already applied here
  if (!res.ok) return res;

  const acc = credits.get(res.payer) ?? { balance: 0, currency: res.currency, lastNonce: 0, publicKey: res.publicKey };
  acc.balance += Number(res.value);
  credits.set(res.payer, acc);
  return { ok: true as const, balance: acc.balance };
}

/** Call — signature + balance only, no on-chain round trip (~1ms) */
export function spend(header: string, method: string, path: string, unitPrice: number) {
  const [payer, nonceStr, signature] = header.split('.');
  const acc = credits.get(payer);
  if (!acc) return { ok: false as const, reason: 'NO_CREDIT_ACCOUNT' };

  const nonce = Number(nonceStr);
  if (!Number.isInteger(nonce) || nonce <= acc.lastNonce) return { ok: false as const, reason: 'BAD_NONCE' };

  const message = Buffer.from(`${method}:${path}:${nonce}`, 'utf8').toString('hex').toUpperCase();
  if (!verify(message, signature, acc.publicKey)) return { ok: false as const, reason: 'BAD_SIGNATURE' };

  if (acc.balance < unitPrice) return { ok: false as const, reason: 'INSUFFICIENT_CREDIT' };

  acc.balance -= unitPrice;
  acc.lastNonce = nonce;
  return { ok: true as const, remaining: acc.balance };
}
두 scheme의 역할 분담

xrpl-payment저빈도·고단가 상품(코호트 질의, 인사이트 리포트)에, xrpl-credit고빈도·저단가 상품(집계 API)에 쓴다. 크레딧 잔액이 소진되면 서버가 다시 402를 반환하므로, 구매자 에이전트 입장에서는 충전과 사용이 동일한 프로토콜 흐름 안에서 처리된다.

리워드 소비 — 가전 구매 할인 쿠폰 정산

백서 5장의 "생태계 소비"를 온체인으로 구현하면 기여자의 WLBN 소각제조사에 대한 RLUSD 정산이 한 흐름으로 연결된다. 이 경로가 있어야 WLBN이 단순 포인트가 아니라 실제 구매력을 갖는다.

#단계온체인 / 오프체인결과
1기여자가 쿠폰 발행 요청 (예: 5,000 WLBN)오프체인 요청환산율 스냅샷 확정 (AMM 시세 기준)
2기여자 → Issuer로 WLBN Payment온체인TrustLine 상계 = 유통량 감소
3쿠폰 코드 발급 + 제조사 정산 원장에 채무 기록오프체인기여자가 즉시 사용 가능
4월 정산 — 플랫폼 → 제조사 RLUSD Payment온체인제조사는 법정화폐 등가로 수취
쿠폰 발행 한도는 매출에 종속된다

4단계에서 제조사에 지급할 RLUSD는 결국 B2B 데이터 매출에서 나온다. 따라서 기간 쿠폰 발행 총액은 같은 기간 B2B 매출의 일정 비율(예: 40%)을 상한으로 두어야 한다. 이 상한이 없으면 보상은 무제한 발행되는데 정산 재원은 유한한 구조가 되어, 쿠폰 환급 불능 사태로 이어진다. 고정 예산 배분과 이 상한이 함께 걸릴 때만 토큰 경제가 닫힌다.

가치 흐름 요약

fund flow over one epochlog

[IN ]  Manufacturer → Operational   RLUSD  (aggregate credit · cohort · report · dataset)
[IN ]  AI agent      → Operational   WLBN / RLUSD  (per-call x402 payment)
[OUT]  Operational   → nodes         WLBN   distribute(nodes, EPOCH_BUDGET)   ← fixed budget
[BURN] Operational   → Issuer        WLBN   50% of payments received
[BURN] Contributor   → Issuer        WLBN   100% of coupon issuance
[SETL] Operational   → manufacturer  RLUSD  coupons redeemed (cap: B2B revenue × 40%)

STEP 01 XRPL 기반 & 지갑 셋업

Testnet 클라이언트 모듈과 4개 지갑을 준비하고 Faucet으로 자금을 조달한다.

산출물

  • xrpl/client.ts — 재사용 가능한 연결 싱글턴
  • xrpl/wallet.ts — 시드 기반 로드 + 신규 생성/펀딩
  • scripts/01-bootstrap-wallets.ts — 실행 시 .env에 붙여넣을 값 출력
src/xrpl/client.tstypescript

import { Client } from 'xrpl';
import { env } from '../config/env';

let client: Client | null = null;

export async function getClient(): Promise<Client> {
  if (client?.isConnected()) return client;
  client = new Client(env.XRPL_ENDPOINT, { connectionTimeout: 10_000 });
  client.on('disconnected', (code) => console.warn('[xrpl] disconnected', code));
  await client.connect();
  return client;
}

export async function closeClient(): Promise<void> {
  if (client?.isConnected()) await client.disconnect();
  client = null;
}
src/scripts/01-bootstrap-wallets.tstypescript

import { getClient, closeClient } from '../xrpl/client';

const ROLES = ['ISSUER', 'OPERATIONAL', 'DEVICE', 'AGENT'] as const;

async function main() {
  const client = await getClient();

  for (const role of ROLES) {
    // the Faucet creates the account and funds it with initial XRP
    const { wallet, balance } = await client.fundWallet();
    console.log(`\n# ${role}`);
    console.log(`${role}_SEED=${wallet.seed}`);
    console.log(`${role}_ADDRESS=${wallet.classicAddress}   # balance ${balance} XRP`);
  }

  await closeClient();
}

main().catch((e) => { console.error(e); process.exit(1); });
Reserve 계산

각 계정은 Base Reserve 1 XRP를 묶어두고, TrustLine·NFT Offer·AMM 오브젝트마다 0.2 XRP씩 추가로 잠긴다. Device Wallet은 WLBN + RLUSD TrustLine 2개 = 0.4 XRP가 추가로 필요하므로, Faucet 지급액(통상 10~100 XRP)으로 충분하지만 대량 기기 시뮬레이션 시에는 사전 잔액 점검 로직을 넣는다.

STEP 02 WLBN 토큰 발행 (Issued Asset)

Issuer가 WLBN과 Mock RLUSD를 발행하고, 보유 지갑들이 TrustLine을 설정한다.

발행 절차

  1. Issuer 계정 설정AccountSet으로 asfDefaultRipple 활성화, TransferRate(선택) 설정
  2. 보유자 TrustLine — Operational·Device·Agent가 TrustSet으로 한도 설정
  3. 초기 발행 — Issuer → Operational로 Payment를 보내면 그 금액만큼 토큰이 존재하게 됨 (IOU는 별도 mint 트랜잭션이 없음)
Default Ripple은 발행자에서 켠다

XRPL에서 IOU는 항상 발행자 계정을 경유해 이동한다. 따라서 발행자 계정은 asfDefaultRippleON으로 설정해야 한다. 이 플래그가 꺼져 있으면 보유자 간 전송(Operational → Device)이 tecPATH_DRY로 실패하고, Step 4의 보상 지급과 Step 5의 AMM이 모두 성립하지 않는다.

의도치 않은 유동성 전이 방지는 보유자 측 TrustLine에 tfSetNoRipple을 적용해 달성한다. 이 조합은 "발행자를 경유한 정상 전송"은 허용하고 "보유자 계정을 브릿지로 삼는 제3자 경로"는 차단한다.

src/xrpl/token.tstypescript

import { Client, Wallet, AccountSetAsfFlags, TrustSetFlags, IssuedCurrencyAmount } from 'xrpl';
import { submit } from './submit';

/** Issuer account setup — DefaultRipple ON so IOUs can move between holders */
export async function configureIssuer(client: Client, issuer: Wallet) {
  await submit(client, issuer, {
    TransactionType: 'AccountSet',
    Account: issuer.classicAddress,
    SetFlag: AccountSetAsfFlags.asfDefaultRipple,
    Domain: Buffer.from('kweather.example').toString('hex').toUpperCase(),
  });
}

/** Holder TrustLine — NoRipple blocks third-party bridge routing */
export async function setTrustLine(
  client: Client, holder: Wallet, issuerAddress: string,
  currency: string, limit = '1000000000',
) {
  await submit(client, holder, {
    TransactionType: 'TrustSet',
    Account: holder.classicAddress,
    LimitAmount: { currency, issuer: issuerAddress, value: limit },
    Flags: TrustSetFlags.tfSetNoRipple,
  });
}

/** IOU transfer — from the Issuer it is issuance; between holders it is circulation */
export async function sendIOU(
  client: Client, from: Wallet, destination: string,
  amount: IssuedCurrencyAmount, destinationTag?: number,
) {
  return submit(client, from, {
    TransactionType: 'Payment',
    Account: from.classicAddress,
    Destination: destination,
    Amount: amount,
    ...(destinationTag !== undefined ? { DestinationTag: destinationTag } : {}),
  });
}

토큰 정의

토큰Currency (hex)역할초기 공급
WLBN574C424E0000…DePIN 보상 · API 사용료 결제 수단1,000,000 (Operational 배정)
RLUSD (Mock)524C5553440000…스테이블 페어 · 기관 결제 유입 통화100,000 (AMM/테스트용)

참고 실제 RLUSD는 Ripple이 발행하는 스테이블코인이다. PoC에서는 동일 Issuer가 발행하는 Mock RLUSD로 대체하고, Mainnet 전환 시 issuer 주소만 실제 발행자로 교체하도록 상수로 분리해 둔다.

STEP 03 IoT 라이선스 NFT (XLS-20)

기기 1대 = NFT 1개. 온체인에서 라이선스 보유 여부를 조회해 데이터 제출 권한을 판정한다.

발행 → 전송 흐름

NFTokenMint Issuer · URI=ipfs://… NFTokenCreateOffer Issuer · Amount=0 · Sell NFTokenAcceptOffer Device · 수령 확정 account_nfts verifyDeviceLicense() NFTokenID OfferIndex 소유권 이전 Amount=0 판매 오퍼 + Destination 지정 → 지정 기기만 수령 가능 (무상 발급)
Fig 2. XLS-20 라이선스 발급 시퀀스
src/xrpl/nft.tstypescript

import { Client, Wallet, NFTokenMintFlags, NFTokenCreateOfferFlags, convertStringToHex } from 'xrpl';
import { submit } from './submit';

export const TAXON = { OAQ: 2026, IAQ: 3026 } as const;   // license collection per network

/** Mint a license NFT → returns the NFTokenID */
export async function mintLicense(
  client: Client, issuer: Wallet, metadataUri: string, taxon: number = TAXON.IAQ,
) {
  const res = await submit(client, issuer, {
    TransactionType: 'NFTokenMint',
    Account: issuer.classicAddress,
    URI: convertStringToHex(metadataUri),      // ipfs://Qm...
    NFTokenTaxon: taxon,
    Flags: NFTokenMintFlags.tfTransferable,
  });

  // meta.nftoken_id is populated by rippled
  const meta = res.result.meta as { nftoken_id?: string };
  if (!meta.nftoken_id) throw new Error('NFTokenID not found in metadata');
  return meta.nftoken_id;
}

/** Create a zero-price sell offer restricted to one recipient */
export async function offerLicenseTo(
  client: Client, issuer: Wallet, nftokenId: string, destination: string,
) {
  const res = await submit(client, issuer, {
    TransactionType: 'NFTokenCreateOffer',
    Account: issuer.classicAddress,
    NFTokenID: nftokenId,
    Amount: '0',
    Destination: destination,                  // only this address can accept
    Flags: NFTokenCreateOfferFlags.tfSellNFToken,
  });

  const meta = res.result.meta as { offer_id?: string };
  if (!meta.offer_id) throw new Error('OfferID not found in metadata');
  return meta.offer_id;
}

/** Device wallet accepts the offer → ownership transfers */
export async function acceptLicense(client: Client, device: Wallet, offerId: string) {
  return submit(client, device, {
    TransactionType: 'NFTokenAcceptOffer',
    Account: device.classicAddress,
    NFTokenSellOffer: offerId,
  });
}

온체인 라이선스 검증

오라클 엔드포인트가 매 요청마다 호출하는 핵심 함수다. Issuer 주소와 Taxon을 함께 확인해야 임의의 제3자가 발행한 위조 NFT를 거를 수 있다.

src/xrpl/nft.ts (continued)typescript

import { env } from '../config/env';

export interface LicenseInfo {
  valid: boolean;
  nftokenId?: string;
  uri?: string;
  taxon?: number;
}

/** Verify on-chain that the address holds a valid license NFT for this network */
export async function verifyDeviceLicense(
  client: Client, walletAddress: string, taxon: number = TAXON.IAQ,
): Promise<LicenseInfo> {
  const res = await client.request({
    command: 'account_nfts',
    account: walletAddress,
    ledger_index: 'validated',
  });

  const hit = res.result.account_nfts.find(
    (n) => n.Issuer === env.ISSUER_ADDRESS && n.NFTokenTaxon === taxon,
  );
  if (!hit) return { valid: false };

  return {
    valid: true,
    nftokenId: hit.NFTokenID,
    taxon: hit.NFTokenTaxon,
    uri: hit.URI ? Buffer.from(hit.URI, 'hex').toString('utf8') : undefined,
  };
}

메타데이터 스키마 (IPFS) — 실외 OAQ 예시

ipfs://Qm…/oaq-device.jsonjson

{
  "schema": "kweather-oaq-device/v1",
  "name": "Kweather OAQ Node #0042",
  "description": "Outdoor Air Quality station license",
  "device": {
    "serial": "KW-OAQ-0042",
    "model": "KW-OUT-1",
    "sensors": ["pm25", "pm10", "o3", "no2", "temp", "rh"],
    "installed_at": "2026-03-11"
  },
  "context": { "geohash5": "wydm9", "space_type": "outdoor" },
  "license": { "tier": "oaq-standard", "reward_multiplier": 1.4, "valid_until": "2028-03-11" }
}

실내 IAQ 노드의 메타데이터는 주거 환경 버킷을 추가로 포함한다 — 라이선스 계층 · 품질 점수 절의 kweather-iaq-device/v1 스키마 참고. 두 스키마 모두 정밀 좌표를 넣지 않는다는 규칙을 공유한다.

캐싱과 폐기(Revocation)

account_nfts를 요청마다 호출하면 텔레메트리 처리량이 원장 조회에 묶인다. 결과를 주소 단위로 30~60초 TTL 캐싱하고, 라이선스 폐기가 필요하면 NFT를 회수(재오퍼)하거나 tfBurnable 발행 후 NFTokenBurn으로 소각하는 정책을 함께 정의해야 한다. tfTransferable만 설정하면 발행자가 강제 회수할 수 없다는 점에 유의한다.

STEP 04 DePIN 오라클 & 보상 엔진

텔레메트리 수신 → 라이선스 검증 → WLBN 보상 지급까지의 무인 파이프라인.

API 명세

항목내용
EndpointPOST /api/iaq/telemetry · POST /api/aws/telemetry
Body{ schema, deviceAddress, signature, batch: [ { ts, pm25, co2, tvoc, tempC, rh, … } ] }
202{ accepted: 5 } — 수신 확인. 보상은 에폭 배치에서 지급
401{ error: "BAD_SIGNATURE" } — 기기 서명 불일치
403{ error: "NO_VALID_LICENSE" } — 라이선스 NFT 미보유
422{ error: "OUT_OF_RANGE" } / "TIMESTAMP_SKEW"
429{ error: "RATE_LIMITED" } — 제출 주기 위반
src/services/oracle.tstypescript

import { getClient } from '../xrpl/client';
import { verifyDeviceLicense } from '../xrpl/nft';
import { sendIOU } from '../xrpl/token';
import { WLBN } from '../config/currency';
import { env, operationalWallet } from '../config/env';

import { absoluteHumidity } from './iaq/humidity';
import { TAXON } from '../xrpl/nft';

export interface IaqReading {
  ts: number;
  pm1: number; pm25: number; pm10: number;
  co2: number; tvoc: number;
  tempC: number; rh: number; hpa: number;
}

const RANGES: Record<keyof IaqReading, [number, number]> = {
  ts: [0, Number.MAX_SAFE_INTEGER],
  pm1: [0, 1000], pm25: [0, 1000], pm10: [0, 1000],
  co2: [350, 5000], tvoc: [0, 60000],
  tempC: [-10, 60], rh: [0, 100], hpa: [850, 1100],
};

const lastSeen = new Map<string, number>();
const MIN_INTERVAL_MS = 4 * 60_000;      // 5-minute batch cadence, 1 minute of slack

export async function ingest(deviceAddress: string, batch: IaqReading[]) {
  // 1) submission interval floor — prevents reward farming
  const prev = lastSeen.get(deviceAddress) ?? 0;
  if (Date.now() - prev < MIN_INTERVAL_MS) throw new HttpError(429, 'RATE_LIMITED');

  // 2) physical plausibility — check the whole batch
  for (const reading of batch) {
    for (const [k, [lo, hi]] of Object.entries(RANGES)) {
      const v = reading[k as keyof IaqReading];
      if (typeof v !== 'number' || v < lo || v > hi) throw new HttpError(422, 'OUT_OF_RANGE');
    }
  }

  // 3) on-chain license check (IAQ Taxon, 60s TTL cache)
  const client = await getClient();
  const license = await verifyDeviceLicense(client, deviceAddress, TAXON.IAQ);
  if (!license.valid) throw new HttpError(403, 'NO_VALID_LICENSE');

  // 4) derive absolute humidity, store off-chain — only the batch Merkle root goes on-ledger
  const enriched = batch.map((r) => ({ ...r, ah: absoluteHumidity(r.tempC, r.rh) }));
  await saveTelemetry(deviceAddress, enriched, license.nftokenId!);

  // 5) no immediate payout — settled in bulk by distribute() in the epoch batch
  await enqueueForEpoch(deviceAddress, license, enriched);

  lastSeen.set(deviceAddress, Date.now());
  return { accepted: batch.length };      // HTTP 202
}
IAQ 관측망은 배치 정산을 사용한다

요청당 1 Payment를 발생시키면 3~5초의 원장 확정 대기가 HTTP 응답에 포함되고, 노드가 늘수록 Sequence 경합이 심해진다. 더 근본적으로, 고정 예산 배분은 에폭 종료 시점에 전체 노드의 품질 점수를 알아야 계산할 수 있으므로 즉시 지급 자체가 성립하지 않는다.

따라서 수신은 202 Accepted로 즉시 응답하고, 보상은 에폭(예: 24시간) 배치에서 distribute() 결과를 직렬 큐로 순차 지급한다. 실외 OAQ 관측망에서 요청당 즉시 지급을 유지하려면 아래 Sequence 경합 대응이 필수다.

Sequence 경합

Operational Wallet 하나가 동시에 여러 Payment를 서명하면 Sequence 번호가 충돌해 tefPAST_SEQ / terPRE_SEQ가 발생한다. Hot wallet의 트랜잭션 제출은 단일 직렬 큐로 감싸거나, Ticket(TicketCreate)을 미리 확보해 병렬 서명하는 방식 중 하나를 반드시 채택한다.

STEP 05 AMM 유동성 풀 (XLS-30)

WLBN ↔ RLUSD 풀을 생성해 원장 내에서 가격 발견과 즉시 스왑이 가능하게 한다.

AMMCreate
풀 생성
두 자산의 초기 비율이 곧 초기 가격. 특별 수수료(= Owner Reserve, Testnet 0.2 XRP)가 부과된다.
AMMDeposit
유동성 공급
단일 자산 / 양방향 예치 모두 가능. LPToken을 수령한다.
Payment + Paths
스왑 실행
별도 swap 트랜잭션이 없다. 크로스-커런시 Payment가 AMM/오더북을 자동 경유한다.
src/xrpl/amm.tstypescript

import { Client, Wallet, AMMDepositFlags } from 'xrpl';
import { submit } from './submit';
import { WLBN, RLUSD } from '../config/currency';
import { env } from '../config/env';

const wlbn  = (value: string) => ({ currency: WLBN,  issuer: env.ISSUER_ADDRESS, value });
const rlusd = (value: string) => ({ currency: RLUSD, issuer: env.ISSUER_ADDRESS, value });

/** Initial ratio 100,000 WLBN : 1,000 RLUSD → 1 WLBN ≈ 0.01 RLUSD */
export async function createAmm(client: Client, lp: Wallet) {
  return submit(client, lp, {
    TransactionType: 'AMMCreate',
    Account: lp.classicAddress,
    Amount:  wlbn('100000'),
    Amount2: rlusd('1000'),
    TradingFee: 500,          // 0.5% (unit: 1/100,000)
  });
}

/** Two-asset deposit */
export async function depositBoth(client: Client, lp: Wallet, a: string, b: string) {
  return submit(client, lp, {
    TransactionType: 'AMMDeposit',
    Account: lp.classicAddress,
    Asset:  { currency: WLBN,  issuer: env.ISSUER_ADDRESS },
    Asset2: { currency: RLUSD, issuer: env.ISSUER_ADDRESS },
    Amount: wlbn(a),
    Amount2: rlusd(b),
    Flags: AMMDepositFlags.tfTwoAsset,
  });
}

/** RLUSD → WLBN swap: fixed target amount + max spend cap (slippage defense) */
export async function swapRlusdForWlbn(
  client: Client, buyer: Wallet, targetWlbn: string, maxRlusd: string,
) {
  const paths = await client.request({
    command: 'path_find', subcommand: 'create',
    source_account: buyer.classicAddress,
    destination_account: buyer.classicAddress,
    destination_amount: wlbn(targetWlbn),
    source_currencies: [{ currency: RLUSD, issuer: env.ISSUER_ADDRESS }],
  } as never);

  return submit(client, buyer, {
    TransactionType: 'Payment',
    Account: buyer.classicAddress,
    Destination: buyer.classicAddress,     // to yourself = currency conversion
    Amount: wlbn(targetWlbn),
    SendMax: rlusd(maxRlusd),              // slippage cap
    Paths: (paths.result as { alternatives: { paths_computed: unknown }[] })
      .alternatives[0]?.paths_computed as never,
    Flags: 0x00020000,                     // tfPartialPayment not used
  });
}

/** Query pool state */
export async function getPoolState(client: Client) {
  const res = await client.request({
    command: 'amm_info',
    asset:  { currency: WLBN,  issuer: env.ISSUER_ADDRESS },
    asset2: { currency: RLUSD, issuer: env.ISSUER_ADDRESS },
  } as never);
  return res.result;
}
스왑 시 주의점
  • 자기 자신에게 보내는 Payment가 XRPL의 통화 변환 관용구다. Destination을 본인 주소로 둔다.
  • SendMax를 반드시 지정한다. 없으면 풀 상태에 따라 예상보다 훨씬 많은 RLUSD가 소진될 수 있다.
  • tfPartialPayment를 켜면 목표량 미달로도 성공 처리되므로, 결제 검증 로직에서 실제 전달액(delivered_amount)을 확인해야 한다. Step 6의 검증에서 이 필드를 쓰는 이유다.
  • 양쪽 자산 모두 매수자에게 TrustLine이 설정돼 있어야 한다.

STEP 06 x402 자율 결제 게이트웨이

PoC의 핵심. 외부 AI 에이전트가 사람 개입 없이 HTTP 402 응답을 읽고, 온체인 결제 후 재요청해 데이터를 얻는다.

AI Agent x402 Gateway XRPL ① POST /api/llm/inference (no auth header) ② 402 Payment Required + 결제 조건 JSON ③ Payment 0.1 WLBN → Operational (DestinationTag) ④ tesSUCCESS · tx_hash (3~5s finality) ⑤ 재요청 Authorization: x402 <tx_hash> ⑥ nonce 중복 확인 후 tx 조회 ⑦ Destination · Amount · tesSUCCESS ⑧ 200 OK + LLM 추론 결과 실패 시 → 409 (원장 미반영, Retry-After) / 402 (조건 불일치 · 해시 재사용)
Fig 3. x402 챌린지–결제–검증 시퀀스

1차 요청 — 402 챌린지

HTTP/1.1 402 Payment Requiredhttp

HTTP/1.1 402 Payment Required
Content-Type: application/json
WWW-Authenticate: x402 realm="kweather-llm", network="xrpl-testnet"

{
  "x402Version": 1,
  "resource": "/api/llm/inference",
  "accepts": [
    {
      "scheme": "xrpl-payment",
      "network": "xrpl-testnet",
      "payTo": "rOperationalWalletAddress...",
      "asset": { "currency": "WLBN", "issuer": "rIssuerAddress...", "value": "0.1" },
      "destinationTag": 402001,
      "maxTimeoutSeconds": 120
    },
    {
      "scheme": "xrpl-payment",
      "network": "xrpl-testnet",
      "payTo": "rOperationalWalletAddress...",
      "asset": { "currency": "RLUSD", "issuer": "rIssuerAddress...", "value": "0.001" },
      "destinationTag": 402001,
      "maxTimeoutSeconds": 120
    }
  ],
  "description": "Kweather Weather LLM inference — per-call pricing"
}

2차 요청 — 트랜잭션 검증

src/services/x402.tstypescript

import { getClient } from '../xrpl/client';
import { WLBN, RLUSD } from '../config/currency';
import { env } from '../config/env';

const usedHashes = new Set<string>();     // replace with Redis SETNX + TTL in production

export type VerifyResult =
  | { ok: true; currency: 'WLBN' | 'RLUSD'; value: string }
  | { ok: false; reason: string };

const PRICES = {
  [WLBN]:  { label: 'WLBN'  as const, min: Number(env.X402_PRICE_WLBN)  },
  [RLUSD]: { label: 'RLUSD' as const, min: Number(env.X402_PRICE_RLUSD) },
};

export async function verifyPayment(txHash: string): Promise<VerifyResult> {
  // 0) block reuse — stops unlimited calls with the same hash
  if (usedHashes.has(txHash)) return { ok: false, reason: 'HASH_ALREADY_USED' };

  const client = await getClient();
  let res;
  try {
    res = await client.request({ command: 'tx', transaction: txHash, binary: false });
  } catch {
    return { ok: false, reason: 'TX_NOT_FOUND' };   // may not have propagated yet → prompt a retry
  }

  const tx = res.result.tx_json ?? (res.result as never as Record<string, unknown>);
  const meta = res.result.meta as { TransactionResult: string; delivered_amount?: unknown };

  // 1) ledger finality + success
  if (res.result.validated !== true) return { ok: false, reason: 'NOT_VALIDATED' };
  if (meta.TransactionResult !== 'tesSUCCESS') return { ok: false, reason: meta.TransactionResult };
  if ((tx as { TransactionType: string }).TransactionType !== 'Payment')
    return { ok: false, reason: 'NOT_A_PAYMENT' };

  // 2) recipient matches
  if ((tx as { Destination: string }).Destination !== env.OPERATIONAL_ADDRESS)
    return { ok: false, reason: 'WRONG_DESTINATION' };

  // 3) check actual delivered amount — trust delivered_amount to block PartialPayment bypass
  const delivered = meta.delivered_amount as
    { currency: string; issuer: string; value: string } | string | undefined;
  if (!delivered || typeof delivered === 'string')
    return { ok: false, reason: 'XRP_NOT_ACCEPTED' };

  const price = PRICES[delivered.currency];
  if (!price) return { ok: false, reason: 'UNSUPPORTED_CURRENCY' };
  if (delivered.issuer !== env.ISSUER_ADDRESS) return { ok: false, reason: 'WRONG_ISSUER' };
  if (Number(delivered.value) < price.min) return { ok: false, reason: 'INSUFFICIENT_AMOUNT' };

  usedHashes.add(txHash);
  return { ok: true, currency: price.label, value: delivered.value };
}

게이트웨이 미들웨어

src/api/middleware.x402.tstypescript

import { RequestHandler } from 'express';
import { verifyPayment } from '../services/x402';
import { buildChallenge } from '../services/x402.challenge';

export const x402Gate: RequestHandler = async (req, res, next) => {
  const auth = req.header('authorization');
  const match = auth?.match(/^x402\s+([A-F0-9]{64})$/i);

  if (!match) {
    res.setHeader('WWW-Authenticate', 'x402 realm="kweather-llm", network="xrpl-testnet"');
    return res.status(402).json(buildChallenge(req.path));
  }

  const result = await verifyPayment(match[1]);
  if (!result.ok) {
    // not yet in the ledger → prompt retry; otherwise reissue 402
    const retryable = result.reason === 'TX_NOT_FOUND' || result.reason === 'NOT_VALIDATED';
    if (retryable) { res.setHeader('Retry-After', '3'); return res.status(409).json(result); }
    return res.status(402).json({ ...buildChallenge(req.path), error: result.reason });
  }

  res.setHeader('X-Payment-Response', JSON.stringify({ settled: true, ...result }));
  next();
};

AI 에이전트 클라이언트 Mock

src/scripts/mock-agent.tstypescript

import { getClient } from '../xrpl/client';
import { sendIOU } from '../xrpl/token';
import { agentWallet, env } from '../config/env';
import { toCurrencyHex } from '../config/currency';

const API = 'http://localhost:3000/api/llm/inference';
const body = { query: 'Summarize tomorrow\'s indoor humidity outlook for Seoul Gangnam' };

async function call(headers: Record<string, string> = {}) {
  return fetch(API, {
    method: 'POST',
    headers: { 'content-type': 'application/json', ...headers },
    body: JSON.stringify(body),
  });
}

async function main() {
  // (1) call without payment → receive 402 + payment terms
  const challenge = await call();
  if (challenge.status !== 402) throw new Error(`expected 402, got ${challenge.status}`);
  const terms = await challenge.json();
  const option = terms.accepts.find((a: { asset: { currency: string } }) => a.asset.currency === 'WLBN');
  console.log('[agent] terms received:', option.asset.value, option.asset.currency);

  // (2) pay on-chain — the agent decides and pays on its own
  const client = await getClient();
  const tx = await sendIOU(
    client, agentWallet, option.payTo,
    { currency: toCurrencyHex(option.asset.currency), issuer: option.asset.issuer, value: option.asset.value },
    option.destinationTag,
  );
  const hash = tx.result.hash;
  console.log('[agent] payment settled:', hash);

  // (3) retry with tx_hash attached (retries absorb propagation delay)
  for (let i = 0; i < 5; i++) {
    const res = await call({ authorization: `x402 ${hash}` });
    if (res.status === 200) { console.log('[agent] response:', await res.json()); return; }
    if (res.status !== 409) throw new Error(`failed ${res.status}: ${await res.text()}`);
    await new Promise((r) => setTimeout(r, 3000));
  }
  throw new Error('verification timed out');
}

main().catch((e) => { console.error(e); process.exit(1); });

Value Capture — 50% 소각 배치

결제로 수취한 WLBN의 절반을 Issuer(= blackhole 역할)로 되돌려 유통량을 줄인다. IOU를 발행자에게 되돌려 보내면 그 부채가 상계되어 실질적으로 소각된다.

src/services/burn.tstypescript

import { getClient } from '../xrpl/client';
import { sendIOU } from '../xrpl/token';
import { WLBN } from '../config/currency';
import { env, operationalWallet } from '../config/env';

/** Return BURN_RATIO of the WLBN received via x402 this period to the Issuer = burn */
export async function runBurnBatch(periodRevenueWlbn: string) {
  const amount = (Number(periodRevenueWlbn) * Number(env.BURN_RATIO)).toFixed(6);
  if (Number(amount) <= 0) return { burned: '0' };

  const client = await getClient();
  const res = await sendIOU(client, operationalWallet, env.ISSUER_ADDRESS, {
    currency: WLBN, issuer: env.ISSUER_ADDRESS, value: amount,
  });

  console.log(`[burn] ${amount} WLBN → issuer  tx=${res.result.hash}`);
  return { burned: amount, txHash: res.result.hash };
}
XRPL의 "소각"은 스마트컨트랙트 burn과 다르다

IOU는 발행자의 부채 기록이다. 보유자가 발행자에게 되돌려 보내면 TrustLine 잔액이 상계되어 유통량이 실제로 감소한다. 별도의 burn 함수는 필요 없다. 다만 Issuer 계정이 여전히 DefaultRipple과 발행 권한을 갖고 있으므로 "재발행 불가"를 담보하려면 별도의 blackhole 계정(regular keyrrrrrrrrrrrrrrrrrrrrrhoLvTp로 설정하고 마스터키 비활성화)을 쓰는 것이 더 강한 보증이다. PoC에서는 Issuer 반환으로 충분하나, 토크노믹스를 대외 공표할 때는 이 차이를 명시해야 한다.

운영 토큰 가치 순환

공급(보상)과 수요(API 결제), 그리고 소각이 만드는 순환 구조.

WLBN 유통 공급량 IoT 기기 확산 라이선스 NFT 발급 데이터 품질·밀도 ↑ 관측망 커버리지 확대 LLM API 호출량 ↑ x402 자율 결제 수익 50% 소각 유통량 감소 관측 데이터 제출 1 WLBN 보상 → 공급 ↑ 데이터 가치 상승 0.1 WLBN 결제 수취 소각 → 유통량 ↓ 토큰 가치 ↑ → 신규 기기 유입 (플라이휠 폐루프)
Fig 4. 공급–수요–소각 플라이휠 · AMM은 WLBN↔RLUSD 가격 발견을 담당
파라미터PoC 값조정 레버
노드 보상에폭 예산 고정 분배 · α=0.50시뮬레이션으로 확정. 목표 노드 25,000대, 유입 상한 45,000대
API 단가집계 0.002 · 코호트 0.05 · 리포트 0.5 RLUSD상품별 차등, 선불 크레딧 할인율, 계약 기반 데이터셋
소각률결제 수취분 50% + 쿠폰 발행분 전량운영비 대비 조정, 유통량 목표에 연동
쿠폰 발행 상한기간 B2B 매출 × 40%α 천장을 0.667로 결정하는 제약. 월 매출 5,000만원 도달 전까지 상환 미개방
AMM 수수료0.5% (TradingFee 500)LP 인센티브 vs 스왑 비용 트레이드오프
공급 초과가 구조적으로 닫히는 지점

초기 설계에서는 보상이 제출 건수에 비례해 무한 증가하는 반면 소각은 API 수요에 종속되어, 수요가 붙기 전 노드가 먼저 늘면 공급 초과가 확정적이었다. 고정 예산 배분은 이 고리를 끊는다 — 총 발행량은 에폭 예산 × 에폭 수로 사전 확정되고, 노드 증가는 개별 보상 희석으로만 반영된다.

대신 두 개의 새로운 제약이 생긴다. (a) 희석이 지나치면 참여 유인이 사라지므로 B2B 매출에 연동한 예산 조정 거버넌스가 필요하고, (b) 쿠폰 정산 재원이 유한하므로 쿠폰 발행 총액 상한이 반드시 함께 걸려야 한다. 이 두 규칙까지 들어와야 토큰 경제가 닫힌다.

운영 에폭 예산 시뮬레이션

에폭 예산 규모와 희석 곡선을 노드 수 시나리오별로 계산해 수치를 확정한다. 아래 값은 모두 sim/tokenomics.mjs 실행 결과이며, 파라미터를 바꿔 재현할 수 있다. 예측이 아니라 가정 기반 모델 출력이라는 점을 전제로 읽어야 한다.

결론 먼저

Finding 01
보상 곡선은 단봉형이다
매출이 커버리지에 종속되므로 노드당 보상은 N_half(25,000대)에서 최대이고 그 이후 감소한다. 노드를 무작정 늘리는 것이 최적이 아니다. 공격 시나리오(12만 대)의 노드당 보상은 592원으로 유지 하한의 39%에 불과하다.
Finding 02
α=0.40으로는 유지 하한에 못 미친다
최대 보상이 1,471원으로 유지 하한 1,500원을 넘지 못한다. 세 시나리오 모두 M9~M17부터 하한 아래에 머문다. α를 0.45 이상으로 올려야 지속 구간이 열린다.
Finding 03
24개월 회수는 보상만으로 불가능
lite 노드의 24개월 회수에 필요한 α는 0.678로, 쿠폰 상한이 허용하는 천장 0.667을 넘는다. 회수 목표를 36개월로 두거나 하드웨어 원가를 낮춰야 한다.
Finding 04
tier 배수 조정은 제로섬이다
예산이 고정이므로 상위 tier 배수를 올리면 lite가 그만큼 깎인다. 회수기간을 평준화할 수는 있어도 어느 조합도 전 tier 24개월을 만족시키지 못한다. 해법은 배수가 아니라 하드웨어 보조다.

모델 구조와 가정

핵심은 매출이 노드 수의 S-curve 함수라는 점이다. 노드가 적으면 k-익명성 미달로 팔 수 있는 상품 자체가 없고, 커버리지가 임계를 넘으면 계약이 급증하며, 이후 포화한다. 예산은 max(부트스트랩 감쇠분, α × 매출)로 정의한다 — 순수 매출 연동은 부트스트랩이 불가능하고(매출 0 → 보상 0 → 노드 0), 순수 고정은 매출과 무관하게 발행되어 토큰이 무너지기 때문이다.

core model equationslog

revenue   R(n)      = (R_cap / 12) · n² / (n² + N_half²)
budget    B(t, n)   = max( B₀ · 0.5^(t / T_half),  α · R(n) )
reward    r(n, tier) = B · mult[tier] / (n · w̄)      w̄ = Σ share·mult
steady    r(n)      = α · (R_cap/12) · mult · n / (w̄ · (n² + N_half²))   ← single-peaked in n
peak      r_max     = α · (R_cap/12) · mult / (w̄ · 2 · N_half)           @ n = N_half
파라미터근거 / 성격
토큰 환산1 WLBN = 10원쿠폰 상환 고정 환산. AMM 가격의 하방 지지선 역할
α (예산/매출)0.40 → 0.50 권장검증 대상 레버
부트스트랩 예산월 3,000만원, 반감기 12개월가정
R_cap (연 매출 상한)30억원가정 — 가장 불확실한 항목
N_half25,000대전국 균등 커버리지 가정
tier 구성비lite 55% / std 30% / pro 15%가정
하드웨어 원가55,000 / 120,000 / 210,000원센서 BOM 기준 추정
전기료200원/월 (2W 상시)산출
유지 하한1,500원/월가정 — 이하면 관리 동기 상실
쿠폰 상환율발행 WLBN의 60%가정
쿠폰 상한매출의 40%정책 — α 천장을 0.667로 결정

희석 곡선

α=0.50 지속 구간 12,927 ~ 48,347대 0500 1,0001,5002,000 노드당 보상 (원/월) 유지 하한 1,500원 1천2.5천5천 1만2.5만5만 10만15만 노드 수 (로그 스케일) peak 1,838원 α=0.50 α=0.45 α=0.40 (현행) — 하한 미달
Fig 5. 정상상태 노드당 보상 (lite tier) — 매출 연동 예산만 가정

세 곡선 모두 25,000대에서 정점을 찍고 내려온다. α=0.40 곡선(보라)은 유지 하한선에 한 번도 닿지 않는다. α=0.45부터 하한선과 두 점에서 교차하며, 그 사이가 지속 가능 구간이다. 구간의 하단은 매출이 아직 부족한 상태, 상단은 희석이 과도한 상태를 뜻한다.

α최대 보상구간 하단구간 상단구간 폭
0.301,103원도달 불가
0.40 현행1,471원도달 불가 — 최대 보상 < 유지 하한
0.451,654원15,942대39,205대23,263대
0.50 권장1,838원12,927대48,347대35,420대
0.602,206원9,808대63,721대53,913대
0.667 천장2,452원8,538대73,202대64,664대

천장 α는 쿠폰 상한 ÷ 상환율 = 0.40 ÷ 0.60 = 0.667을 넘을 수 없다. 이를 넘기면 쿠폰 상환 재원이 매출을 초과해 환급 불능에 빠진다.

시나리오별 36개월 시뮬레이션 (현행 α=0.40)

시나리오M12M18M24M36부트스트랩 소요하한 미달
보수 (1.2만 대) 3,323대
3,320원
6,000대
1,300원
8,677대
911원
11,362대
1,108원
3.33억원M17~M36
기본 (4만 대) 9,259대
1,191원
20,000대
1,435원
30,741대
1,440원
38,936대
1,337원
2.38억원M11~M36
공격 (12만 대) 20,838대
1,447원
60,000대
1,044원
99,162대
697원
118,897대
592원
1.93억원M9~M36

각 칸은 노드 수 / lite 노드당 월 보상이다. 공격 시나리오가 부트스트랩 자본은 가장 적게 쓰지만(1.93억원 — 매출이 빨리 붙어서), M36 보상은 592원으로 가장 나쁘다. 노드를 빨리 늘린 대가로 희석이 심해진 것이다. 보수 시나리오는 반대로 자본을 3.33억원 쓰고도 M36 보상이 1,108원에 그친다 — 매출 곡선의 정점에 못 미쳐서다.

부트스트랩 구간에는 쿠폰 상환을 열지 마라

세 시나리오 모두 M0부터 쿠폰 상한을 위반한다. 매출이 거의 없는데 부트스트랩 예산 3,000만원의 60%인 1,800만원이 상환 청구로 나가기 때문이다. 이는 모델 오류가 아니라 정책 공백이다.

따라서 쿠폰 상환은 월 매출이 일정 수준(예: 5,000만원)에 도달한 뒤 개방하고, 그 전까지 보상은 적립만 하거나 플랫폼이 원가로 흡수할 수 있는 제휴 마켓 상품으로만 소비하게 한다. 이 규칙이 없으면 부트스트랩 자본이 보상 예산과 쿠폰 상환에 이중으로 소모된다.

tier 배수 검증

동일 예산(1억) · 동일 노드 수(4만) 기준으로 tier별 하드웨어 회수기간을 비교했다.

배수 세트가중평균litestandardpro
현행 1.0 / 1.6 / 2.21.360 1,838원
33.6개월
2,941원
43.8개월
4,044원
54.6개월
원가비례 1.0 / 2.2 / 3.81.780 1,404원
45.7개월
3,090원
41.5개월
5,337원
40.9개월
배수를 올려도 총량은 늘지 않는다

예산이 고정이므로 상위 tier 배수를 올리면 lite가 정확히 그만큼 깎인다 (33.6 → 45.7개월). 원가비례 배수는 회수기간을 41~46개월로 평준화하지만, 어느 조합도 전 tier 24개월을 만족시키지 못한다.

그럼에도 현행 배수를 그대로 두면 실질적 문제가 생긴다. lite만 경제성이 있으니 lite만 늘어나고, 결과적으로 제조사가 가장 원하는 CO₂·TVOC 데이터가 모이지 않는다. 해법은 배수 조정(제로섬)이 아니라 상위 tier 하드웨어 원가를 낮추는 것이다 — 제조사 번들, 렌탈, 또는 직접 보조.

레버 민감도

lite 노드의 회수 목표를 달성하려면 각 레버를 단독으로 얼마까지 움직여야 하는가.

목표필요 보상필요 α필요 R_cap필요 N_half필요 HW 원가
회수 24개월2,492원0.678 천장 초과 50.8억원14,755대30,494원
회수 36개월1,728원0.470 35.3억원21,279대45,741원
가장 강력한 레버는 N_half다

r_max = α·(R_cap/12)·mult / (w̄ · 2 · N_half) — 최대 보상은 N_half에 반비례한다. N_half를 25,000 → 15,000으로 낮추면 α를 건드리지 않고도 최대 보상이 1,471 → 2,451원으로 뛴다.

실무적으로 N_half를 낮추는 방법은 전국 균등 배치를 포기하는 것이다. k-익명성은 지역별로 충족하면 되므로, 전국 격자를 모두 채우는 대신 수도권·광역시의 인구 밀집 격자에 집중 배치하면 훨씬 적은 노드로 상품성이 생긴다. "전국 커버리지"는 마케팅 문구로는 좋지만 토크노믹스에는 직접적인 독이다.

권장 파라미터 세트

항목확정값근거
α (예산 / 매출)0.50지속 구간 35,420대 확보. 쿠폰 유출은 매출의 30%로 상한 40% 대비 10%p 여유
목표 노드 수25,000대보상 곡선 정점. 이 지점의 lite 보상 1,838원
신규 유입 상한45,000대지속 구간 상단 48,347대 직전. 초과 시 신규 라이선스 발급 중단
회수기간 목표36개월24개월은 α 천장을 넘겨 구조적으로 불가
tier 배수현행 1.0 / 1.6 / 2.2 유지배수 조정은 제로섬. 원가 측면에서 해결
하드웨어 보조std 18% · pro 34%36개월 회수 기준 필요 원가 98,682원 / 138,388원
노드당 지급 상한예산의 0.5% 유지200대 미만에서만 구속. 부트스트랩 초기 며칠만 영향
쿠폰 상환 개방 조건월 매출 5,000만원 이상부트스트랩 구간의 이중 지출 차단
recommended set — verification output (α=0.50, n=25,000)log

Sustainable band (maintenance floor): 12,927 ~ 48,347 nodes   peak KRW 1,838 @ 25,000

tier          reward@25k    HW cost   payback   cost for 36mo   subsidy
lite            1,838     55,000    33.6M          58,976       0%
standard        2,941    120,000    43.8M          98,682      18%
pro             4,044    210,000    54.6M         138,388      34%

Coupon outflow = revenue × 30%   (10pp headroom under the 40% cap)
이 수치의 신뢰 구간

결과 전체가 R_cap(연 매출 상한 30억원)과 N_half(25,000대)라는 두 가정에 걸려 있다. 두 값 모두 아직 실측이 없는 추정이며, 각각 1.5배만 틀려도 권장 α와 목표 노드 수가 통째로 바뀐다.

따라서 이 세트는 Phase 2에서 첫 제조사 계약 2~3건이 체결된 직후 반드시 재산출해야 한다. 그때 확보되는 실측값은 (a) 계약 1건당 연 매출, (b) 계약 성사에 필요한 최소 커버리지 — 이 둘이 곧 R_cap과 N_half다. 재산출 전까지 α는 거버넌스로 조정 가능한 변수로 두고 코드에 하드코딩하지 마라.

재현

모든 수치는 아래 스크립트 실행 결과다. 파라미터 블록만 교체하면 재산출된다.

terminalbash

node sim/tokenomics.mjs           # summary report
node sim/tokenomics.mjs --json    # full 36-month time series as JSON

운영 잔여 리스크

구현 중 인지하고 관리해야 할 항목들.

항목리스크대응우선순위
기기 신원 NFT 보유만 검증하면 시드 탈취 시 위조 데이터 무제한 주입 페이로드 서명 검증 + 시계열 이상탐지 + 인근 노드 교차검증권장
오라클 신뢰 오프체인 API가 단일 신뢰점 — 데이터가 원장에 남지 않음 배치 단위 머클루트를 Memos에 기록해 사후 감사 가능성 확보권장
Reserve 고갈 TrustLine·Offer 누적으로 Hot wallet XRP 부족 → 전 트랜잭션 실패 잔액 임계 모니터링 + 미체결 Offer 정리 배치권장
Testnet 초기화 Testnet은 주기적으로 리셋되어 지갑·NFT·AMM이 소멸 부트스트랩 스크립트를 멱등하게 작성, 데모 전 재실행 절차 문서화인지
보상 인플레이션 보상이 제출 건수에 비례하면 노드 확산이 곧 발행량 증가 고정 예산 배분 + α=0.50 확정. 잔여 과제는 예산 조정 거버넌스설계 반영
과잉 노드 확산 보상 곡선이 단봉형이라 목표 초과 시 노드당 보상이 유지 하한 아래로 떨어져 대량 이탈 신규 라이선스 발급을 45,000대에서 중단. Fig 5 참조필수
R_cap · N_half 추정 오차 권장 파라미터 전체가 두 가정에 종속. 1.5배 오차면 α와 목표 노드 수가 통째로 바뀜 Phase 2 첫 계약 2~3건 직후 재산출. α는 하드코딩하지 말고 거버넌스 변수로 유지필수
실내 데이터 민감성 CO₂·PM 시계열에서 재실 인원·취침 시각·부재 기간이 추정됨 5단계 비식별화 + k-익명성 + 차분 공격 방어. 개인정보 법률 검토 선행 필수필수
쿠폰 정산 재원 보상은 토큰으로 무제한 발행 가능하나 쿠폰 정산은 유한한 RLUSD 매출에서 나감 기간 쿠폰 발행 총액을 같은 기간 B2B 매출의 일정 비율로 상한 설정필수
센서 캘리브레이션 드리프트 저가 MOX·광산란 센서는 수개월 단위로 편차가 누적되어 데이터 상품성이 하락 품질 점수의 driftDays 항목 + 인근 노드 상관 기반 자동 보정 계수권장
x402 요청 헤더 본 설계는 원 스펙의 Authorization: x402 <hash> 커스텀 방식 유지 표준 x402는 X-PAYMENT 헤더를 쓴다. 응답 바디는 이미 표준 스키마이므로, 헤더만 교체하면 정합 완료인지

운영 실행 체크리스트

각 Step은 승인 후 다음 단계로 진행한다. 완료 판정 기준을 함께 명시했다.

Step 1 — XRPL 기반 & 지갑

  • Testnet WebSocket 클라이언트 싱글턴 + 재연결 처리
    완료 기준: server_info 응답 수신
  • Issuer / Operational / Device / Agent 4개 지갑 Faucet 펀딩
  • .env + .env.example 구성, .gitignore 등록, zod 검증
  • 공통 submit() 래퍼로 tesSUCCESS + validated 강제

Step 2 — WLBN 토큰

  • currency hex 상수 확정 (WLBN / RLUSD)
  • Issuer AccountSetasfDefaultRipple 활성화
  • Operational·Device·Agent TrustSet — tfSetNoRipple 적용
  • Issuer → Operational 초기 발행
    완료 기준: account_lines에서 잔액 확인

Step 3 — 라이선스 NFT

  • IPFS 메타데이터 스키마 확정 및 업로드(또는 Mock URI)
  • mintLicenseofferLicenseToacceptLicense 전체 흐름
  • verifyDeviceLicense() — Issuer + Taxon 이중 확인
  • 미보유 주소가 valid:false로 판정되는 네거티브 테스트

Step 4 — 오라클 & 보상

  • POST /api/iaq/telemetry 구현 + 6단계 수신 검증
  • 기기 서명 검증 → 라이선스 조회 순서 준수 (싼 검사 우선)
  • 라이선스 검증 실패 시 403, 보상 미지급 확인
  • 절대습도·이슬점 유도 후 저장
    완료 기준: absoluteHumidity(20,60)≈10.35, (30,60)≈18.21
  • 에폭 배치 distribute() — 총 지급액이 EPOCH_BUDGET을 초과하지 않음
    완료 기준: 노드 수를 10배로 늘려도 총 발행량 불변
  • 배치 머클루트를 Memos에 기록, 머클 증명 검증
  • Hot wallet 직렬 제출 큐 적용
    완료 기준: 동시 10건 요청에서 Sequence 오류 0건

Step 5 — AMM

  • AMMCreate WLBN/RLUSD 풀 생성 (TradingFee 0.5%)
  • AMMDeposit 추가 유동성 공급 및 LPToken 수령 확인
  • path_find + SendMax 기반 RLUSD→WLBN 스왑
  • 스왑 전후 amm_info 풀 비율 변화 기록

Step 6 — x402 게이트웨이

  • 헤더 없는 요청 → 402 + accepts 배열 결제 조건
  • verifyPayment() — 수취인·발행자·통화·전달액·tesSUCCESS 검증
  • delivered_amount 기반 판정 (PartialPayment 방어)
  • tx_hash 재사용 차단 nonce 스토어
  • Mock 에이전트 E2E: 402 → 결제 → 재요청 → 200
    완료 기준: 스크립트 1회 실행으로 전 과정 무인 완료
  • 50% 소각 배치 잡 및 유통량 감소 검증

IAQ 사양 — 데이터 계층

  • IAQ 텔레메트리 스키마 v1 확정 + 기기 서명 규격
  • Taxon 분리 (IAQ 3026 / OAQ 2026), tier별 보상 배수 적용
  • qualityScore() 4개 항목 산출 — 특히 인근 노드 상관
  • 비식별화 P1~P5 파이프라인, 에폭 salt 회전 확인
  • k-익명성 롤업 — geohash3에서도 미달 시 응답 거부
    완료 기준: 노드 4개 격자 질의가 상위 격자로 롤업되거나 거부됨
  • 차분 공격 방어 4종 (반올림·필터 상한·질의 예산·최소 코호트)
  • 코호트 응답에 노드 목록이 포함되지 않음을 스키마 레벨에서 보장
  • 선불 크레딧 — 충전·서명 차감·nonce 단조 증가
    완료 기준: 동일 nonce 재전송이 BAD_NONCE로 거부됨
  • 쿠폰 발행 → WLBN 소각 → 제조사 RLUSD 정산 흐름
데모 시나리오 (7분)
  1. 미인가 주소로 IAQ 텔레메트리 전송 → 403 (라이선스 없음)
  2. NFT 발급 후 동일 주소 재전송 → 202 Accepted, 절대습도 유도값 확인
  3. 에폭 배치 실행 → 노드별 품질 점수와 지분 배분 출력, 총액 = 예산 확인
  4. 노드 수를 10배로 복제 후 재실행 → 총 발행량 불변, 개별 보상만 희석
  5. 제조사가 코호트 질의 → 402, 결제 후 재요청 → 200 + 크기만 반환
  6. 필터를 좁혀 재질의 → 422 COHORT_TOO_NARROW (실제 크기 미노출)
  7. 같은 tx_hash로 재호출 → 402 (HASH_ALREADY_USED)
  8. 쿠폰 발행 → Issuer TrustLine 상계로 유통량 감소 확인

운영 용어집

용어설명
DePINDecentralized Physical Infrastructure Network. 물리 인프라(센서·기지국 등)의 구축·운영을 토큰 인센티브로 분산화하는 모델.
XLS-20XRPL의 네이티브 NFT 표준. NFTokenMint 등 전용 트랜잭션 타입을 제공하며 스마트컨트랙트가 불필요하다.
XLS-30XRPL 네이티브 AMM 표준. 오더북과 AMM 유동성이 자동으로 합산 라우팅된다.
Issued Asset (IOU)발행자 계정에 대한 채권 형태의 토큰. 별도 mint 트랜잭션 없이 발행자가 Payment를 보내면 생성된다.
TrustLine보유자가 특정 발행자의 특정 통화를 얼마까지 받을지 선언하는 온체인 관계. 없으면 tecNO_LINE.
Rippling동일 통화의 TrustLine들을 연결해 잔액이 자동 이동하는 XRPL 고유 메커니즘. 발행자 경유 전송의 기반이자 오남용 위험 요소.
x402HTTP 402 Payment Required를 실사용하는 M2M 결제 프로토콜. 서버가 결제 조건을 제시하고 클라이언트가 온체인 결제 증빙을 붙여 재요청한다.
delivered_amount트랜잭션 메타데이터에 기록된 실제 전달 금액. PartialPayment 우회를 막기 위해 검증은 이 값으로만 수행한다.
Destination TagPayment에 붙이는 32비트 정수 식별자. 동일 수취 주소로 들어오는 결제의 용도·고객을 구분한다.
IAQIndoor Air Quality. 실내 공기질. 본 문서에서는 PM·CO₂·TVOC·온습도·기압 6종 지표의 총칭.
절대습도 (AH)단위 부피당 실제 수증기 질량(g/m³). 온도 종속인 상대습도와 달리 제습 부하를 직접 나타내므로, 제습 알고리즘·결로 예측의 입력값이 된다.
TVOC총휘발성유기화합물. MOX 센서는 상대 지수를 내므로 절대 농도로 해석하지 않고 추세·급변 탐지에 사용한다.
geohash위경도를 문자열로 인코딩하는 격자 체계. 자릿수가 짧을수록 넓은 영역을 뜻한다 (5자리 ≈ 4.9km, 3자리 ≈ 156km).
k-익명성어떤 속성 조합으로도 최소 k명 미만으로 좁혀지지 않도록 보장하는 성질. 본 사양은 k=5를 하한, 반올림 후 25를 실질 하한으로 둔다.
차분 공격필터를 조금씩 바꾼 반복 질의의 결과 차이로 개별 대상을 분리해내는 공격. 카운트 반올림·필터 개수 상한·질의 예산으로 방어한다.
에폭 (Epoch)보상 정산 주기(예: 24시간). 이 단위로 고정 예산을 품질 점수 지분에 따라 배분한다.
선불 크레딧온체인 결제 1건으로 잔액을 충전하고 이후 호출은 서명으로만 차감하는 방식. 고빈도 API에서 원장 확정 지연을 회피한다.
α (알파)B2B 매출 중 노드 보상 예산으로 배정하는 비율. 쿠폰 상한 ÷ 상환율로 천장이 결정된다(0.667). 권장값 0.50.
N_half매출이 상한의 절반에 도달하는 노드 수. 최대 보상이 여기에 반비례하므로 가장 강력한 레버다.
지속 가능 구간노드당 보상이 유지 하한 이상인 노드 수 범위. 보상 곡선이 단봉형이라 임계점이 아니라 구간으로 나타난다.
Blackhole 계정Regular Key를 무효 주소로 설정하고 마스터키를 비활성화해 영구히 서명 불가하게 만든 계정. 진짜 소각 보증에 사용.