빠른 시작
등록된 앱 ID와 허용 출처를 준비한 뒤, 앱 구성에 맞는 연동 방식을 선택합니다. 현재 예제는 Maroo 테스트넷(chainId 450815) 전용입니다.
웹 스크립트
섹션 제목: “웹 스크립트”웹사이트의 HTML에 아래 한 줄을 추가하면 로그인 버튼이 생성됩니다. tenant_your_app을 발급받은 앱 ID로 바꿉니다. React나 npm 설치는 필요하지 않습니다.
<script defer src="https://pouchi-wallet.vercel.app/sdk/v1/pouchi.js" data-app-id="tenant_your_app"></script>로그인 버튼과 별도로 결제 요청 API를 연결해야 합니다. 웹 스크립트 SDK에서 앱 등록, 허용 출처, 로그인 상태와 결제 요청 순서로 진행하세요. 앱 ID가 없다면 business@pouchi.app으로 앱 이름과 개발·배포 출처를 알려 주세요.
React SDK
섹션 제목: “React SDK”React와 TypeScript 앱에서 Provider와 훅을 사용하려면 아래 단계로 진행합니다. 패키지 접근 권한이 필요합니다. 공개 npm 베타는 준비 중입니다. 에이전트는 AI 에이전트 연결, 기존 Privy 앱은 이전 안내를 확인하세요.
React 시작 프로젝트 받기에는 아래 코드와 Vite·TypeScript 설정이 들어 있습니다. SDK, 토큰과 앱 ID는 포함하지 않습니다. 압축을 풀고 README의 접근 권한·설정 안내를 먼저 따라야 합니다. 파일을 직접 구성하려면 아래 단계로 진행하세요.
연동 준비
섹션 제목: “연동 준비”예제는 Maroo 테스트넷(chainId 450815) 전용입니다. Node.js 22.12 이상과 npm이 필요합니다. 기존 React 앱을 사용하거나 아래 명령으로 빈 앱을 만듭니다.
npm create vite@latest my-pouchi-app -- --template react-tscd my-pouchi-appnpm install먼저 business@pouchi.app으로 앱 등록과 패키지 읽기 권한을 요청하세요. 앱 이름, 개발 출처와 배포 출처를 전달하고 아래 항목을 받거나 확인합니다.
| 필요한 항목 | 확인 내용 |
|---|---|
| SDK 접근 | shard-lab GitHub Packages의 패키지 읽기 권한 |
| 앱 설정 | 앱 ID와 경로가 붙지 않은 HTTPS API 주소 |
| 허용 출처 | 실제 브라우저 주소의 프로토콜·호스트·포트와 일치. localhost와 127.0.0.1은 서로 다름 |
| 체인 | Maroo 테스트넷, chainId 450815 |
| 가스 대납 | 앱의 대납 설정, 예산과 건당 한도. 이용자 지갑의 가스비 차감 없이 실행 가능한지 확인 |
| 테스트 자금 | 로그인 후 표시될 지갑 주소에 넣을 네이티브 테스트 OKRW의 지급 방법 |
이 값은 다른 데모 앱에서 복사하지 않습니다. 앱 ID와 API 주소를 받지 않았다면 다음 단계로 진행하지 마세요.
SDK 설치
섹션 제목: “SDK 설치”프로젝트 루트에 .npmrc를 만듭니다. 아래 환경 변수 이름을 그대로 두고 실제 토큰은 파일에 적지 않습니다.
@shard-lab:registry=https://npm.pkg.github.com//npm.pkg.github.com/:_authToken=${NC_WAAS_PACKAGES_TOKEN}패키지 읽기 권한이 있는 GitHub 토큰을 터미널에서 입력합니다. bash 또는 zsh에서 아래 명령으로 입력하면 값이 화면이나 셸 기록에 남지 않습니다.
printf 'GitHub Packages token: 'read -r -s NC_WAAS_PACKAGES_TOKENprintf '\n'export NC_WAAS_PACKAGES_TOKENnpm install @shard-lab/nc-waas-react viemread:packages 범위 외에 해당 조직의 패키지 접근 권한도 필요합니다. 401·403·404가 나면 토큰을 문서나 이메일에 붙이지 말고 조직의 접근 권한을 문의하세요. 사용한 터미널 세션이 끝나면 unset NC_WAAS_PACKAGES_TOKEN으로 환경 변수를 지울 수 있습니다.
앱 설정
섹션 제목: “앱 설정”프로젝트 루트의 .env.local에 발급받은 두 값을 넣습니다. VITE_ 값은 브라우저에 공개되므로 서버 비밀 키를 넣지 않습니다.
VITE_POUCHI_API_URL=YOUR_POUCHI_API_URLVITE_POUCHI_TENANT_ID=YOUR_POUCHI_APP_ID.env.local과 패키지 토큰은 Git에 커밋하지 않습니다. 앱 ID·API 주소만 공개 설정이며 clientSecret은 여기에 넣지 않습니다.
React 연결
섹션 제목: “React 연결”chain.ts, providers.tsx, App.tsx는 src/에, vite.config.ts와 pouchi-vite.ts는 프로젝트 루트에 만듭니다. Vite 설정은 개발 서버에서 SDK의 서명용 WASM을 원본 그대로 제공하기 위해 필요합니다. 파일 무결성 검사는 유지합니다. 양 언어가 같은 예제를 쓰며 버튼 문구는 영어입니다.
import { defineChain } from "viem";
export const marooTestnet = defineChain({ id: 450815, name: "Maroo Testnet", nativeCurrency: { name: "OKRW", symbol: "OKRW", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc-testnet.maroo.io"] } }, testnet: true});import type { ReactNode } from "react";import { PrivyProvider } from "@shard-lab/nc-waas-react";import { marooTestnet } from "./chain";
export function Providers({ children }: { children: ReactNode }) { return ( <PrivyProvider apiUrl={import.meta.env.VITE_POUCHI_API_URL} appId={import.meta.env.VITE_POUCHI_TENANT_ID} config={{ loginMethods: ["email"], appearance: { theme: "auto", appName: "My App" }, defaultChain: marooTestnet, supportedChains: [marooTestnet] }} > {children} </PrivyProvider> );}import { useRef, useState } from "react";import { NcWaasError, usePrivy, useSendTransaction, useWallets } from "@shard-lab/nc-waas-react";import { isAddress, parseEther } from "viem";
export function App() { const { ready, authenticated, user, login, logout } = usePrivy(); const { wallets } = useWallets(); const { sendTransaction } = useSendTransaction(); const [recipient, setRecipient] = useState(""); const [pending, setPending] = useState(false); const [error, setError] = useState(""); const [hash, setHash] = useState(""); const sending = useRef(false);
if (!ready) return <p>Loading wallet...</p>; if (!authenticated) return <button onClick={login}>Sign in with Pouchi</button>;
const send = async () => { if (sending.current) return; setError(""); setHash(""); const to = recipient.trim(); if (!isAddress(to)) { setError("Enter a valid recipient address."); return; } if (!wallets[0]) { setError("Wait for your wallet to be ready."); return; } sending.current = true; setPending(true); try { const result = await sendTransaction({ to, value: parseEther("0.01") }); setHash(result.hash); } catch (cause) { setError(cause instanceof NcWaasError && cause.code === "USER_REJECTED" ? "Transfer cancelled." : cause instanceof Error ? cause.message : "Transfer failed. Try again."); } finally { sending.current = false; setPending(false); } };
return ( <main> <p>{user?.email?.address}</p> <p>Wallet: {wallets[0]?.address ?? "Preparing wallet..."}</p> <label> Test recipient address <input value={recipient} onChange={(event) => setRecipient(event.target.value)} placeholder="0x..." autoComplete="off" disabled={pending} /> </label> <p>Send 0.01 test OKRW on Maroo Testnet. Check the recipient before approving.</p> <button disabled={pending || !wallets[0]} onClick={() => void send()}> {pending ? "Waiting for approval..." : "Send 0.01 OKRW"} </button> {error && <p role="alert">{error}</p>} {hash && <p role="status">Transaction hash: <code>{hash}</code></p>} <button disabled={pending} onClick={() => { void logout().catch(() => setError("Sign out failed.")); }}>Sign out</button> </main> );}import { defineConfig } from "vite";import react from "@vitejs/plugin-react";import { pouchiWasm } from "./pouchi-vite";export default defineConfig({ plugins: [react(), pouchiWasm()] });import { createReadStream } from "node:fs";import { createRequire } from "node:module";import { dirname, join } from "node:path";import type { Plugin } from "vite";
// SDK 0.1.0 keeps an integrity-checked WASM URL relative to its module.// Vite dev prebundling moves the module; serve the original bytes at that URL.// Production builds emit the asset normally. Integrity checks remain enabled.export function pouchiWasm(): Plugin { return { name: "pouchi-wasm-dev", configureServer(dev) { const reactEntry = createRequire(import.meta.url).resolve("@shard-lab/nc-waas-react"); const sdkEntry = createRequire(reactEntry).resolve("@shard-lab/nc-waas-sdk/browser-assets"); const relative = "generated/distributed-dkls/browser/dkls_client_bg.wasm"; const file = join(dirname(sdkEntry), relative); dev.middlewares.use((req, res, next) => { if (!req.url?.split("?")[0].endsWith(`/${relative}`)) return next(); if (req.method !== "GET" && req.method !== "HEAD") return next(); res.setHeader("content-type", "application/wasm"); if (req.method === "HEAD") { res.end(); return; } const stream = createReadStream(file); stream.on("error", () => { res.statusCode = 500; res.end("Pouchi WASM asset unavailable. Check the SDK installation."); }); stream.pipe(res); }); } };}앱 진입점을 아래처럼 바꿉니다. Vite 클라이언트 타입 설정(types: ["vite/client"] 또는 기존 src/vite-env.d.ts)과 CSS import는 유지하세요.
import { StrictMode } from "react";import { createRoot } from "react-dom/client";import { Providers } from "./providers";import { App } from "./App";import "./index.css";
createRoot(document.getElementById("root")!).render( <StrictMode><Providers><App /></Providers></StrictMode>);PrivyProvider와 NcWaasProvider는 같은 Provider의 이름입니다. 이 예제에서는 apiUrl, appId, config.defaultChain을 사용합니다.
첫 로그인과 송금
섹션 제목: “첫 로그인과 송금”앱에 등록한 출처와 같은 주소로 개발 서버를 실행합니다. 예를 들어 http://localhost:5173을 등록했다면 다음처럼 실행하고 그 주소로 접속합니다. 포트가 사용 중이면 자동으로 다른 포트로 옮기지 않고 종료합니다.
npm run dev -- --host localhost --port 5173 --strictPort- Sign in with Pouchi을 누르고 이메일 코드를 입력합니다. 첫 가입 때 Pouchi 화면의 지갑 보호 설정을 마칩니다. 패스키만으로 새로 가입하지 않습니다.
- 화면에 나온 지갑 주소와 Maroo 테스트넷을 확인합니다. 아직 잔액이 없다면 발급 담당자에게 이 공개 주소로 네이티브 테스트 OKRW를 요청합니다. 토큰·PIN·복구 코드는 보내지 않습니다.
- 받을 테스트 지갑 주소를 입력합니다. 처음부터 고정된 받는 주소는 없으며, 스스로 관리하는 테스트 주소를 사용합니다. 다른 체인이나 실제 자산을 사용하지 않습니다.
- Send 0.01 OKRW를 누릅니다. 승인 화면에서 받는 주소와 0.01 OKRW, 앱이 부담하는 가스비를 확인한 뒤 직접 승인합니다. 대납이 안 되면 계속 진행하지 말고 앱 설정을 확인하세요.
- 요청 중에는 버튼이 잠깁니다. 취소·실패는 화면에 표시되고, 제출되면 거래 해시가 표시됩니다. 해시를 받았다는 것만으로 체인 확정이 된 것은 아닙니다. 테스트넷 탐색기나 체인 조회로 영수증의 성공 상태를 확인합니다.
이 송금은 네이티브 테스트 OKRW 전송입니다. 쇼핑 데모가 쓰는 별도 결제 토큰과 바꾸어 생각하지 마세요. 지갑의 송금액과 앱의 가스 대납 예산은 별개입니다.
완료 확인
섹션 제목: “완료 확인”| 단계 | 확인할 결과 |
|---|---|
| 로그인 | 로그인한 이메일과 현재 앱의 지갑 주소 표시 |
| 송금 전 | 지갑에 0.01 OKRW 이상, 앱 대납 설정과 예산 준비 |
| 승인 | Pouchi 화면의 주소·금액·수수료 확인 |
| 제출 | 앱 화면에 거래 해시 표시 |
| 확정 | 같은 Maroo 테스트넷에서 영수증 성공과 받는 주소·금액 확인 |
Vite 설정을 빠뜨리면 개발 중 verified DKG asset hash mismatch 오류가 날 수 있습니다. 이 경우 개발 서버의 WASM 요청이 HTML이 아닌 application/wasm으로 응답하는지 확인하고, 위 설정을 유지한 뒤 Vite를 다시 실행하세요. 무결성 검사를 끄지 않습니다.
로그인 수단
섹션 제목: “로그인 수단”기본은 이메일 코드(loginMethods: ["email"])입니다.
| 수단 | 상태 |
|---|---|
email |
기본. 설정 없이 동작 |
google, apple, kakao, naver |
앱 설정에서 켜고, 돌아올 페이지(config.oauth.redirectUrl, 기본은 현재 페이지)를 OAuth redirect URI로 등록해야 보입니다 |
passkey |
로그인만. 가입은 이메일로 하고 linkPasskey()로 추가합니다 |
wallet, sms |
지원하지 않음 (설정 호환을 위해 받기만 하고 숨김) |
서버 연동
섹션 제목: “서버 연동”네이티브 송금에는 앱의 제안 백엔드가 필요하지 않습니다. 실행 가능한 Node 라우트, JWT 검증과 선택 기능인 컨트랙트 호출 제안은 서버 연동에서 이어서 구현합니다.
viem과 wagmi
섹션 제목: “viem과 wagmi”지갑은 EIP-1193 provider를 줍니다. viem custom()과 wagmi 커넥터를 그대로 씁니다.
import { createWalletClient, custom } from "viem";
const provider = await wallets[0].getEthereumProvider();const client = createWalletClient({ account: wallets[0].address, chain: marooTestnet, transport: custom(provider) });await client.sendTransaction({ to, value });이 탭을 사용할 때 먼저 npm install wagmi @tanstack/react-query로 선택 의존성을 설치합니다.
import { createConfig, http } from "wagmi";import { ncWaasConnector } from "@shard-lab/nc-waas-react/wagmi";
const config = createConfig({ chains: [marooTestnet], connectors: [ncWaasConnector()], transports: { [marooTestnet.id]: http() } });MCP 에이전트 연결
섹션 제목: “MCP 에이전트 연결”앱을 만들지 않고 AI 에이전트에 결제를 맡기려면 지갑 MCP를 연결합니다. 연결 순서, 클라이언트별 설정, 오류 해결은 AI 에이전트 연결에 있습니다.
자주 겪는 오류
섹션 제목: “자주 겪는 오류”SDK 오류는 NcWaasError입니다. code로 종류를 구분하고, API에서 온 오류는 apiCode에 서버 코드가 있습니다. EIP-1193 숫자 코드는 rpcCode에 있습니다.
import { NcWaasError } from "@shard-lab/nc-waas-react";
try { await sendTransaction({ to, value });} catch (error) { if (error instanceof NcWaasError && error.code === "USER_REJECTED") return; // 이용자가 닫음 throw error;}| 증상 | 원인 | 해결 |
|---|---|---|
npm install이 401·403·404로 실패 |
레지스트리 설정, 토큰 범위, 조직의 패키지 접근 권한 또는 해당 버전 제공 여부 | .npmrc와 NC_WAAS_PACKAGES_TOKEN을 확인. read:packages가 있어도 404라면 담당자에게 정확한 패키지 이름·버전과 접근 권한을 확인 |
INVALID_PARAMS: NcWaasProvider needs tenantId (or appId) |
appId가 비어 있음. 보통 환경 변수가 읽히지 않은 경우 |
.env.local의 변수 이름(VITE_ 접두사)을 확인하고 개발 서버를 다시 시작 |
INVALID_PARAMS: apiUrl must be an absolute URL 또는 apiUrl must be an HTTPS origin (HTTP only on loopback) without a path |
apiUrl이 비었거나, 경로가 붙었거나, 로컬이 아닌데 http임 |
https://api.example.com처럼 경로 없는 HTTPS 주소를 넣음. http는 localhost, 127.0.0.1에서만 |
INVALID_PARAMS: NcWaasProvider needs chain (or config.defaultChain) |
체인을 넘기지 않음 | config.defaultChain이나 chain에 marooTestnet을 넣음 |
UNSUPPORTED_CHAIN: defaultChain ... differs from ... |
chain과 config.defaultChain이 서로 다름 |
둘 중 하나만 쓰거나 같은 체인으로 맞춤 |
| 브라우저 콘솔에 API 요청의 CORS 오류 | 앱 출처가 앱 설정의 허용 출처(applicationOrigins)에 없음 |
포트까지 같은 출처(예: http://localhost:5173)를 등록 |
NOT_AUTHENTICATED: sign in first |
로그인 전에 송금이나 서명을 호출함 | ready && authenticated일 때만 호출 |
NOT_READY: another transaction is waiting for approval |
앞의 승인이 아직 열려 있음 | 앞의 sendTransaction이 끝난 뒤 다시 호출 |
USER_REJECTED (rpcCode 4001) |
이용자가 승인 화면을 닫거나 거절함 | 취소로 처리. 다시 시도할 버튼을 보여 줌 |
API_ERROR, apiCode가 SPONSORSHIP_OPERATION_LIMIT |
가스 대납의 건당 한도가 실제 가스비보다 작음 | 콘솔에서 건당 한도를 올림. 실행하려는 작업의 현재 가스 추정값을 확인. 고정 금액으로 가정하지 않음 |
다음 단계
섹션 제목: “다음 단계”- 위임 결제: 한 번 승인받고 한도 안에서 반복 실행하기
- AI 에이전트 연결: MCP 클라이언트의 에이전트가 한도 안에서 결제하기
- AI 개발 도구 연동: 코딩 에이전트가 이 과정을 대신 하게 하기
- 보안 모델: 키 조각과 복구 방식
- React SDK 레퍼런스: 주요 공개 API와 지원 상태