Casper Carbon v2: AI 에이전트가 실제로 탄소 배출권을 구매하게 만들기 위해 필요했던 것들
요약
Casper Carbon 프로젝트가 AI 에이전트를 통해 탄소 배출권을 실제로 구매할 수 있도록 지갑 통합 과정을 재구축한 기술적 사후 분석입니다. 기존 CDN 기반 SDK의 성능 저하와 버그를 해결하기 위해 직접적인 브라우저 확장 프로그램 통합 방식을 채택했습니다.
핵심 포인트
- 기존 CDN 방식의 2MB 페이로드 및 성능 저하 문제 식별
- 이벤트 리스너 누수 및 정규표현식 충돌로 인한 브라우저 정체 해결
- Casper Wallet과 직접 통신하는 경량화된 통합 방식 도입
- 자율 AI 에이전트 기반의 RWA 마켓플레이스 구축 경험 공유
우리가 어떻게 지갑 통합을 처음부터 다시 구축했는지, 모든 리스팅을 조용히 취소해버린 통화 변환 버그를 어떻게 해결했는지, 그리고 Agentic Buildathon 결승 라운드 시간에 맞춰 Casper Testnet에서 라이브 에이전트 기반 RWA 마켓플레이스를 어떻게 출시했는지에 대한 이야기입니다.
Casper Agentic Buildathon을 팔로우해 오셨다면, 저희 프로젝트인 Casper Carbon을 알고 계실 것입니다. 4개의 Odra 스마트 컨트랙트(smart contracts), 3개의 자율 AI 에이전트(autonomous AI agents) (Verifier, Compliance, Market), 그리고 Next.js 대시보드로 구성되어 있으며, 모두 Casper Testnet에서 라이브로 실행되고 있습니다.
결승 라운드를 위해, 저희는 마지막 간극을 메우기로 했습니다. 바로 사용자가 Casper Wallet 브라우저 확장 프로그램을 통해 마켓플레이스에서 실제로 탄소 배출권(carbon credits)을 구매할 수 있도록 하는 것이었습니다. 그 이후로 2주간의 디버깅, 하나의 SDK 포기, 조용한 데이터 오염(data-corruption) 버그, 그리고 훨씬 더 나은 시스템을 구축하는 과정이 이어졌습니다.
다음은 전체 기술적 사후 분석(postmortem)입니다.
지갑(The Wallet): 2MB CDN에서 window.CasperWalletProvider까지
1단계: CSPR.click (우리가 이를 포기한 이유)
저희의 원래 지갑 통합은 사전 구축된 UI 위젯인 CSPR.click을 사용했습니다. 이론적으로는 완벽했습니다. CDN 스크립트 태그를 드롭하고, 컨테이너 div를 추가하고, showTopBar()를 호출하기만 하면 세련된 지갑 팝업을 얻을 수 있었습니다.
실제로는 다음과 같았습니다:
- 2MB CDN 페이로드(payload). 번들에는 전체 UI 라이브러리, SVG 에셋, 그리고 페이지 내부에 React와 유사한 가상 DOM(virtual DOM)을 렌더링하는 런타임이 포함되어 있었습니다. 매 페이지 로드 시 이를 파싱하는 동안 정체가 발생했습니다.
MaxListeners경고. SDK는 수십 개의 이벤트 리스너(event listeners)를 부착하고 이를 전혀 정리하지 않았습니다. 세 번의 페이지 이동 후, Node는MaxListenersExceededWarning을 경고했고 브라우저 탭은 느려졌습니다.- 지갑 연결 시 Regex 충돌. SDK의 내부 메시지 파서는 특정 Casper Wallet 응답에서 치명적인 백트래킹(backtracking)을 일으키는 정규표현식(regex)을 사용했습니다. 페이지는 8초 동안 멈춘 뒤 `
우리는 정규표현식(regex) 충돌 원인을 격리하기 위해 3일을 보냈습니다. 결국 우리는 깨달았습니다. 만약 미리 빌드된 UI가 문제라면, 그것을 제거하십시오. Casper Wallet과 직접 통신하십시오.
2단계: 직접 통합 (Direct integration)
Casper Wallet 브라우저 확장 프로그램이 설치되어 있으면 window.CasperWalletProvider()를 사용할 수 있습니다. API는 간단합니다:
const provider = window.CasperWalletProvider();
await provider.requestConnection();
const publicKey = await provider.getActivePublicKey();
이것이 전부입니다. CDN도, UI 프레임워크도, 번들링된 React도 필요 없습니다.
한 가지 미묘한 차이점은 CasperWalletProvider()에 인자가 전달되지 않는다는 점입니다. 이전 문서에는 window.location.origin을 전달하는 것으로 나와 있었으나, 이는 최신 확장 프로그램 버전에서 오류를 발생시킵니다. 그냥 그대로 호출하십시오.
우리는 이 세 가지 호출을 감싸는 얇은 React 컨텍스트(context)로 wallet.tsx를 다시 작성했습니다. 전체 지갑 통합 코드가 2MB에서 약 40줄의 TypeScript로 줄어들었습니다.
// web/src/lib/wallet.tsx (단순화 버전)
const provider = window.CasperWalletProvider();
await provider.requestConnection();
...
3단계: 서버 측 배포 빌드 (Server-side deploy building)
사용자가 연결되면, 크레딧을 구매해야 합니다. 흐름은 다음과 같습니다:
- 사용자가 Buy 클릭 → 서버가 서명되지 않은 배포(unsigned deploy)를 빌드 → 이를 JSON으로 반환
- 사용자가 Casper Wallet 확장 프로그램에서 서명
- 서명된 배포가 Casper RPC 엔드포인트로 프록시(proxy)됨
가장 까다로운 부분은 배포 빌더(deploy builder)였습니다. casper-js-sdk의 Deploy 클래스에는 정적(static) 메서드인 toJSON()이 있습니다 — 즉, deploy.toJSON()이 아니라 Deploy.toJSON(deploy)로 호출해야 합니다. 이것이 조용히 비어 있는 응답과 유효한 배포 사이의 차이를 만듭니다.
우리는 또한 전형적인 CLValue 대소문자 함정에 빠졌습니다. SDK는 newCLUInt256() (대문자 U, I)을 사용하는 반면, 직관적으로는 newCLUint256()이라고 생각하기 쉽습니다. SDK는 버전마다 이 부분의 일관성이 없으므로, 자동 완성(auto-complete)을 믿기 전에 타이핑(typings)을 확인하십시오.
마켓 에이전트(The Market Agent): "50 베이시스 포인트(Basis Points)"가 모든 것을 취소했을 때
우리의 핵심 기능 중 하나는 **마켓 에이전트 (Market Agent)**입니다. 이는 온체인 (on-chain) 크레딧 가격을 Carbonmark의 실시간 스팟 가격 (spot price)과 비교하여, 50 베이시스 포인트 (bps)를 초과하여 벗어나는 리스팅 (listings)을 취소하는 자율 봇 (autonomous bot)입니다.
예선 라운드 이후, 마켓 에이전트는 정상적으로 작동하며 매 사이클마다 "Adjusted 12 listings"를 기록하고 있었고, 우리는 그것이 완벽하게 작동하고 있다고 생각했습니다. 하지만 마켓플레이스 (marketplace) 페이지에는 활성화된 리스팅이 하나도 표시되지 않았습니다.
여기에 버그가 있었습니다:
// agents/src/market.ts (수정 전)
const externalPriceMotes = BigInt(
Math.floor(externalPrice * 1_000_000_000)
...
externalPrice는 Carbonmark로부터 USD per tonne (톤당 달러) 단위로 들어옵니다. 예를 들어 $10라고 가정해 봅시다. 그러면 externalPriceMotes는 10_000_000_000이 됩니다.
하지만 온체인 리스팅 가격은 CSPR motes 단위입니다. 만약 검증자 (verifier)가 톤당 15 CSPR로 가격을 책정했고 CSPR가 $0.10라면, 적정 가격은 (10 / 0.10) * 1e9 = 100_000_000_000 motes가 되어야 하며, 10_000_000_000이 되어서는 안 됩니다.
마켓 에이전트는 15_000_000_000 (온체인)을 10_000_000_000 (CSPR로 취급된 USD)과 비교하고 있었고, 스프레드 (spread)를 (15e9 - 10e9) / 1e7 = 500 bps로 계산하여, 500 > 50이기 때문에 모든 리스팅을 취소하고 있었던 것입니다.
단 하나도 빠짐없이 말이죠.
문제를 파악한 후 수정 방법은 간단했습니다. CoinGecko에서 CSPR/USD 가격을 가져와 적절하게 변환하는 것이었습니다:
const csprPrice = await fetchCsprUsdPrice(); // 예: 0.10
const externalPriceMotes = BigInt(
Math.floor((externalPrice / csprPrice) * 1_000_000_000)
...
또한 캐싱 (60초 TTL)과 3회의 재시도 (retries) 기능이 포함된 fetchCsprUsdPrice() 함수를 추가했습니다:
export async function fetchCsprUsdPrice(): Promise<number | null> {
// CoinGecko → "casper-network" → usd
// 60초 동안 캐싱, 2초 간격으로 3회 재시도
...
요점 (Upshot)
이제 마켓 에이전트는 스프레드를 정확하게 계산합니다. 우리의 리스팅은 활성 상태를 유지하며, 구매 (Buy) 버튼이 실제로 작동합니다.
검증자 (The Verifier): 시장 인지형 가격 책정 (Market-Aware Pricing)
이왕 하는 김에 검증자 (verifier)도 수정했습니다. 기존에는 다음과 같이 하드코딩되어 있었습니다:
price_per_token: "15000000000" // 항상 15 CSPR
CSPR 가격이 일주일 만에 50% 변동했을 때는 전혀 말이 되지 않는 방식이었습니다. 이제는 동일하게 Carbonmark와 CoinGecko 데이터를 가져와 공정 시장 가치 (fair market value)로 가격을 책정합니다:
const [carbonPrice, csprPrice] = await Promise.all([
fetchCarbonPrice(config.CARBON_PROJECT_KEY),
fetchCsprUsdPrice(),
...
두 가격 가져오기 중 하나라도 실패하면 15 CSPR로 폴백 (fallback) 됩니다. 기능은 다소 저하되지만, 서비스가 차단되지는 않습니다.
UI 개선: 실시간 에이전트 상태, 개선된 에러 상태, 스로틀링된 폴링 (Throttled Polling)
에이전트 실시간 인디케이터 (Agent live indicators)
대시보드에 각 에이전트별로 녹색/회색 점을 추가했습니다. 이 점들은 각 에이전트 계정별로 CSPR.cloud의 배포 이력 (deploy history)을 폴링 (polling) 하여 작동합니다. 만약 최근 40초 이내에 배포가 제출되었다면 점은 녹색이 되고, 그렇지 않으면 회색이 됩니다.
초기 구현에서는 세 명의 에이전트 각각에 대해 10초마다 배포 이력 요청을 보냈습니다. 세 명의 에이전트가 있는 페이지를 로드할 경우, CSPR.cloud에 분당 18개의 요청을 보내게 됩니다. 저희는 폴링 훅 (poll hook) 자체에 스로틀링 (throttling)을 적용했습니다:
// usePoll은 요청이 이미 진행 중(in-flight)이면 건너뜁니다
const inflight = useRef(false);
if (inflight.current) return;
...
기본 간격: 체인 상태 (chain state) 30초, 에이전트 활동 (agent activity) 30초 (기존 10초에서 상향).
ErrorCard 및 EmptyState
이제 모든 페이지는 데이터를 일관된 에러/빈 상태 (error/empty states)로 감쌉니다:
// web/src/components/shared.tsx
export function ErrorCard({ message }: { message: string }) {
return (
...
더 이상 처리되지 않은 프로미스 거부 (unhandled promise rejections)로 인해 페이지가 조용히 빈 화면으로 변하는 일은 없습니다.
검증 가능한 AI 히어로 섹션 (Verifiable AI hero section)
대시보드에는 이제 SHA-256 커밋먼트 모델 (commitment model)을 설명하는 전용 콜아웃 (callout)이 있습니다. 모든 LLM 판단은 온체인 (on-chain)에 해시 커밋 (hash-committed)되며, 브라우저는 무결성을 검증하기 위해 해시를 재계산합니다. 그 아래에 있는 세 개의 에이전트 설명 카드는 누가 무엇을 하는지에 대한 시각적 요약을 빠르게 제공합니다.
변하지 않은 것 (그리고 그 이유)
- 컨트랙트 로직 (Contract logic). 예선 라운드에서 배포된 네 개의 Odra 컨트랙트 (AgentRegistry, CarbonProjectRegistry, CarbonCreditToken, CarbonMarketplace)는 수정되지 않았습니다. 우리의 모든 수정 사항은 TypeScript 에이전트 레이어와 Next.js 대시보드에 적용되었으며, 컨트랙트는 그대로 작동합니다.
- 컴플라이언스 에이전트 (The compliance agent). 사기 행위를 자율적으로 계속 모니터링하며, 변경이 필요하지 않았습니다.
- 상태 디코더 (The state decoder). 우리가 역공학(reverse-engineered)한 Odra
state딕셔너리 디코더 (필드 인덱스의 blake2b256 + 매핑 키, 수동 bytesrepr 역직렬화 방식)는 첫날부터 문제없이 체인 상태(chain state)를 읽어오고 있습니다.
저장소 (Repository)
모든 코드는 github.com/harishkotra/casper-carbon에서 오픈 소스로 공개되어 있습니다.
git clone https://github.com/harishkotra/casper-carbon
cd casper-carbon
./demo.sh
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기