실내외 날씨 데이터 생태계와
온체인 데이터 거래 인프라
전국 단위 IoT 날씨 측정망 — 실내(IAQ)와 실외(OAQ) 두 계층 — 으로 생활 환경 데이터를 수집해 가전 제조사의 R&D·마케팅과 환경 서비스에 공급하고, 기여자에게는 온체인 보상을 돌려주는 선순환 생태계. Part 1은 사업 백서, Part 2 이후는 이를 구현하는 XRPL DePIN · x402 기술 사양이다.
백서 1 개요
현대인은 하루의 90% 이상을 실내에서 생활하며, 실내 날씨과 기후 환경은 개인의 건강과 직결된다. 그러나 대부분의 실내 공조 가전(공기청정기, 제습기, 에어컨 등)은 출하 시 설정된 표준 알고리즘에 의존해 작동한다.
본 프로젝트는 전국 단위의 IoT 실내 날씨 측정망을 통해 사용자의 실제 거주 환경 데이터를 실시간으로 수집하고, 이를 가전 제조사에 제공하여 기술 발전과 초개인화 마케팅을 지원하는 '실내 기후 데이터 플랫폼 생태계'를 구축하는 것을 목적으로 한다.
Part 1(백서)은 생태계의 사업 구조를 기술한다. Part 2~3(기술 개요 · 날씨 데이터 사양)과 Step 1~6 · 운영 섹션은 이 구조를 XRPL 위에 구현하기 위한 데이터 스키마, 라이선스·보상 설계, 비식별화 파이프라인, API 과금 프로토콜, 그리고 단계별 구현 지침을 다룬다. 백서의 각 장은 대응하는 기술 섹션으로 연결된다.
백서 2 문제 제기 및 시장의 한계
백서 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) |
|---|---|---|---|
| 온·습도 (상대습도 · 절대습도 환산) | 제습기, 에어컨 | 주거 형태별 최적 제습 알고리즘 개발, 결로 방지 예측 모델링 구축 | 장마철 고습도 유지 가구 타겟팅 제습기 프로모션, 건조 가구 대상 가습기 교체 시기 알림 |
| 미세먼지·초미세먼지 (PM10 / PM2.5) | 공기청정기, 청소기 | 필터 수명 예측 모델 고도화, 오염도 급증 시 팬 속도 자동 제어 로직 설계 | 대기오염도와 실내 오염도가 비례하는 가구 추출, 대용량 프리미엄 공기청정기 타겟 광고 |
| 이산화탄소 (CO₂) | 환기 청정기 (전열교환기) | 밀폐 공간 내 CO2 농도 변화율 기반 AI 환기 시점 예측 기술 개발 | 환기 부족 다중이용시설 및 영유아 가구 대상 스마트 환기 시스템 도입 제안 |
| TVOC (총휘발성유기화합물) | 새집증후군 제거기 | 건축 자재별 배출 가스 패턴 분석, 센서 감도 최적화 | 신축 아파트 입주 및 인테리어 시공 가구 대상 특화 필터 마케팅 |
| 온도 / 체감온도 | 에어컨, 난방기기 | 체감 기반 목표온도 제어 로직 설계, 계절별 냉난방 부하 프로파일 구축 | 혹서·혹한 체감 구간이 지속되는 가구 대상 냉난방 기기 교체 제안 |
4.2 실외(OAQ) 지표 — 환경 서비스 · 외기 연동 제어
실외 지표는 수요자 구성이 다르다. 실내 지표가 가전 제조사 한 축이라면, 실외는 지자체·환경 서비스가 1차 수요자이고 가전 제조사는 "외기 조건에 반응하는 제어"라는 형태로 2차 수요자가 된다.
| 수집 지표 | 주 활용 주체 | 기술적 활용 방안 (R&D) | 서비스 활용 방안 |
|---|---|---|---|
| 미세먼지·초미세먼지 (PM10 / PM2.5) | 가전 제조사 · 지자체 | 격자별 침투율(I/O ratio) 산출, 외기 연동 선제 급기·차단 제어 알고리즘 개발 | 외기 악화 예보 기반 창문 닫기·공기청정기 예약 가동 알림 |
| 오존 (O₃) | 지자체 · 보건 | 광화학 생성 패턴 분석, 환기 금지 시간대 판정 모델 구축 | 오존주의보 구간 환기 억제 권고 — 환기형 청정기에 제어 신호 전달 |
| 이산화질소 (NO₂) | 지자체 · 교통 | 도로변 배출 기여도 분리, 가로변 노출 지도 작성 | 통학로·보행로 저노출 경로 안내 |
| 아황산가스 (SO₂) | 산업단지 · 환경 | 산업 배출원 영향 반경 추정, 풍향별 확산 모델 검증 | 인근 주거지 대상 배출 이벤트 알림 |
| 일산화탄소 (CO) | 지자체 · 안전 | 교통 정체 구간 축적 패턴 분석 | 밀집 구역 단기 노출 경보 |
| 풍속 · 풍향 | 전 주체 | 오염물질 이동·확산 방향 추정, 배출원 역추적 | 인접 격자로부터의 오염 유입 예측 |
4.1과 4.2에서 PM2.5만 양쪽에 등장한다. 같은 물질을 같은 광산란 방식으로 실내외에서 동시에 측정하기 때문이다. 나머지 지표는 한쪽에만 존재하므로 단독 상품이지만, PM2.5는 두 관측망을 곱해 침투율이라는 파생 지표를 만든다. 이 파생 지표가 Phase 3에서 가장 비싼 데이터가 되는 이유이며, 실외를 기상 관측으로 두었다면 존재하지 않았을 값이다.
위 표의 마케팅 항목은 모두 "특정 조건을 만족하는 가구를 추출"하는 형태다. 이는 개인을 직접 식별하지 않고 코호트 단위 카운트만 반환하는 API로 구현해야 하며, 실제 광고 도달은 플랫폼이 중개하고 제조사에는 대상자 목록을 넘기지 않는다. 구체적 설계는 비식별화 파이프라인과 코호트 API 절에서 다룬다.
백서 5 데이터 보상 메커니즘
광범위하고 유의미한 데이터를 지속적으로 확보하려면 사용자의 자발적인 IoT 디바이스 운영이 필수적이다.
기본 7개 × 0.78^(단계−1) × 품질 등급(0~1.2)으로 수량만 정의되며, 가치는 정의하지 않는다.제출 건수에만 비례해 보상하면 센서를 방치하거나 조작해 건수만 채우는 것이 최적 전략이 된다. 본 사양은 전송률 게이트와 가동률·이상치율·인근 노드 상관·캘리브레이션 경과일을 합산한 품질 점수(Quality Score)로 품질 등급(0 · 1.0 · 1.2)을 매기고, 에폭 예산을 고정한 뒤 등급별 정량으로 지급하며 미지급분은 재분배하거나 소각한다. 방출 총량은 단계 게이트와 10단계 누적 상한(26.65억개)으로 사전 확정된다.
WLBN과 라이선스 NFT는 회사 지분·배당·이익분배·원리금 상환을 청구할 권리를 표창하지 않는다. 보상은 검증된 데이터 기여의 품질에 따라 배분되는 유틸리티 토큰이며, 지급 수량·가치·환금성은 보장되지 않는다. 본 문서의 모든 수치는 설계·검증용 가정이며 어떤 경우에도 수익에 대한 예측이나 약속이 아니다. 기기 구매는 데이터 수집 장비의 구매이며 투자 계약이 아니다.
백서 6 비즈니스 수익 모델
세 수익원은 모두 x402 데이터 상품으로 매핑된다 — 구독료는 선불 크레딧, 리드 수수료는 코호트 API 건당 과금, 리포트는 LLM 추론 건당 과금이다. 과금 수단이 하나의 프로토콜로 통일되므로 별도 빌링 시스템 없이 정산이 가능하다. 여기에 수요자가 먼저 금액을 예치하고 데이터를 요청하는 역방향 채널로 데이터 바운티가 더해진다.
백서 6+ 데이터 바운티 — 생명을 연장하는 데이터
현대인은 하루의 약 90%를 실내에서 보내며, WHO는 실내 공기 오염에 따른 조기 사망을 연간 수백만 명 규모로 추산한다. 실내 날씨 측정은 편의 기능이 아니라 호흡기·심혈관 질환의 예방이 시작되는 지점이다. 노드 운영자가 매일 쌓는 1분 단위 데이터는 "측정되지 않아 개입되지 못했던" 생활 공간을 관리 가능한 공간으로 바꾸는 공중보건 인프라이며, 본 생태계의 보상은 단순한 채굴 개념이 아니라 그 기여에 대한 대가다. 데이터 바운티는 이 기여를 필요로 하는 곳과 데이터 제공자를 직접 연결하는 장치다.
백서 6의 세 수익원이 플랫폼이 만든 상품을 판매하는 순방향 흐름이라면, 데이터 바운티는 수요자가 금액을 걸고 데이터를 요청하는 역방향 흐름이며, 그 대가는 단순 API 호출권이 아니라 가입 스폰서에게만 귀속되는 기간 한정 데이터 사용권이다. 스폰서 예치금은 XRPL 메인넷 온체인에서 투명하게 확인되고, 바운티 기간 종료 시 참여 기여도에 따라 정산된다. 측정하는 사람은 더 벌고, 필요한 곳은 검증된 실측 데이터를 얻고, 그 위에서 청정 솔루션이 필요한 가정에 정확히 닿는다 — 데이터가 건강 개입으로 이어지는 완결 구조다. 바운티 보드는 실행 플랫폼 wellbian.io/data에서 운영된다.
백서 7 향후 발전 로드맵
NFTokenTaxon으로만 구분된다.실외 OAQ는 선행 지표, 실내 IAQ는 결과 지표다. 특히 PM2.5는 두 관측망이 같은 물질을 측정하므로, 같은 격자에서 두 시계열을 함께 확보하면 "외기 PM이 X일 때 이 주거 형태의 실내 PM이 Y로 변한다"는 침투율(I/O ratio) 전이 함수를 직접 학습할 수 있다. 이는 실외를 기상 관측으로 두었을 때는 얻을 수 없는 값이며, 제조사가 살 수 있는 가장 비싼 형태의 데이터가 된다. 본 기술 사양이 두 관측망을 하나의 원장·하나의 과금 체계 위에 올린 이유다.
기술 개요 플랫폼 개요
백서의 3주체 구조를 온체인으로 구현하면 두 개의 경제 루프가 하나의 원장 위에서 연결된다. 공급 측은 IAQ 노드가 실내 환경 데이터를 올리고 토큰 보상을 받는 DePIN 루프이고, 수요 측은 가전 제조사와 AI 에이전트가 데이터 API를 호출하며 토큰을 지불하는 x402 루프다. 두 루프는 동일 원장의 AMM 풀을 통해 유동성이 연결된다.
두 관측망, 하나의 인프라
실내 IAQ 관측망과 실외 OAQ 관측망은 센서 구성과 데이터 스키마만 다를 뿐,
라이선스 발급·보상 지급·데이터 과금 경로는 완전히 동일하다.
구현상으로는 NFTokenTaxon과 텔레메트리 스키마 버전으로만 구분되며,
지갑·토큰·AMM·x402 게이트웨이는 단일 인스턴스를 공유한다.
| 구분 | 실내 IAQ 관측망 | 실외 OAQ 관측망 |
|---|---|---|
| 측정 지표 | PM2.5/10, CO₂(NDIR), TVOC, 온·습도(체감온도 유도) | PM2.5/10, O₃, NO₂, SO₂, CO, 온·습도, 풍속·풍향 |
| 설치 주체 | 일반 가구 · 사무실 · 상업시설 | 건물 옥상 · 가로변 · 학교 · 산업단지 인근 |
NFTokenTaxon | 3026 | 2026 |
| 주 수요자 | 가전 제조사 · 스마트홈 플랫폼 | 지자체 · 환경 서비스 · AI 에이전트 |
| 공유 인프라 | Issuer/Hot 지갑 · WLBN·RLUSD · AMM 풀 · x402 게이트웨이 · 소각 배치 | 동일 (단일 인스턴스 공유) |
메인넷에서 검증된 5가지 명제
| # | 명제 | 검증 수단 | XRPL 기능 |
|---|---|---|---|
| 1 | 기기 신원을 온체인으로 위조 불가능하게 증명할 수 있다 | verifyDeviceLicense() 온체인 조회 | XLS-20 NFT |
| 2 | 데이터 기여에 대한 마이크로 보상을 무인 자동화할 수 있다 | 에폭 정산 → 배치 Payment (24h 클레임 홀드) | Issued Asset (IOU) |
| 3 | 토큰과 스테이블코인 간 스왑이 원장 내에서 가능하다 | Mainnet AMM 풀에서 RLUSD ↔ WLBN 실거래 스왑 | XLS-30 AMM |
| 4 | AI 에이전트가 사람 개입 없이 API 사용료를 결제할 수 있다 | 402 응답 → 결제 → 재요청 → 200 | Payment + tx 조회 |
| 5 | 개인을 식별하지 않고도 제조사가 쓸 만한 코호트를 추출할 수 있다 | k-익명성 미달 시 응답 거부, 카운트만 반환 | 오프체인 (온체인 미기록) |
마이크로페이먼트가 성립하려면 수수료가 결제 금액보다 압도적으로 작아야 한다. XRPL의 기본 트랜잭션 비용은 10 drops(0.00001 XRP)이고 확정(finality)까지 3~5초다. 0.002 RLUSD 단위의 집계 API 과금과, 수만 개 노드에 대한 에폭 배치 보상 지급이 수수료에 잡아먹히지 않고 성립하는 이유이며, NFT·IOU·AMM이 스마트컨트랙트 없이 네이티브 트랜잭션 타입으로 제공되어 감사 대상 코드 표면이 작아 운영 리스크가 낮다.
기술 개요 시스템 아키텍처
오프체인 3계층(기기 · 게이트웨이 · 소비자)과 온체인 1계층(XRPL 원장)으로 구성된다.
지갑 역할 분리
| 지갑 | 역할 | 키 보관 | 주요 트랜잭션 |
|---|---|---|---|
Issuer (Cold)rDJz8WJhsKgydqJzSZXMpsJot3eRmSkR5 | Wellbian(WLBN) 발행 주체, NFT 발행자, 소각 수취처. RLUSD는 Ripple 발행(rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De)을 TrustLine으로 수용. | 오프라인 / HSM 가정 | AccountSet, NFTokenMint, 최초 Payment |
Hot Wallet(운영 지갑)rhTTtx8YyKzcmPDsoroYXuHdGMThrnweb1 | 에폭 보상 지급, x402 결제 수취, 소각 배치 실행 | 서버 환경변수 (KMS 권장) | Payment, AMMDeposit |
| IoT Device (ARC-600DA) | 기기 지갑, 라이선스 NFT 보유자 | 기기 시큐어 엘리먼트 가정 | TrustSet, NFTokenAcceptOffer |
| AI Agent · 제조사 클라이언트 | x402 클라이언트, API 사용료 지불자 | 에이전트 로컬 | TrustSet, Payment |
Issuer 키가 유출되면 무제한 발행이 가능하므로, 런타임 서버는 절대 Issuer 시드를 로드하지 않는다. 발행은 1회성 부트스트랩 스크립트에서만 수행하고, 이후 모든 상시 트랜잭션은 Hot Wallet(운영 지갑)이 담당한다. 이 경계는 코드 레벨(모듈 분리 + 별도 키 주입)로 강제되며, 운영 서버는 Issuer 시드를 로드하지 않는다.
기술 개요 기술 스택 & 네트워크
Client, Wallet, autofill/sign/submitAndWait 사용.네트워크 파라미터
| 항목 | 값 | 비고 |
|---|---|---|
| WebSocket | wss://xrplcluster.com | Mainnet 공용 클러스터 노드 |
| JSON-RPC | https://xrplcluster.com | 단발 조회용 |
| 계정 활성화 | Payment (외부 입금) | 계정 활성화 · Base Reserve 이상 XRP 사전 입금(Faucet 없음) |
| Explorer | livenet.xrpl.org | tx / account 시각 검증 |
| Base Reserve | 1 XRP | Mainnet 기준 계정 유지 최소 잔액 |
| Owner Reserve | 0.2 XRP / 오브젝트 | TrustLine · NFT Offer · AMM 등 각각 차감 |
| Finality | 3~5초 | 비동기 대기 및 폴링 설계 필수 |
통화 코드 인코딩
XRPL의 currency 필드는 3자리 ASCII이거나 40자리 hex(160-bit)여야 한다.
WLBN·RLUSD 모두 3자를 초과하므로 hex 인코딩이 필수다. 이 값을 상수로 고정해두지 않으면
TrustSet과 Payment의 통화가 불일치해 tecNO_LINE / tecPATH_DRY가 발생한다.
// 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
환경 변수
XRPL_ENDPOINT=wss://xrplcluster.com
XRPL_NETWORK=mainnet
# Cold — bootstrap script only. Never inject into the runtime server
ISSUER_SEED=sEd..............................
# Hot — API server runtime
HOT_SEED=sEd..............................
HOT_WALLET_ADDRESS=r..............................
# Counterparties
TREASURY_ADDRESS=rPZFY2yLgqjmBrxtxWHXnB1doBt7gu7xin
RLUSD_ISSUER_ADDRESS=rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De
# Pricing policy (RLUSD)
X402_PRICE_AGGREGATE_RLUSD=0.002
X402_PRICE_COHORT_RLUSD=0.05
X402_PRICE_REPORT_RLUSD=0.5
X402_DEST_TAG=402001
# Reward policy
REWARD_BASE_DAILY_WLBN=7
PHASE_DECAY=0.78
EXCELLENT_BONUS_WLBN=1.4
NODE_DAILY_CAP_WLBN=10.5
BUDGET_PER_NODE_WLBN=7.14
CLAIM_HOLD_HOURS=24
BURN_RATIO=0.5
.env는 반드시 .gitignore에 등록한다. 운영(Mainnet) 시드를 커밋하면 저장소가 노출되는 순간 서명 권한이 유출된다. 운영 전환 시에는 .env 대신
KMS/Vault에서 런타임 주입하고, 서명은 Hot Wallet 키에 한정한다.
기술 개요 모듈 구조
각 XRPL 기능을 독립 모듈로 분리해 단계별 테스트와 교체가 가능하도록 한다.
공통 제출 헬퍼
모든 트랜잭션이 tesSUCCESS인지, 그리고 validated: true인지 한 곳에서 검증한다.
이 래퍼를 강제하지 않으면 "제출은 됐지만 원장에 반영되지 않은" 상태를 성공으로 오인하게 된다.
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) 구조를 공유하며, 각각 버전 필드를 갖는다.
측정 지표 사양
| 지표 | 필드 | 센서 방식 | 측정 범위 | 목표 정확도 |
|---|---|---|---|---|
| 미세먼지 | pm25 pm10 |
광산란 (PMS) | 0 ~ 1000 µg/m³ | ±10% (≥100 구간) |
| 이산화탄소 | co2 |
NDIR | 400 ~ 5000 ppm | ±(50 ppm + 3%) |
| 총휘발성유기화합물 | tvoc |
MOX 반도체식 | 0 ~ 60000 ppb | 상대 지수 (절대값 비보증) |
| 온도 | tempC |
디지털 온습도 복합 | −10 ~ 60 °C | ±0.3 °C |
| 상대습도 | rh |
정전용량식 | 0 ~ 100 %RH | ±2 %RH |
| 체감온도 유도 | feelsLikeC |
온도 + 상대습도에서 계산 | −10 ~ 60 °C | 입력 오차 전파 |
| 절대습도 유도 | 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 ppb | oaq-standard |
| 이산화질소 | no2 | 전기화학식 | 0 ~ 500 ppb | oaq-standard |
| 온도 · 상대습도 | tempC rh | 디지털 복합 | −30 ~ 60 °C / 0 ~ 100 %RH | oaq-standard |
| 아황산가스 | so2 | 전기화학식 | 0 ~ 200 ppb | oaq-pro |
| 일산화탄소 | co | 전기화학식 | 0 ~ 50 ppm | oaq-pro |
| 풍속 · 풍향 | wind windDir | 초음파식 | 0 ~ 60 m/s / 0 ~ 360° | oaq-pro |
전기화학식 가스 센서(O₃·NO₂·SO₂·CO)는 온도·습도에 강하게 교차 반응하고 수명이 1~2년으로 짧다. 실내 노드보다 드리프트가 빠르므로 품질 점수의 보정(calibration, 15점)·상관(correlation, 20점) 가중을 실외에 더 크게 적용하고, 인근 국가 측정망(에어코리아) 값과의 상관을 보정 계수 산출에 함께 쓴다.
실내 1단계 기본 리워드는 일 7개(정상 등급)이며, 실외 라인은 기존 홀더에게 우선 제안된다. 실외 리워드 수량은 실외 모델 출시 시 공표한다.
절대습도는 왜 유도해야 하는가
백서 4장이 첫 번째 지표로 절대습도를 명시한 것은 정확한 선택이다. 상대습도는 온도 종속량이라 제습 부하 계산에 직접 쓸 수 없다. 같은 60 %RH라도 20 °C에서는 약 10.3 g/m³, 30 °C에서는 약 18.2 g/m³로 실제 수분량이 1.8배 차이난다. 제습기가 뽑아내야 할 물의 양은 후자가 압도적으로 많다. 따라서 노드는 상대습도를 전송하되, 서버는 수신 즉시 절대습도를 유도해 저장한다.
/**
* 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분 주기로 측정·전송하며, 서버는 시간 단위 집계(TelemetryHourly)를 생성하고 원시 데이터는 7일 보관한다.
전송 단위마다 기기 키로 서명하며, 서버는 서명자 주소와 deviceAddress가 일치하는지 확인한 뒤
라이선스 NFT를 조회한다. 서명이 없으면 NFT를 보유한 주소를 사칭한 임의 페이로드를 막을 수 없다.
{
"schema": "wellbian-iaq-telemetry/v1",
"deviceAddress": "rDeviceWalletAddress...",
"signature": "3045022100...",
"batch": [
{
"ts": 1786195200,
"pm25": 11.4, "pm10": 15.8,
"co2": 812, "tvoc": 143,
"tempC": 23.4, "rh": 58.1, "feelsLikeC": 24.1
},
{
"ts": 1786195260,
"pm25": 11.1, "pm10": 15.2,
"co2": 845, "tvoc": 139,
"tempC": 23.4, "rh": 58.4, "feelsLikeC": 24.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장의 "양질의 데이터"를 정량화하고, 리워드를 수량으로만 정의해 방출 총량이 단계 게이트와 10단계 누적 상한(26.65억개)으로 사전 확정되도록 한다.
Taxon 및 티어 체계
라이선스 NFT는 NFTokenTaxon으로 관측망을 구분하고, 메타데이터의 license.tier로 센서 구성 등급을 표현한다. 티어는 센서 구성과 접근 가능한 데이터 상품을 구분하며, 리워드 수량은 티어가 아니라 단계와 품질 등급으로 정해진다.
| Taxon | 관측망 | Tier | 필수 센서 | 리워드 |
|---|---|---|---|---|
3026 | 실내 IAQ | iaq-lite | PM2.5/10, 온·습도 | 미정 (출시 시 확정) |
3026 | 실내 IAQ | iaq-standard | PM2.5, PM10, CO₂(NDIR), TVOC, 온·습도 (ARC-600DA) | 1단계 기본 일 7개 (정상 등급 · 지급 보장 아님) |
3026 | 실내 IAQ | iaq-pro | 추가 센서 미정 (출시 시 확정) | 동일 규칙 (단계 × 품질 등급) |
2026 | 실외 OAQ | oaq-standard | PM2.5/10, O₃, NO₂, 온·습도 | 미정 (실외 출시 시 확정) |
2026 | 실외 OAQ | oaq-pro | + SO₂, CO, 풍속·풍향 | 미정 (실외 출시 시 확정) |
디바이스 메타데이터 (IPFS)
주거 환경 속성은 백서 2장이 지적한 "평형·위치·채광에 따른 공조 효율" 분석에 필수적이다. 다만 이 속성들의 조합은 그 자체로 가구를 특정할 수 있으므로, 메타데이터 단계에서부터 연속값이 아닌 버킷으로만 기록한다.
{
"schema": "wellbian-iaq-device/v1",
"name": "Wellbian IAQ Node #10427",
"description": "Indoor Air Quality license",
"device": {
"serial": "KW-IAQ-10427",
"model": "ARC-600DA",
"sensors": ["pm25", "pm10", "co2", "tvoc", "temp", "rh"],
"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-standard", "valid_until": "2029-05-02" }
}
NFT의 URI는 온체인에 기록되고 IPFS 콘텐츠는 누구나 조회할 수 있다.
따라서 정확한 주소·동호수·연속 좌표를 여기에 넣으면 영구히 공개된다.
geohash5(약 4.9 km 격자)와 버킷 값만 기록하고,
정밀 주소는 수집·저장하지 않는다. 위치는 동(洞) 단위로만 보관하며 좌표는 반올림해 저장한다.
품질 점수 (Quality Score)
백서 5장의 "24시간 끊김 없이 양질의 데이터"를 4개 관측 가능한 지표로 분해한다. 특히 인근 노드 상관계수는 센서를 실외에 방치하거나 값을 합성하는 조작을 잡아내는 핵심 항목이다 — 같은 지역의 다른 노드와 온도·PM 추세가 전혀 맞지 않으면 그 노드의 데이터는 신뢰할 수 없다.
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)
calibrationAgeDays: 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);
if (uptime < 0.2) return 0; // 가동률 20% 미만이면 점수 0
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.calibrationAgeDays / 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대당 금액을 지급하면 시세에 따라 방출이 요동친다. 대신 수량을 고정한다 — 에폭 예산은 활성 지점 수 × 7.14 × 단계 계수로 정해지고, 각 노드는 품질 등급에 따라 정상 7 · 우수 8.4 · 미달 0을 1차 지급받는다. 미지급분은 가중치 비례로 재분배하되 지점당 10.5개를 상한으로 하고 초과분은 소각한다.
import { qualityScore, QualityInputs } from './quality';
export const BASE_DAILY = 7; // 정상 등급 1차 지급 (수량 · 시세 무관)
export const EXCELLENT_DAILY = 8.4; // 품질 상위 10% (+1.4)
export const NODE_CAP = 10.5; // 재분배 후 지점당 상한 — 초과분 소각
export const BUDGET_PER_NODE = 7.14; // 에폭 예산 = 활성 지점 × 7.14 × 단계 계수
export const PHASE_DECAY = 0.78; // 단계마다 22% 체감
export type Grade = 0 | 1 | 1.2;
export interface NodeEpochStats { address: string; quality: QualityInputs; txRate: number; licenseHeld: boolean; }
/** 전송률 게이트 → 품질 점수 상위 10% 우수. 게이트 미달은 0 */
export function grade(n: NodeEpochStats, top10Cut: number): Grade {
if (n.txRate < 0.8) return 0;
return qualityScore(n.quality) >= top10Cut ? 1.2 : 1;
}
/** 에폭 정산: 총 지급 + 소각 = 예산 (항상 성립) */
export function distribute(nodes: NodeEpochStats[], phase: number) {
const factor = Math.pow(PHASE_DECAY, phase - 1);
const active = nodes.filter((n) => n.licenseHeld);
const budget = active.length * BUDGET_PER_NODE * factor;
const cut = percentile(active.map((n) => qualityScore(n.quality)), 0.9);
// 1) 1차 지급 — 정상 7 · 우수 8.4 · 미달 0 (80~95% 전송률은 선형 감액)
const paid = active.map((n) => {
const g = grade(n, cut);
const ramp = n.txRate >= 0.95 ? 1 : n.txRate < 0.8 ? 0 : (n.txRate - 0.8) / 0.15;
return { address: n.address, g, amount: BASE_DAILY * g * ramp * factor };
});
const unpaid = budget - paid.reduce((s, p) => s + p.amount, 0);
// 2) 미지급분 재분배 — 가중치 비례, 지점당 상한 10.5 × factor, 초과분 소각
const w = paid.reduce((s, p) => s + p.g, 0);
let burn = 0;
for (const p of paid) {
if (p.g === 0 || w === 0) continue;
const target = Math.min(p.amount + unpaid * (p.g / w), NODE_CAP * factor);
burn += p.amount + unpaid * (p.g / w) - target;
p.amount = target;
}
return { paid, burn, budget }; // Σ paid.amount + burn === budget
}
이 설계로 잔여 리스크 표의 보상 인플레이션 항목이 구조적으로 닫힌다. 10단계 누적 방출은 26.65억개로 사전 확정되고(리워드 1기 버킷 28억 이내), 단계 체감 22%로 지점이 늘수록 지점당 수량이 줄어든다. 품질 미달분은 지급하지 않고 소각하므로 품질이 나쁠수록 총 방출이 준다. 단계 진입에는 수요 조건이 병기되어 수요 없이 방출만 늘어나는 경로가 없다.
고정 예산 배분은 에폭 종료 시점에 전체 노드의 점수를 알아야 계산할 수 있다.
따라서 Step 4의 요청당 즉시 지급 방식은 IAQ 관측망에서는 사용하지 않는다.
텔레메트리 수신은 202 Accepted로 즉시 응답하고,
보상은 에폭(1일, KST 자정 마감) 배치로 distribute() 결과를 적립하고, 적립분은 24시간 홀드 후 클레임할 수 있다.
이는 Sequence 경합 대응과도 자연스럽게 맞물린다.
날씨 데이터 사양 비식별화 파이프라인
백서 3.2의 "개인정보를 비식별화 처리한 후 메타데이터로 가공"을 5단계 변환으로 구체화한다. 실내 데이터는 재실 여부·생활 패턴이 그대로 드러나는 민감 정보이므로, 실외 날씨 데이터보다 훨씬 강한 처리가 필요하다.
CO₂ 농도 시계열 하나만으로 재실 인원수, 취침·기상 시각, 부재 기간이 상당히 정확하게 추정된다. PM2.5 급등은 조리 시각을, TVOC 패턴은 청소·인테리어 시공을 드러낸다. 이 데이터가 정확한 위치와 결합되면 사실상 특정 가구의 생활 로그가 된다. 따라서 정밀 위치·원시 시간해상도·개별 노드 식별자가 동시에 외부로 나가는 경로는 존재해서는 안 된다.
5단계 변환
| # | 단계 | 원시 | 공개 |
|---|---|---|---|
| P1 | 공간 일반화 | 동(洞) 단위 위치 · 반올림 좌표 | geohash5 — 약 4.9 km 격자 |
| P2 | 시간 집계 | 1분 해상도 측정값 | 1시간 단위 평균·최대·분산 |
| P3 | 식별자 치환 | rDeviceAddress… | HMAC(NFTokenID, epochSalt) — 에폭마다 회전 |
| P4 | k-익명성 게이트 | — | 버킷 내 노드 < 5이면 상위 격자로 롤업, 그래도 미달이면 응답 거부 |
| P5 | 속성 일반화 | 평형 84.3 m², 7층 | 60-85sqm, 5-10F |
P3의 에폭마다 회전하는 salt가 중요하다. 고정 salt로 해시하면 가명 ID가 영구 식별자가 되어 장기 추적이 가능해진다. 에폭마다 salt를 교체하면 에폭 간 연결이 끊긴다. 다만 이 경우 장기 시계열 분석이 불가능해지므로, 장기 분석은 가명 ID가 아닌 버킷 단위 집계로만 제공한다.
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시간 집계 | 전체 | 구독 | 제한 공개 (지연 24h) |
| 코호트 카운트 | — | 건당 과금 | 불가 |
| 대상자 연락처 · 노드 목록 | — | 불가 | 불가 |
백서 6장의 "타겟팅 광고 수수료" 모델에서 제조사에 넘어가는 것은 코호트 크기와 광고 소재뿐이다. 대상자 목록은 절대 이전되지 않는다. 제조사는 "조건 X를 만족하는 가구 1,240곳에 이 소재를 노출" 요청을 넣고, 플랫폼이 자체 앱·알림 채널로 도달시킨 뒤 리드 수만 정산한다. 이 경계가 무너지면 생태계 전체가 개인정보 처리 리스크에 직접 노출된다.
본 절은 기술적 설계이며 법률 자문이 아니다. 국내 서비스 시 실내 환경 데이터의 개인정보 해당 여부, 가명정보 처리 요건, 수집·이용 동의 문구, 제3자 제공과 위탁의 구분, 그리고 보상 지급이 대가성 있는 데이터 판매로 해석될 여지에 대해 착수 전 개인정보 전문 법률 검토가 반드시 선행되어야 한다.
날씨 데이터 사양 B2B 데이터 상품 · 과금 설계
백서 6장의 세 수익원을 x402 프로토콜 위의 세 가지 상품으로 매핑한다. 과금 수단이 하나로 통일되므로 별도 빌링 시스템 없이 온체인 정산이 성립한다.
| 수익원 (백서 6장) | 엔드포인트 | 응답 | 과금 | 참고 단가 |
|---|---|---|---|---|
| API 구독료 | GET https://wellbian.io/api/data/aggregates |
격자·시간대별 지표 집계 | 선불 크레딧 차감 | 0.002 RLUSD / call |
| 타겟팅 광고 수수료 | POST https://wellbian.io/api/data/cohort (planned) |
조건 만족 코호트 크기만 | 건당 결제 | 0.05 RLUSD / query |
| 인사이트 리포트 | POST https://wellbian.io/api/data/report (planned) |
LLM 분석 리포트 | 건당 결제 | 0.5 RLUSD |
| (R&D 데이터셋) | POST https://wellbian.io/api/data/dataset (planned) |
비식별 학습셋 스냅샷 URL | 건당 결제 (고액) | 협의 · 계약 기반 |
코호트 API — 개인을 노출하지 않는 타겟팅
백서 4장의 마케팅 활용은 전부 이 엔드포인트 하나로 수렴한다. 핵심은 응답에 노드 목록이 없다는 것이다. 크기와 대표 통계만 반환한다.
{
"region": "wydm",
"window": { "from": "2026-08-01", "to": "2026-08-31" },
"filters": [
{ "metric": "ah", "op": "gte", "value": 14.0, "duty": 0.6 },
{ "metric": "area_bucket", "op": "in", "value": ["60-85sqm", "85-135sqm"] }
]
}
{
"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."
}
{
"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을 동시에 제시하고, 구독 고객은 후자를 택하게 한다.
{
"x402Version": 1,
"resource": "/api/data/aggregates",
"accepts": [
{
"scheme": "xrpl-payment",
"network": "xrpl-mainnet",
"payTo": "rhTTtx8YyKzcmPDsoroYXuHdGMThrnweb1",
"asset": { "currency": "RLUSD", "issuer": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De", "value": "0.002" },
"destinationTag": 402010,
"maxTimeoutSeconds": 120
},
{
"scheme": "xrpl-credit",
"network": "xrpl-mainnet",
"payTo": "rhTTtx8YyKzcmPDsoroYXuHdGMThrnweb1",
"topUpMinimum": { "currency": "RLUSD", "issuer": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De", "value": "100" },
"destinationTag": 402011,
"unitPrice": "0.002",
"authScheme": "x402-credit .."
}
]
}
크레딧 충전은 온체인 Payment 1건으로 이뤄지고, 이후 호출은 서명으로만 인증된다. 서명 대상에 nonce를 포함시켜 재전송을 막고, nonce는 단조 증가를 강제한다.
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 };
}
xrpl-payment는 저빈도·고단가 상품(코호트 질의, 인사이트 리포트)에,
xrpl-credit은 고빈도·저단가 상품(집계 API)에 쓴다.
크레딧 잔액이 소진되면 서버가 다시 402를 반환하므로, 구매자 에이전트 입장에서는
충전과 사용이 동일한 프로토콜 흐름 안에서 처리된다.
리워드 소비와 결제 구조
백서 5장의 "생태계 소비"(가전 구매 할인·필터 구독 결제·제휴 마켓)는 제휴 마켓 개설 시 별도 사양으로 공표한다. 결제 구조는 공표 정책을 따른다 — 기업 고객은 원화·RLUSD, AI 에이전트는 자동 결제(x402), 프로토콜 내부는 매출 일부로 WLBN을 매입해 소각한다. 고객에게 토큰을 사서 결제하라고 요구하지 않으며, 배분 비율은 사전 공표한다.
가치 흐름 요약
[IN ] Enterprise → Hot RLUSD / KRW (aggregate credit · cohort · report · dataset)
[IN ] AI agent → Hot RLUSD / WLBN (per-call x402 payment)
[OUT] Hot → points WLBN 7 × 0.78^(phase−1) × grade ← quantity, never value
[BURN] Hot → Issuer WLBN quality shortfall (redistribution excess)
[BURN] Treasury → Issuer WLBN scheduled 2.5B in 4 tranches · buyback from revenue (2.465B)
[LP ] Treasury → AMM WLBN/RLUSD 20% of participation fees on sale
STEP 01 XRPL 기반 & 지갑 셋업
Mainnet(wss://xrplcluster.com) 클라이언트 모듈과 3개 지갑(Issuer 콜드 · Hot · Treasury)을 준비하고, 각 계정에 Reserve용 XRP를 사전 예치한다.
산출물
xrpl/client.ts— 재사용 가능한 연결 싱글턴xrpl/wallet.ts— 시드 기반 로드 + 신규 생성/펀딩scripts/01-bootstrap-wallets.ts— 실행 시.env에 붙여넣을 값 출력
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;
}
import { Wallet } from 'xrpl';
import { getClient, closeClient } from '../xrpl/client';
import { env } from '../config/env';
const ROLES = ['ISSUER', 'HOT', 'TREASURY'] as const;
const MIN_RESERVE_XRP = 5;
// ISSUER(cold) rDJz8WJhsKgydqJzSZXMpsJot3eRmSkR5
// HOT rhTTtx8YyKzcmPDsoroYXuHdGMThrnweb1
// TREASURY rPZFY2yLgqjmBrxtxWHXnB1doBt7gu7xin
async function main() {
const client = await getClient();
for (const role of ROLES) {
// Mainnet: 시드로 로드하고 잔액을 확인한다 (Faucet 없음)
const wallet = Wallet.fromSeed(env[`${role}_SEED`]);
const info = await client.request({
command: 'account_info', account: wallet.classicAddress, ledger_index: 'validated',
});
const balance = Number(info.result.account_data.Balance) / 1_000_000;
if (balance < MIN_RESERVE_XRP) throw new Error(`${role} underfunded: ${balance} XRP`);
console.log(`\n# ${role}`);
// 시드는 출력하지 않는다 — .env에서만 관리한다
console.log(`${role}_ADDRESS=${wallet.classicAddress} # balance ${balance} XRP`);
}
await closeClient();
}
main().catch((e) => { console.error(e); process.exit(1); });
각 계정은 Base Reserve 1 XRP를 묶어두고, TrustLine·NFT Offer·AMM 오브젝트마다 0.2 XRP씩 추가로 잠긴다. Hot 지갑은 WLBN + RLUSD TrustLine 2개 = 0.4 XRP가 추가로 필요하므로, 각 계정에 최소 5 XRP 이상을 사전 예치하고 대량 기기 등록 시에는 사전 잔액 점검 로직을 넣는다.
STEP 02 Wellbian 토큰(WLBN) 발행 (Issued Asset)
Issuer(콜드)가 Wellbian 토큰(WLBN)을 발행하고, 보유 지갑들이 WLBN 및 실제 RLUSD TrustLine을 설정한다.
발행 절차
- Issuer 계정 설정 —
AccountSet으로asfDefaultRipple활성화,TransferRate(선택) 설정 - 보유자 TrustLine — Hot·Treasury·사용자 지갑이
TrustSet으로 한도 설정 - 초기 발행 — Issuer → Treasury/Hot으로
Payment를 보내면 그 금액만큼 토큰이 존재하게 됨 (IOU는 별도 mint 트랜잭션이 없음)
XRPL에서 IOU는 항상 발행자 계정을 경유해 이동한다. 따라서 발행자 계정은
asfDefaultRipple을 ON으로 설정해야 한다.
이 플래그가 꺼져 있으면 보유자 간 전송(Hot → 사용자 지갑)이 tecPATH_DRY로 실패하고,
Step 4의 보상 지급과 Step 5의 AMM이 모두 성립하지 않는다.
의도치 않은 유동성 전이 방지는 보유자 측 TrustLine에 tfSetNoRipple을 적용해 달성한다.
이 조합은 "발행자를 경유한 정상 전송"은 허용하고 "보유자 계정을 브릿지로 삼는 제3자 경로"는 차단한다.
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('wellbian.io').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) | 역할 | 초기 공급 |
|---|---|---|---|
| WLBN | 574C424E0000… | DePIN 보상 · API 사용료 결제 수단 | 10,000,000,000 (총 발행) |
| RLUSD | 524C5553440000… | 스테이블 페어 · 기관 결제 유입 통화 (Issuer rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De) | 외부 발행 (Ripple) — 자체 발행 없음 |
공급 총 발행량은 100억 WLBN이다. XRPL IOU에는 총량 필드가 없으므로,
공급량은 Issuer가 내보낸 채무(gateway_balances의 obligations)의 합으로 정의된다.
2026-09-01 기준 배분은 트레저리 99억 9,995만, 핫월렛(보상 재원) 약 5만, 유통 약 5이다.
방출 스케줄 — 단계 체감
총 발행은 100억이지만 한 번에 유통시키지 않는다. 방출 기준치는 실내 1단계 지점당 일 7 WLBN · 월 213 · 연 2,555(정상 등급)이며, 단계가 오를 때마다 22% 체감한다(7 × 0.78^(단계−1)). 이는 계획값이며 지급을 약속하는 수치가 아니다. 실제 수령량은 품질 등급과 재분배 결과에 따라 달라진다.
| 단계 | 일 기본 | 1단계 대비 | 단계 방출량 |
|---|---|---|---|
| 1 국내 실내 (25,000 지점) | 7.00개 | 100% | 0.64억 |
| 3 | 4.26개 | 61% | 1.87억 |
| 5 | 2.59개 | 37% | 3.31억 |
| 7 | 1.58개 | 23% | 3.74억 |
| 10 | 0.75개 | 11% | 2.73억 |
| 10단계 누적 | 26.65억 (리워드 1기 28억 이내) |
목표 규모 1단계는 국내 실내 25,000 지점(제네시스 한정 모집)이다. 이후 실내 → 실외로 넓히며 기존 홀더에게 실외 라인을 우선 제안하고, 최종 목표는 실내를 포함한 통합 관측망 100만 지점이다. 단계 진입은 지점 수와 수요 조건을 함께 만족하는 마일스톤 게이트로만 열린다.
배분 총 발행 100억 중 노드 홀더 리워드는 1기 28억(28%)과 10단계 게이트 통과 시에만 열리는 2기 18억(18%)이다. 소각 예정 25억, LP 리워드 8억, 팀 6억, 생태계 5억, 전략 투자 5억, 유동성·준비금 5억 — 전체 표는 토크노믹스 정책 절에 있다.
체감 시간 기반 반감기는 쓰지 않는다. 해마다 반으로 줄면 계획 방출 기준이 흔들린다. 대신 지점 수 게이트로 정의된 단계마다 22% 체감하며, 브레이크는 시간이 아니라 게이트(수요 조건)와 10단계 누적 상한이다.
상한 지점당 1차 지급은 등급별 정량(정상 7 · 우수 8.4 · 미달 0)이고, 미지급분 재분배 후에도 지점당 10.5개를 넘지 않는다. 초과분은 지급하지 않고 소각하므로 어떤 경우에도 총 지급 + 소각 = 에폭 예산이다.
참고 실제 RLUSD는 Ripple이 발행하는 스테이블코인이다. 서비스 대금(x402)은 이 RLUSD로만 수취하며, Issuer 주소는 rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De로 상수에 고정한다. 자체 발행 모의 RLUSD는 사용하지 않는다.
STEP 03 IoT 라이선스 NFT (XLS-20)
기기 1대 = NFT 1개. 온체인에서 라이선스 보유 여부를 조회해 데이터 제출 권한을 판정한다.
리딤 4단계 — ① 사전 구매·프로모션 시 코드가 미결합(UNBOUND) 상태로 발급된다 (WLBN-XXXX-XXXX-XXXX). ② 사용자가 wellbian.io/redeem에서 코드와 기기 시리얼을 입력하면 코드가 해당 기기에 결합된다. ③ Hot 지갑이 NFT를 민팅하고(Issuer는 콜드 지갑) NFTokenCreateOffer(Amount=0)를 사용자 주소로 생성한다. ④ 사용자가 NFTokenAcceptOffer로 수령을 확정한다. 지원 지갑은 간편지갑(아이디·비밀번호 또는 구글 로그인), D'CENT 모바일 인앱, Xaman, GemWallet, Crossmark이다.
발행 → 전송 흐름
import { Client, Wallet, NFTokenMintFlags, NFTokenCreateOfferFlags, convertStringToHex } from 'xrpl';
import { submit } from './submit';
export const TAXON = {
OBS_GENESIS: 1001, OBS_REGION: 1002, OBS_BIZ: 1003, // 관측소 NFT (계정당 1개)
VOUCHER: 1010, // 사전예약 바우처 (테스트 900010)
OAQ: 2026, IAQ: 3026, AI: 4026, RADAR: 5026, SAT: 6026, // 스테이션 NFT (기기당 1개)
} as const;
/** Mint a license NFT → returns the NFTokenID */
export async function mintLicense(
client: Client, hot: Wallet, metadataUri: string, taxon: number = TAXON.IAQ,
) {
// 서명자는 Hot 지갑, 발행 주체(Issuer)는 콜드 지갑 — NFTokenMinter로 위임
const res = await submit(client, hot, {
TransactionType: 'NFTokenMint',
Account: hot.classicAddress,
Issuer: env.ISSUER_ADDRESS,
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, hot: Wallet, nftokenId: string, destination: string,
) {
const res = await submit(client, hot, {
TransactionType: 'NFTokenCreateOffer',
Account: hot.classicAddress,
NFTokenID: nftokenId,
Amount: '0',
Destination: destination, // 리딤으로 확인된 사용자 계정만 수락 가능
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;
}
/** User wallet accepts the offer → ownership transfers.
* D'CENT 펜딩 오퍼 폴백은 설정 플래그 allowPendingOffer(기본 false)로 제어한다. */
export async function acceptLicense(client: Client, user: Wallet, offerId: string) {
return submit(client, user, {
TransactionType: 'NFTokenAcceptOffer',
Account: user.classicAddress,
NFTokenSellOffer: offerId,
});
}
온체인 라이선스 검증
오라클 엔드포인트가 매 요청마다 호출하는 핵심 함수다. Issuer 주소와 Taxon을 함께 확인해야 임의의 제3자가 발행한 위조 NFT를 거를 수 있다.
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 예시
{
"schema": "wellbian-oaq-device/v1",
"name": "Wellbian OAQ Node #0042",
"description": "Outdoor Air Quality station license",
"device": {
"serial": "KW-OAQ-0042",
"model": "TBD",
"sensors": ["pm25", "pm10", "o3", "no2", "temp", "rh"],
"installed_at": "2026-03-11"
},
"context": { "geohash5": "wydm9", "space_type": "outdoor" },
"license": { "tier": "oaq-standard", "valid_until": "2029-03-11" }
}
실내 IAQ 노드의 메타데이터는 주거 환경 버킷을 추가로 포함한다 —
라이선스 계층 · 품질 점수 절의 wellbian-iaq-device/v1 스키마 참고.
두 스키마 모두 정밀 좌표를 넣지 않는다는 규칙을 공유한다.
account_nfts를 요청마다 호출하면 텔레메트리 처리량이 원장 조회에 묶인다.
결과를 주소 단위로 30~60초 TTL 캐싱하고, 라이선스 폐기가 필요하면
NFT를 회수(재오퍼)하거나, tfBurnable로 발행한 뒤 발행 권한을 가진 콜드 Issuer가 NFTokenBurn으로 소각하는 정책을 함께 정의해야 한다.
tfTransferable만 설정하면 발행자가 강제 회수할 수 없다는 점에 유의한다.
STEP 04 DePIN 오라클 & 보상 엔진
텔레메트리 수신 → 라이선스 검증 → WLBN 보상 지급까지의 무인 파이프라인.
API 명세
| 항목 | 내용 |
|---|---|
| Endpoint | GET https://api.wellbianlabs.io/v1/c?<base64> (Supabase Edge Function proxy) |
| Body | { schema, deviceAddress, token, batch: [ { ts, pm25, co2, tvoc, tempC, rh, … } ] } |
| 202 | { accepted: 5 } — 수신 확인. 보상은 일 1회 에폭 정산에서 적립되며, 24시간 홀드 후 클레임 시 지급 |
| 401 | { error: "BAD_TOKEN" } — 수집 토큰 불일치 (기기별 토큰으로 전환 중, 페이로드 서명 검증은 로드맵) |
| 403 | { error: "NO_VALID_LICENSE" } — 라이선스 NFT 미보유 |
| 422 | { error: "OUT_OF_RANGE" } — 기기 시각이 10분 초과로 어긋나면 거부하지 않고 서버 수신 시각(KST)으로 보정 후 수용 |
| 429 | { error: "RATE_LIMITED" } — 제출 주기 위반 |
import { getClient } from '../xrpl/client';
import { verifyDeviceLicense } from '../xrpl/nft';
import { env } from '../config/env';
import { absoluteHumidity } from './iaq/humidity';
import { TAXON } from '../xrpl/nft';
export interface IaqReading {
ts: number;
pm25: number; pm10: number;
co2: number; tvoc: number;
tempC: number; rh: number;
}
const RANGES: Record<keyof IaqReading, [number, number]> = {
ts: [0, Number.MAX_SAFE_INTEGER],
pm25: [0, 1000], pm10: [0, 1000],
co2: [350, 5000], tvoc: [0, 60000],
tempC: [-10, 60], rh: [0, 100],
};
const lastSeen = new Map<string, number>();
const MIN_INTERVAL_MS = 4 * 60_000; // 1-minute transmit cadence; floor keeps 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) accrual only — the daily epoch settles accruals; payout happens on claim
await accrueForEpoch(deviceAddress, license, enriched);
lastSeen.set(deviceAddress, Date.now());
return { accepted: batch.length }; // HTTP 202
}
보상 엔진 파라미터
| 항목 | 정책 값 | 현행 구현 (2026-09) |
|---|---|---|
| 에폭 | 1일 · KST 자정 경계 | 동일 |
| 일일 리워드 | 7 × 0.78^(단계−1) × 품질 등급(0 · 1.0 · 1.2) | 1단계 정상 7 정액 적립 |
| 에폭 예산 | 활성 지점 × 7.14 × 단계 계수 | 지점당 7 상한으로 동일 효과 |
| 재분배 | 미지급분 가중치 비례 · 지점당 상한 10.5 · 초과분 소각 | 거버넌스 변수로 반영 예정 |
| 품질 등급 | 전송률 95%+ 전액 · 80~95% 선형 · 80% 미만 0 · 품질 점수 상위 10% 우수 | 품질 점수(uptime 40 · outlier 25 · correlation 20 · calibration 15) 산출 중 · uptime < 0.2이면 0 |
| 클레임 홀드 | 24시간 | 동일 |
| 방출 상한 | 10단계 누적 26.65억 (리워드 1기 28억 이내) | 보상 풀 소진 시 적립 중단 |
요청당 1 Payment를 발생시키면 3~5초의 원장 확정 대기가 HTTP 응답에 포함되고, 노드가 늘수록 Sequence 경합이 심해진다. 더 근본적으로, 고정 예산 배분은 에폭 종료 시점에 전체 노드의 품질 점수를 알아야 계산할 수 있으므로 즉시 지급 자체가 성립하지 않는다.
따라서 수신은 202 Accepted로 즉시 응답하고, 에폭은 KST 자정을 경계로 하는 1일 고정 단위다. 정산은 5분 주기의 재개 가능 스텝 러너 크론이 수행하며, 핫월렛 트랜잭션은 아웃박스 패턴으로 발행된다. 보상은 적립되고 지급은 클레임 시점에 이루어진다. 실외 OAQ 관측망의 보상 tier는 아직 미정이다.
Hot Wallet(운영 지갑) 하나가 동시에 여러 Payment를 서명하면 Sequence 번호가 충돌해 tefPAST_SEQ / terPRE_SEQ가 발생한다. Hot Wallet의 트랜잭션 제출은 아웃박스 패턴으로 기록 후 단일 워커가 순차 제출하며, 크론이 중단되어도 미완료 아웃박스부터 재개한다.
STEP 05 AMM 유동성 풀 (XLS-30)
WLBN ↔ RLUSD 풀을 생성해 원장 내에서 즉시 스왑이 가능하게 한다. AMM은 스왑 편의 전용이며 Wellbian(WLBN)의 가격 발견·가치 평가 기능을 하지 않는다.
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.RLUSD_ISSUER_ADDRESS, value });
/** AMM pool deposit 100,000 WLBN : 1,000 RLUSD — 예시 값. 예치 비율은 스왑 교환비 산정용일 뿐
* WLBN의 가치·법정화폐 환산을 의미하지 않는다. */
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.RLUSD_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.RLUSD_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: 0, // tfPartialPayment 미사용
});
}
/** 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.RLUSD_ISSUER_ADDRESS },
} as never);
return res.result;
}
- 자기 자신에게 보내는 Payment가 XRPL의 통화 변환 관용구다.
Destination을 본인 주소로 둔다. SendMax를 반드시 지정한다. 없으면 풀 상태에 따라 예상보다 훨씬 많은 RLUSD가 소진될 수 있다.tfPartialPayment를 켜면 목표량 미달로도 성공 처리되므로, 결제 검증 로직에서 실제 전달액(delivered_amount)을 확인해야 한다. Step 6의 검증에서 이 필드를 쓰는 이유다.- 양쪽 자산 모두 매수자에게 TrustLine이 설정돼 있어야 한다.
STEP 06 x402 자율 결제 게이트웨이
메인넷 운영 중인 자율 결제 게이트웨이. 외부 AI 에이전트가 사람 개입 없이 HTTP 402 응답을 읽고, 온체인 결제 후 재요청해 데이터를 얻는다.
1차 요청 — 402 챌린지
HTTP/1.1 402 Payment Required
Content-Type: application/json
WWW-Authenticate: x402 realm="wellbian-data", network="xrpl-mainnet"
{
"x402Version": 1,
"resource": "/api/data/aggregates",
"accepts": [
{
"scheme": "xrpl-payment",
"network": "xrpl-mainnet",
"payTo": "rhTTtx8YyKzcmPDsoroYXuHdGMThrnweb1",
"asset": { "currency": "WLBN", "issuer": "rDJz8WJhsKgydqJzSZXMpsJot3eRmSkR5", "value": "<수량>" }, // 수량 기준 단가 · RLUSD/법정화폐 환산 아님
"destinationTag": 402001,
"maxTimeoutSeconds": 120
},
{
"scheme": "xrpl-payment",
"network": "xrpl-mainnet",
"payTo": "rhTTtx8YyKzcmPDsoroYXuHdGMThrnweb1",
"asset": { "currency": "RLUSD", "issuer": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De", "value": "0.002" },
"destinationTag": 402001,
"maxTimeoutSeconds": 120
}
],
"description": "Wellbian IAQ 집계 데이터 — 호출당 과금"
}
2차 요청 — 트랜잭션 검증
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.HOT_WALLET_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' };
const EXPECTED_ISSUER = { [WLBN]: env.ISSUER_ADDRESS, [RLUSD]: env.RLUSD_ISSUER_ADDRESS };
if (delivered.issuer !== EXPECTED_ISSUER[delivered.currency])
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 };
}
게이트웨이 미들웨어
import { RequestHandler } from 'express';
import { verifyPayment } from '../services/x402';
import { buildChallenge } from '../services/x402.challenge';
export const x402Gate: RequestHandler = async (req, res, next) => {
const payTx = req.header('x-payment-tx'); // 선불 크레딧은 x-api-key 경로로 별도 처리
const match = payTx?.match(/^([A-F0-9]{64})$/i);
if (!match) {
res.setHeader('WWW-Authenticate', 'x402 realm="wellbian-data", network="xrpl-mainnet"');
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 에이전트 클라이언트 예제
import { getClient } from '../xrpl/client';
import { sendIOU } from '../xrpl/token';
import { agentWallet, env } from '../config/env';
import { toCurrencyHex } from '../config/currency';
const API = 'https://wellbian.io/api/data/aggregates';
const query = new URLSearchParams({ taxon: 'IAQ', geohash: 'wydm6', from: '2026-08-31T00:00:00+09:00', to: '2026-09-01T00:00:00+09:00' });
async function call(headers: Record<string, string> = {}) {
return fetch(`${API}?${query}`, { method: 'GET', headers });
}
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({ 'x-payment-tx': 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); });
소각 정책 — 총 50억 · 수량과 트랜잭션 공개
예정 소각 25억(마일스톤 4회 분할)과 매입 소각 24.65억(사업 수익으로 시장에서 매입)을 합해 총 발행량의 50%를 소각한다. 여기에 품질 미달 재분배 초과분과 10단계 게이트 미통과 시 2기 리워드 18억이 더해진다. 소각은 IOU를 Issuer로 되돌리는 방식이며, 소각 수량과 트랜잭션은 사후 공표하고 매입 시점·가격은 공개하지 않는다.
import { getClient } from '../xrpl/client';
import { sendIOU } from '../xrpl/token';
import { WLBN } from '../config/currency';
import { env, hotWallet, treasuryWallet } from '../config/env';
export type BurnReason = 'scheduled' | 'buyback' | 'quality-shortfall' | 'round2-unopened';
/** Return WLBN to the Issuer = burn. Quantity and tx hash are published; buyback timing/price are not. */
export async function burn(amountWlbn: string, reason: BurnReason) {
if (Number(amountWlbn) <= 0) return { burned: '0' };
const from = reason === 'quality-shortfall' ? hotWallet : treasuryWallet;
const client = await getClient();
const res = await sendIOU(client, from, env.ISSUER_ADDRESS, {
currency: WLBN, issuer: env.ISSUER_ADDRESS, value: amountWlbn,
});
await publishBurn({ reason, amount: amountWlbn, tx: res.result.hash }); // 수량·tx만 공표
return { burned: amountWlbn, txHash: res.result.hash };
}
IOU는 발행자의 부채 기록이다. 보유자가 발행자에게 되돌려 보내면 TrustLine 잔액이 상계되어
유통량이 실제로 감소한다. 별도의 burn 함수는 필요 없다.
다만 Issuer 계정이 여전히 DefaultRipple과 발행 권한을 갖고 있으므로
"재발행 불가"를 담보하려면 별도의 blackhole 계정(regular key를
rrrrrrrrrrrrrrrrrrrrrhoLvTp로 설정하고 마스터키 비활성화)을 쓰는 것이 더 강한 보증이다.
현재는 Issuer 반환에 의한 상계 방식을 사용하며, blackhole 계정 전환 여부는 별도로 고지한다.
운영 토크노믹스 정책
정량 지급 · 총량 100억 · 소각 50억. 본 절은 tokenomics.wellbianlabs.io에 공표된 정책과 동일하며, 수량만 정의하고 가치는 정의하지 않는다.
출금 개시 — 상장 전 시장가격 형성 차단
원칙 적립된 WLBN은 DEX 상장 전까지 온체인으로 이전되지 않는다. 정산은 매일 이뤄지고 수량은 계정에 그대로 쌓이지만, 실제 트랜잭션은 발생시키지 않는다. 사이트 계정 안의 포인트로만 존재한다.
이유 상장 전에 토큰이 유통되면 장외에서 가격이 먼저 형성된다. 유통량이 극히 적은 상태에서 만들어진 가격은 네트워크의 실제 가치를 반영하지 못하고, 최초 상장가의 기준을 왜곡한다. 출금을 묶어두는 것은 최초 상장 시점에 제 가치를 받기 위한 조치다.
보장 적립은 중단되지 않으며 쌓인 포인트는 절대 사라지지 않는다. 개시일은 재단이 정해 별도 공지하며, 개시 이후 보유 수량 전액을 온체인으로 인출할 수 있다.
우선순위 이 정책은 재단이 정한 최상위 정책으로, 지급·인출에 관한 다른 절의 서술에 우선한다.
단 하나의 원칙 — 정량 지급
| 항목 | 금액 기준이었다면 | 정량 지급 (WLBN) |
|---|---|---|
| 지급 단위 | 「하루 얼마어치」 — 개수가 매일 변동 | 「하루 7개」 — 개수 고정 |
| 시세가 내리면 | 지급 개수가 늘어남 — 방출 폭증·악순환 | 개수 그대로 · 방출 불변 |
| 시세가 오르면 | 지급 개수가 줄어 참여자 이득 없음 | 개수 그대로 · 참여자 이득 |
| 총 방출량 | 예측 불가 | 10단계 누적 26.65억개 확정 |
| 대가의 성격 | 가치 보장 암시 | 데이터 제공에 대한 역무의 대가 |
총량 100억 배분
| 버킷 | 수량 | 비중 | 해제 조건 · 베스팅 |
|---|---|---|---|
| 노드 홀더 리워드 1기 (Phase 1~10) | 28억 | 28% | 목표 유통량 규칙에 따라 에폭마다 방출 |
| 노드 홀더 리워드 2기 (Phase 11~) | 18억 | 18% | 10단계 게이트 통과 시에만 개방 · 미통과 시 소각 전환 |
| 소각 예정 | 25억 | 25% | 마일스톤 분할 소각 · 매입 24.65억과 합해 총 50억 |
| LP 리워드 (유동성 마이닝) | 8억 | 8% | 수량 고정 지급 · 최소 예치 기간 · 조기 인출 시 몰수 |
| 팀 · 기여자 | 6억 | 6% | 12개월 클리프 + 48개월 선형 · 일정 전체 공개 |
| 생태계 · 지역 파트너 | 5억 | 5% | 권역 진입 마일스톤마다 조건부 해제 |
| 전략 투자 · 프라이빗 | 5억 | 5% | 6~12개월 클리프 + 24~36개월 베스팅 · TGE 언락 0 |
| 유동성 · 준비금 | 5억 | 5% | AMM 시드와 매수벽 · 매입분은 전량 소각 |
| 합계 | 100억 | 100% | 제네시스 전량 발행 후 발행 권한 소각 — 추가 발행은 불가능하다 |
일일 리워드 — 단계마다 22% 체감
daily reward = base 7 × 0.78^(phase − 1) × quality grade (0 ~ 1.2)
epoch budget = active points × 7.14 WLBN × phase factor
가격과 무관하게 수량만으로 정의된다. 단계는 연차가 아니라 지점 수와 수요 조건을 함께 만족하는 게이트로 넘어가며, 단계가 오를 때마다 지점당 수량이 22% 줄어든다.
| 단계 | 일 기본 | 월 | 연 | 1단계 대비 | 단계 방출량 | 참여 시점별 누적 수령 |
|---|---|---|---|---|---|---|
| 1 국내 실내 | 7.00개 | 213개 | 2,555개 | 100% | 0.64억 | 10,646개 (제네시스) |
| 3 | 4.26개 | 130개 | 1,554개 | 61% | 1.87억 | 6,098개 (57%) |
| 5 | 2.59개 | 79개 | 946개 | 37% | 3.31억 | 3,331개 (31%) |
| 7 | 1.58개 | 48개 | 575개 | 23% | 3.74억 | 1,647개 (15%) |
| 10 | 0.75개 | 23개 | 273개 | 11% | 2.73억 | — |
| 10단계 누적 | 26.65억 | 리워드 1기 예산 28억 이내 |
제네시스 홀더는 7단계 참여자의 6.5배 수량을 받는다. 참여 기간이 길어서만이 아니라 단계마다 지급 수량 자체가 줄기 때문이다. 그 수량이 나중에 얼마가 될지는 말할 수 없다.
품질 게이트 — 예산은 고정, 배분은 경쟁
first pass normal 7 · excellent 8.4 · failing 0
redistribute unpaid share pro rata by weight · cap 10.5 per point · excess burned
| 전송률 미달률 | 유효 지점 | 미지급분 | 정상 최종 | 우수 최종 | 소각 | 총 지급 |
|---|---|---|---|---|---|---|
| 0% | 100 | 0개 | 7.00개 | 8.40개 | 0개 | 714개 |
| 10% | 90 | 71.4개 | 7.78개 | 9.33개 | 0개 | 714개 |
| 20% | 80 | 142.8개 | 8.75개 | 10.50개 (상한) | 0개 | 714개 |
| 40% | 60 | 285.6개 | 10.50개 | 10.50개 | 84개 | 630개 |
100지점 예시(예산 714 = 100 × 7.14). 모든 행에서 총 지급 + 소각 = 예산이다. 품질이 나쁠수록 총 방출이 줄어든다.
| 항목 | 규칙 |
|---|---|
| 지급 조건 | 전송률 95%+ 전액 · 80~95% 선형 감액 · 80% 미만 무지급 |
| 우수 보너스 | 데이터 품질 상위 10%에 +1.4개 — 공기가 좋은 곳이 아니라 측정이 정확한 곳 |
| 품질 등급 산출 | 전송률 게이트를 통과한 지점에 대해 품질 점수(가동률·이상치·인근 상관·보정)를 매기고, 상위 10%가 우수(1.2), 나머지가 정상(1.0), 게이트 미달이 0 |
| 부정 방지 | 설치 사진은 진입 관문 · 지속 검증은 CO₂ 일주기 패턴 |
현행 정산 엔진은 1단계 기준으로 지점당 일 7개를 적립하고 24시간 홀드 후 클레임한다. 우수 보너스(8.4)·미지급분 재분배·단계 계수 0.78은 본 정책의 확정 사항으로, 엔진의 거버넌스 변수로 순차 반영한다. 반영 전까지 실제 지급은 1차 지급 규칙(정상 7 · 미달 0)만 적용된다.
소각 50억 — 총 발행량의 절반
| 구분 | 수량 | 방식 |
|---|---|---|
| 예정 소각 | 25억 | 마일스톤 분할 · 사전 공표된 4회 |
| 매입 소각 | 24.65억 | 사업 수익으로 시장에서 매입 후 소각 |
| 합계 | 50억 | 총 발행량의 50% |
| 원칙 | 내용 |
|---|---|
| 목표 유통량 추종 | 유통 비율 목표를 정하고, 넘으면 사들여 태운다 |
| 매입분 전량 소각 | 되사서 다시 파는 구조는 방어가 아니라 자전거래다 |
| 품질 미달분 소각 | 재분배 상한을 넘긴 물량은 지급하지 않고 소각한다 |
| 2기 미개방분 소각 | 10단계 게이트 미통과 시 18억이 소각 대상으로 전환된다 |
| 사후 공표 | 소각 수량과 트랜잭션을 공개한다 — 매입 시점과 가격은 공개하지 않는다 |
소각은 「이번 분기 방출 0.9억개 · 소각 1.1억개」처럼 수량으로만 공표한다. 「소각으로 가격이 오른다」는 말은 하지 않는다 — 검증할 수 없는 주장이기 때문이다.
마일스톤 게이트 — 연차가 아니라 달성으로
| 게이트 | 공급 조건 | 수요 조건 | 보상 가중치 | 소각 | 거래소 |
|---|---|---|---|---|---|
| G0 개시 전 | 등록·검증 시스템 | 유료 고객 1건 | — | — | 상장 금지 |
| G1 제네시스 개시 | 모집 시작 | — | 품질 | 없음 | 금지 |
| G2 유동성 확보 | 초기 물량 판매 | — | 품질 | 없음 | 금지 |
| G3 수요 증명 | 5,000장 | B2B 3건 + 재구매 | + 희소성 | 없음 | 금지 |
| G4 완판 | 25,000장 · 가동률 80% 3개월 | 국내 매출 발생 | 품질 + 희소성 | 없음 | DEX 개시 |
| G5 해외 1호 | 60,000 | 해외 매출 발생 | 동일 | 없음 | 지역 거래소 검토 |
| G6 실외 라인 | 12만 | 연매출 10억 | + 실사용 | 없음 | — |
| G7 소각 개시 | 22만 | 연매출 20억 | 3요소 | 개시 | — |
| G8 전환기 | 35만 | 연매출 40억 | 동일 | 본격 | — |
| G9 자립 | 65만 | 사업 수익이 리워드 재원을 전액 부담 | 동일 | 가속 | 글로벌 거래소 검토 |
| G10 성숙 | 100만 | 잉여 수익 발생 | 동일 | 최대 | — |
수요 조건을 못 채우면 다음 게이트가 열리지 않는다. 미달 시 대응은 단계 진입 보류와 방출 축소다.
보조금에서 고객 자금으로
| 국면 | 지점 | 단계 방출량 | 재원 |
|---|---|---|---|
| 1~4단계 | 2.5만 → 22만 | 늘어남 | 보조금 — 하드웨어 사업 수익이 부담 |
| 5~6단계 | 35만 → 50만 | 정점 후 감소 | 전환기 — 데이터 매출이 상당 부분 부담 |
| 7단계~ | 65만 → | 계속 감소 | 자립 — 고객 매출이 전액 부담 · 잉여는 소각 |
방출은 단계 체감 22% 때문에 6단계에서 정점을 찍고 줄어드는데 데이터 매출은 지점 수에 비례해 계속 늘어난다. 두 곡선이 만나는 지점부터 신규 참여금이 아니라 고객이 낸 돈으로 리워드를 지급한다. 자립 시점은 데이터 단가에 달려 있고, 그 단가는 아직 검증 중이다. 1단계에서 실제 계약으로 확인한 뒤 재산정하며, 달성을 약속하는 것이 아니라 목표로 제시한다.
제네시스 모집 구조
| 모집 트랙 | 1인 물량 | 25,000장 기준 | 공략 대상 |
|---|---|---|---|
| 개인 50% | 1장 | 12,500명 | 가정 · 1인 가구 · 육아 가구 |
| 다지점 사업자 30% | 6장 | 1,250명 | 카페 · 학원 · 병원 · 헬스장 — 밀도의 핵심 |
| 기관 · 건물 20% | 40장 | 125곳 | 아파트 단지 · 학교 재단 · 오피스 |
1인당 보유 상한은 개인 5장 / 사업자 50장이다. 미설치 NFT는 리워드가 없다 — 보유가 아니라 가동이 조건이다.
운영 원칙과 공개 정책
| 유동성 투입 우선순위 | 기간 | 수혜 |
|---|---|---|
| ① 거래 깊이 | 영구 | 거래하는 모두 |
| ② 매입 소각 | 영구 | 남은 홀더 전원 |
| ③ 매수 완충 | 일시 | 그 순간 판 사람 |
참여금의 20%가 판매 즉시 유동성으로 들어간다. 결제 구조는 기업 고객 원화·RLUSD, AI 에이전트 자동 결제, 프로토콜 내부는 매출 일부로 WLBN 매입이다. 고객에게 토큰을 사서 결제하라고 요구하지 않으며, 배분 비율은 사전 공표한다.
| 항목 | 공개 | 비공개 |
|---|---|---|
| 유동성 | 풀 잔고 온체인 상시 · 투입 규칙 · 소각 실적 | 매수 주문 가격 · 매입 시점 |
| 베스팅 | 일정 전체 · 언락 수량과 시점 | — |
| 리워드 | 지급 수량 · 품질 기준 · 재분배 결과 | — |
공개된 매수 주문은 시장에 무료 풋옵션을 주는 것이라, 장기 홀더가 아닌 단기 트레이더에게 재원이 흘러간다. 하지 않는 다섯 가지: 노드 수를 KPI로 삼기 · 수요 없이 다음 단계 열기 · 조기 거래소 상장 · 모든 데이터에 같은 보상 · 보조금 영구화.
우리가 보는 지표는 토큰 가격이 아니라 완판 · 가동률 · 데이터 매출이다. 판매가 목표에 미달하면 가격을 방어하는 것이 아니라 방출을 줄인다.
운영 방출 시뮬레이션 — 단계 체감 모델
10단계 누적 방출과 자립 교차점을 단계 체감 모델로 계산한다. 모든 수치는 공표된 정책 파라미터(기본 7개 · 체감 0.78 · 게이트별 지점 수)에서 유도되며, 보상 수준이나 수익에 대한 예측·약속이 아니다. 실제 수령량은 품질 등급과 재분배 결과에 따라 달라진다.
결론 먼저
모델 구조
reward(p, g) = 7 × 0.78^(p − 1) × g g ∈ { 0, 1.0, 1.2 }
budget(p, n) = n_active × 7.14 × 0.78^(p − 1) fixed per epoch
redistribute = unpaid × w_i / Σw cap 10.5 per point · excess → burn
emission(p) = Σ_epochs budget(p, n) − burn(p)
Σ p=1..10 = 2.665B ≤ 2.8B (round-1 reward bucket)
확정 파라미터
| 항목 | 확정값 | 근거 |
|---|---|---|
| 기본 리워드 | 일 7개 (정상 등급) | 사전 공표 수량 · 시세와 무관 |
| 단계 체감 | 0.78 (22%) | 10단계 누적을 리워드 1기 예산 28억 이내로 맞추는 값 |
| 품질 등급 | 0 · 1.0 · 1.2 | 미달 · 정상 · 우수(상위 10%, +1.4개) |
| 재분배 상한 | 지점당 10.5개 | 소수 독점 방지 · 초과분 소각 |
| 1단계 지점 수 | 25,000 | 국내 실내 · 제네시스 한정 모집 |
| 10단계 누적 상한 | 26.65억개 | 리워드 1기 버킷 28억 이내 |
| 2기 리워드 | 18억 | 10단계 게이트 통과 시에만 개방 · 미통과 시 소각 |
자립 시점은 데이터 단가에 달려 있고, 그 단가는 아직 검증 중이다. 1단계에서 실제 계약으로 확인한 뒤 재산정한다. 표의 값은 위 식에 게이트별 지점 수를 대입해 얻으며 공표 데크와 동일하다. 달성을 약속하는 것이 아니라 목표로 제시한다.
운영 잔여 리스크
구현 중 인지하고 관리해야 할 항목들.
| 항목 | 리스크 | 대응 | 우선순위 |
|---|---|---|---|
| 기기 신원 | NFT 보유만 검증하면 시드 탈취 시 위조 데이터 무제한 주입 | 페이로드 서명 검증 + 시계열 이상탐지 + 인근 노드 교차검증 | 권장 |
| 오라클 신뢰 | 오프체인 API가 단일 신뢰점 — 데이터가 원장에 남지 않음 | 배치 단위 머클루트를 Memos에 기록해 사후 감사 가능성 확보 | 권장 |
| Reserve 고갈 | TrustLine·Offer 누적으로 Hot wallet XRP 부족 → 전 트랜잭션 실패 | 잔액 임계 모니터링 + 미체결 Offer 정리 배치 | 권장 |
| 가격 변동 | 받는 수량은 고정이지만 그 수량의 가치는 오르내린다. 하락 위험은 전적으로 보유자가 부담한다 | 정량 지급 · 가치 보장 없음 · 목표가·수익률 미제시 | 인지 |
| 사업 미달 | 데이터가 팔리지 않으면 다음 단계가 열리지 않고 리워드 수량이 줄어든다 | 게이트에 수요 조건 병기 · 미달 시 방출 축소 | 필수 |
| 가동 의무 | 미설치·미가동 지점은 리워드가 지급되지 않는다. 전송률 80% 미만도 무지급이다 | 등급별 차등 · 사전 공표된 기준 · 설치 사진과 CO₂ 일주기 검증 | 필수 |
| 초기 거래 여건 | 개시 직후에는 거래가 얕아 적은 거래로도 시세가 크게 움직일 수 있다 | 유동성 깊이 우선 투입 · 대량 거래 분리 · 조기 상장 금지 | 권장 |
| 실내 데이터 민감성 | CO₂·PM 시계열에서 재실 인원·취침 시각·부재 기간이 추정됨 | 5단계 비식별화 + k-익명성 + 차분 공격 방어. 개인정보 법률 검토 선행 필수 | 필수 |
| 규제 변화 | 가상자산 규제는 관할마다 다르고 바뀐다 | 법률 의견서 확보 후 각 권역 진입 · 유틸리티 성격 유지(지분·배당 없음) | 필수 |
| 핫월렛 키 관리 | Hot Wallet 시드 유출 시 보상 풀 잔액이 즉시 유출됨 | KMS 기반 서명 분리 · 잔액 상한 유지 · Issuer(cold) 시드는 런타임 미로드 | 필수 |
| RLUSD 발행자 의존 | RLUSD 발행자(rMxCK…)의 동결·정책 변경이 결제·정산 경로를 차단할 수 있음 | TrustLine 상태 모니터링 + 대체 결제 통화 경로 사전 정의 | 권장 |
| 아웃박스 중복 지급 | 크론 재시작 시 미확정 트랜잭션을 재제출하면 이중 지급 위험 | 아웃박스 멱등키 + 제출 전 tx 조회 + reconcile 크론 대조 | 필수 |
| 클레임 홀드 중 장애 | 24시간 홀드 구간에서 정산 장애가 나면 적립분이 클레임 불가 상태로 남음 | 적립 원장과 온체인 결과를 reconcile 크론이 대조해 미지급분 자동 복구 | 필수 |
| 센서 캘리브레이션 드리프트 | 저가 MOX·광산란 센서는 수개월 단위로 편차가 누적되어 데이터 상품성이 하락 | 품질 점수의 calibration(15점) 항목 + 인근 노드 상관 기반 자동 보정 계수 | 권장 |
| x402 요청 헤더 | 본 구현은 X-Payment-Tx: <hash> 커스텀 헤더를 쓰며, 선불 크레딧(X-Api-Key · /api/data/topup) 경로를 병행한다 |
표준 x402는 X-PAYMENT 헤더를 쓴다. 응답 바디는 이미 표준 스키마이므로,
헤더만 교체하면 정합 완료 | 인지 |
운영 실행 체크리스트
각 Step은 승인 후 다음 단계로 진행한다. 완료 판정 기준을 함께 명시했다.
Step 1 — XRPL 기반 & 지갑
- 메인넷 WebSocket(
wss://xrplcluster.com) 클라이언트 싱글턴 + 재연결 처리
완료 기준:server_info응답 수신 - Issuer(cold, rDJz8…) / Hot(rhTTt…) / Treasury(rPZFY…) 3개 지갑 준비 및 XRP Reserve 확보
.env+.env.example구성,.gitignore등록, zod 검증- 공통
submit()래퍼로tesSUCCESS+validated강제
Step 2 — Wellbian 토큰
- currency hex 상수 확정 (WLBN / RLUSD)
- Issuer
AccountSet—asfDefaultRipple활성화 - Hot·Treasury·사용자 지갑 TrustSet —
tfSetNoRipple적용 - 총 100억 WLBN 발행 완료 확인 (추가 발행 없음)
완료 기준:account_lines에서 잔액 확인
Step 3 — 라이선스 NFT
- IPFS 메타데이터 스키마 확정 및 업로드 (실 URI 고정)
mintLicense→offerLicenseTo→acceptLicense전체 흐름verifyDeviceLicense()— Issuer + Taxon 이중 확인- 미보유 주소가
valid:false로 판정되는 네거티브 테스트
Step 4 — 오라클 & 보상
GET /v1/c수집 프록시 구현 + 5단계 수신 검증- 기기 서명 검증 → 라이선스 조회 순서 준수 (싼 검사 우선)
- 라이선스 검증 실패 시 403, 보상 미지급 확인
- 절대습도·이슬점 유도 후 저장
완료 기준:absoluteHumidity(20,60)≈10.35,(30,60)≈18.21 - 일 정산 — 총 지급 + 소각 = 에폭 예산(활성 지점 × 7.14 × 단계 계수), 지점당 10.5개 이내
완료 기준: 전송률 미달 지점 비율을 0~40%로 바꿔도 총 지급 + 소각 = 예산 - 클레임 API — 24시간 홀드 경과분만 1건의 WLBN Payment로 합산 지급
- reconcile 크론 — 온체인 결과와 적립·지급 원장 대조, 불일치 자동 복구
- 배치 머클루트를
Memos에 기록, 머클 증명 검증 - 핫월렛 아웃박스 패턴 적용 · 크론 중단 후 재개 검증
완료 기준: 재개 시 중복 지급 0건, Sequence 오류 0건
Step 5 — AMM
AMMCreateWLBN/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 스토어
- 에이전트 E2E(메인넷 실결제): 402 → 결제 → 재요청 → 200
완료 기준: 스크립트 1회 실행으로 전 과정 무인 완료 - 소각 배치 잡(예정 · 매입 · 품질 미달분) 실행 및 소각 수량·tx 공표, 유통량 감소 검증
IAQ 사양 — 데이터 계층
- IAQ 텔레메트리 스키마 v1 확정 + 기기 서명 규격
- Taxon 분리 (
IAQ 3026/OAQ 2026), 품질 등급(0 · 1.0 · 1.2) 적용 qualityScore()4개 항목 산출 — 특히 인근 노드 상관- 비식별화 P1~P5 파이프라인, 에폭 salt 회전 확인
- k-익명성 롤업 — geohash3에서도 미달 시 응답 거부
완료 기준: 노드 4개 격자 질의가 상위 격자로 롤업되거나 거부됨 - 차분 공격 방어 4종 (반올림·필터 상한·질의 예산·최소 코호트)
- 코호트 응답에 노드 목록이 포함되지 않음을 스키마 레벨에서 보장
- 선불 크레딧 — 충전·서명 차감·nonce 단조 증가
완료 기준: 동일 nonce 재전송이BAD_NONCE로 거부됨 - 미지급분 재분배 — 지점당 10.5개 상한, 초과분 소각 검증
- 미인가 주소로 IAQ 텔레메트리 전송 → 403 (라이선스 없음)
- NFT 발급 후 동일 주소 재전송 → 202 Accepted, 절대습도 유도값 확인
- 에폭 배치 실행 → 노드별 품질 점수와 가중치 배분 출력, 총액 = 예산 확인
- 노드 수를 10배로 복제 후 재실행 → 예산 = 활성 지점 × 7.14 × 단계 계수로 비례 증가, 지점당 수량 불변
- 제조사가 코호트 질의 → 402, 결제 후 재요청 → 200 + 크기만 반환
- 필터를 좁혀 재질의 → 422 COHORT_TOO_NARROW (실제 크기 미노출)
- 같은 tx_hash로 재호출 → 402 (HASH_ALREADY_USED)
- 소각 배치 → Issuer TrustLine 상계로 유통량 감소 확인
운영 용어집
| 용어 | 설명 |
|---|---|
| DePIN | Decentralized Physical Infrastructure Network. 물리 인프라(센서·기지국 등)의 구축·운영을 토큰 인센티브로 분산화하는 모델. |
| XLS-20 | XRPL의 네이티브 NFT 표준. NFTokenMint 등 전용 트랜잭션 타입을 제공하며 스마트컨트랙트가 불필요하다. |
| XLS-30 | XRPL 네이티브 AMM 표준. 오더북과 AMM 유동성이 자동으로 합산 라우팅된다. |
| Issued Asset (IOU) | 발행자 계정에 대한 채권 형태의 토큰. 별도 mint 트랜잭션 없이 발행자가 Payment를 보내면 생성된다. |
| TrustLine | 보유자가 특정 발행자의 특정 통화를 얼마까지 받을지 선언하는 온체인 관계. 없으면 tecNO_LINE. |
| Rippling | 동일 통화의 TrustLine들을 연결해 잔액이 자동 이동하는 XRPL 고유 메커니즘. 발행자 경유 전송의 기반이자 오남용 위험 요소. |
| x402 | HTTP 402 Payment Required를 실사용하는 M2M 결제 프로토콜. 서버가 결제 조건을 제시하고 클라이언트가 온체인 결제 증빙을 붙여 재요청한다. |
| delivered_amount | 트랜잭션 메타데이터에 기록된 실제 전달 금액. PartialPayment 우회를 막기 위해 검증은 이 값으로만 수행한다. |
| Destination Tag | Payment에 붙이는 32비트 정수 식별자. 동일 수취 주소로 들어오는 결제의 용도·고객을 구분한다. |
| IAQ | Indoor Air Quality. 실내 날씨(날씨). 본 문서에서는 PM2.5·PM10·CO₂·TVOC·온도·습도 6종 지표의 총칭. |
| 절대습도 (AH) | 단위 부피당 실제 수증기 질량(g/m³). 온도 종속인 상대습도와 달리 제습 부하를 직접 나타내므로, 제습 알고리즘·결로 예측의 입력값이 된다. |
| TVOC | 총휘발성유기화합물. MOX 센서는 상대 지수를 내므로 절대 농도로 해석하지 않고 추세·급변 탐지에 사용한다. |
| geohash | 위경도를 문자열로 인코딩하는 격자 체계. 자릿수가 짧을수록 넓은 영역을 뜻한다 (5자리 ≈ 4.9km, 3자리 ≈ 156km). |
| k-익명성 | 어떤 속성 조합으로도 최소 k명 미만으로 좁혀지지 않도록 보장하는 성질. 본 사양은 k=5를 하한, 반올림 후 25를 실질 하한으로 둔다. |
| 차분 공격 | 필터를 조금씩 바꾼 반복 질의의 결과 차이로 개별 대상을 분리해내는 공격. 카운트 반올림·필터 개수 상한·질의 예산으로 방어한다. |
| 에폭 (Epoch) | 보상 정산 주기. KST 자정을 경계로 하는 1일 고정 단위이며, 이 단위로 에폭 예산(활성 지점 × 7.14 × 단계 계수)을 품질 등급에 따라 지급·재분배한다. |
| 선불 크레딧 | 온체인 결제 1건으로 잔액을 충전하고 이후 호출은 서명으로만 차감하는 방식. 고빈도 API에서 원장 확정 지연을 회피한다. |
| RLUSD | 본 생태계의 정산 통화. 발행자는 rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De이며 WLBN Issuer와 다르다. |
| Issuer · Hot · Treasury 지갑 | Issuer(cold, rDJz8WJhsKgydqJzSZXMpsJot3eRmSkR5)는 발행 전용으로 런타임 미로드, Hot(rhTTtx8YyKzcmPDsoroYXuHdGMThrnweb1)은 지급·수취, Treasury(rPZFY2yLgqjmBrxtxWHXnB1doBt7gu7xin)는 보관을 담당한다. |
| 클레임 홀드 | 에폭 정산으로 적립된 보상이 클레임 가능해지기까지의 24시간 대기 구간. 홀드 경과분은 1건의 WLBN Payment로 합산 지급된다. |
| 아웃박스 패턴 | 핫월렛 트랜잭션을 먼저 아웃박스에 기록하고 단일 워커가 순차 제출하는 방식. 크론이 중단돼도 미완료 항목부터 재개해 이중 지급을 막는다. |
| Taxon 체계 | 관측소 1001·1002·1003, 스테이션 IAQ 3026 · OAQ 2026 · AI 인프라 4026 · 레이더 5026 · 위성 6026, 예매권 1010. |
| 지원 지갑 | 간편지갑(아이디·비밀번호 / 구글), D'CENT 모바일 인앱, Xaman, GemWallet, Crossmark. |
| 단계 (Phase) · 단계 계수 | 지점 수와 수요 조건을 함께 만족하는 게이트로 넘어가는 방출 구간. 단계 계수 0.78^(단계−1)이 지점당 수량에 곱해져 단계마다 22% 체감한다. |
| 품질 등급 | 전송률 게이트와 품질 점수로 정해지는 0 · 1.0 · 1.2의 계수. 미달 0, 정상 1.0(일 7개), 상위 10% 우수 1.2(일 8.4개). |
| 재분배 | 에폭 예산 중 1차 지급 후 남은 미지급분을 가중치 비례로 나누는 절차. 지점당 10.5개를 상한으로 하고 초과분은 소각해 총 지급 + 소각 = 예산을 유지한다. |
| 마일스톤 게이트 | G0~G10의 단계 진입 조건. 공급(지점 수·가동률)과 수요(유료 고객·매출) 조건을 병기하며, 수요 미달 시 단계를 보류하고 방출을 줄인다. |
| 매입 소각 | 사업 수익으로 시장에서 WLBN을 매입해 Issuer로 되돌리는 소각. 매입분은 전량 소각하며 수량·트랜잭션만 공표하고 시점·가격은 공개하지 않는다. |
| 목표 유통량 추종 | 유통 비율 목표를 정하고 이를 넘으면 매입·소각으로 되돌리는 운영 규칙. 소각 총량 50억(예정 25억 + 매입 24.65억)의 집행 기준이다. |
| Blackhole 계정 | Regular Key를 무효 주소로 설정하고 마스터키를 비활성화해 영구히 서명 불가하게 만든 계정. 본 시스템의 Issuer는 blackhole이 아니며, 소각은 Issuer 반환에 의한 상계 방식이다. |