Quickstart
Prepare a registered app ID and allowed origins, then choose the integration for your app. Current examples use Maroo Testnet (chainId 450815).
Web script
Section titled “Web script”Add the following line to your website HTML to create a sign-in button. Replace tenant_your_app with your issued app ID. React and npm installation are not required.
<script defer src="https://pouchi-wallet.vercel.app/sdk/v1/pouchi.js" data-app-id="tenant_your_app"></script>Connect payment requests separately through the API. Follow the web script SDK for app registration, allowed origins, session state and payment requests. For an app ID, email business@pouchi.app with your app name and development and deployment origins.
React SDK
Section titled “React SDK”Continue below for Provider and hook integration in a React TypeScript app. Package access is required. A public npm beta is in preparation. For agents, see AI agent connection. For existing Privy apps, see migration.
Download the React starter for the same example source with Vite and TypeScript configuration. It contains no SDK, token or app ID. Follow its README for access and app prerequisites first. To assemble the project yourself, continue below.
Integration prerequisites
Section titled “Integration prerequisites”This example is for Maroo Testnet (chainId 450815). Use Node.js 22.12 or later and npm. Start with an existing React app or create an empty one:
npm create vite@latest my-pouchi-app -- --template react-tscd my-pouchi-appnpm installFirst email business@pouchi.app to request app registration and package read access. Include your app name and development and deployment origins. Obtain or confirm each item below.
| Requirement | Check |
|---|---|
| SDK access | Read access to the shard-lab package on GitHub Packages |
| App settings | App ID and an HTTPS API origin without a path |
| Allowed origins | Exact browser scheme, host and port. localhost and 127.0.0.1 are different |
| Chain | Maroo Testnet, chainId 450815 |
| Gas sponsorship | App sponsorship settings, budget and per-operation cap. Confirm execution does not charge gas to the user wallet |
| Test funds | How to obtain native test OKRW for the wallet address shown after sign-in |
Do not copy app settings from another demo. Stop here until your app ID and API URL have been issued.
SDK installation
Section titled “SDK installation”Create .npmrc at the project root. Keep the environment variable literal; do not put a real token in this file.
@shard-lab:registry=https://npm.pkg.github.com//npm.pkg.github.com/:_authToken=${NC_WAAS_PACKAGES_TOKEN}Enter a GitHub token with package read permission in the terminal. In bash or zsh, these commands keep the entered value off the screen and out of shell history.
printf 'GitHub Packages token: 'read -r -s NC_WAAS_PACKAGES_TOKENprintf '\n'export NC_WAAS_PACKAGES_TOKENnpm install @shard-lab/nc-waas-react viemThe token needs read:packages and access to this organization’s package. For 401, 403 or 404, ask about package access without pasting the token into documentation or email. Use unset NC_WAAS_PACKAGES_TOKEN when finished with this terminal session.
App configuration
Section titled “App configuration”Put the two issued values in .env.local at the project root. VITE_ variables are public browser configuration; never use them for server secrets.
VITE_POUCHI_API_URL=YOUR_POUCHI_API_URLVITE_POUCHI_TENANT_ID=YOUR_POUCHI_APP_IDDo not commit .env.local or package tokens. App ID and API URL are public settings; clientSecret does not belong here.
React integration
Section titled “React integration”Create chain.ts, providers.tsx and App.tsx in src/, and vite.config.ts and pouchi-vite.ts at the project root. The Vite setup serves the original signing WASM bytes during development and keeps integrity checks enabled. Both guides use the same example with English UI labels.
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); }); } };}Use this entry point. Keep Vite’s client type setup (types: ["vite/client"] or an existing src/vite-env.d.ts) and your 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 and NcWaasProvider are names for the same provider. This example uses apiUrl, appId and config.defaultChain.
First sign-in and transfer
Section titled “First sign-in and transfer”Run the dev server at the exact registered origin. If you registered http://localhost:5173, run the following and open that address. An occupied port stops the server instead of silently changing the origin.
npm run dev -- --host localhost --port 5173 --strictPort- Press Sign in with Pouchi, enter the email code and finish wallet protection in the Pouchi screen. First-time sign-up is not passkey-only.
- Check the displayed wallet address and Maroo Testnet. If the wallet has no balance, ask the access contact for native test OKRW at this public address. Do not send tokens, PINs or recovery codes.
- Enter a test recipient address that you control. No executable destination is prefilled. Do not use another chain or real assets.
- Press Send 0.01 OKRW. Check the recipient, 0.01 OKRW and app-paid gas on the approval screen, then approve it yourself. If sponsorship is unavailable, stop and check app settings.
- The button is disabled while pending. Cancellation or failure is shown in the app; submission displays a transaction hash. A returned hash alone does not prove chain confirmation. Use the testnet explorer or a chain query to check a successful receipt.
This transfers native test OKRW. Do not confuse it with a separate payment token used by a shopping demo. The wallet’s transfer amount and the app’s gas sponsorship budget are separate.
Completion checklist
Section titled “Completion checklist”| Step | Expected result |
|---|---|
| Sign-in | Email and a wallet address for the current app appear |
| Before transfer | At least 0.01 OKRW in the wallet; sponsorship settings and budget ready |
| Approval | Confirm recipient, amount and fee in the Pouchi screen |
| Submission | The app displays a transaction hash |
| Confirmation | Successful receipt on the same Maroo Testnet, with matching recipient and amount |
Without the Vite setup, development can fail with verified DKG asset hash mismatch. Confirm the WASM request returns application/wasm rather than HTML, preserve the setup above and restart Vite. Do not disable integrity checks.
Login methods
Section titled “Login methods”The default is an email code (loginMethods: ["email"]).
| Method | Status |
|---|---|
email |
Default. Works without setup |
google, apple, kakao, naver |
Shown only after they are turned on in the app settings and you register the return page (config.oauth.redirectUrl, the current page by default) as an OAuth redirect URI |
passkey |
Login only. Sign up with email, then add one with linkPasskey() |
wallet, sms |
Not supported (accepted for config compatibility, then hidden) |
Server integration
Section titled “Server integration”Native transfers work without your own proposal backend. For a runnable Node route, JWT verification and the optional contract-call proposer, see server integration.
viem and wagmi
Section titled “viem and wagmi”The wallet exposes an EIP-1193 provider, so viem custom() and the wagmi connector work as usual.
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 });Install the optional dependency first: 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 agent connection
Section titled “MCP agent connection”To let an AI agent pay without building an app, connect the wallet MCP. The connection steps, per-client settings and error fixes are in Connect an AI agent.
Common errors
Section titled “Common errors”SDK errors are NcWaasError. Use code to tell them apart; errors from the API carry the server code in apiCode. The EIP-1193 numeric code is in 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; // the user closed it throw error;}| Symptom | Cause | Fix |
|---|---|---|
npm install fails with 401, 403 or 404 |
Registry configuration, token scope, organization package access or unavailable version | Check .npmrc and NC_WAAS_PACKAGES_TOKEN. If 404 persists with read:packages, ask the issuer to confirm the exact package name, version and access |
INVALID_PARAMS: NcWaasProvider needs tenantId (or appId) |
appId is empty, usually because the environment variable was not read |
Check the variable name (VITE_ prefix) in .env.local and restart the dev server |
INVALID_PARAMS: apiUrl must be an absolute URL or apiUrl must be an HTTPS origin (HTTP only on loopback) without a path |
apiUrl is empty, has a path, or uses http on a non-local host |
Use an HTTPS origin without a path, such as https://api.example.com. http works only on localhost and 127.0.0.1 |
INVALID_PARAMS: NcWaasProvider needs chain (or config.defaultChain) |
No chain was passed | Pass marooTestnet as config.defaultChain or chain |
UNSUPPORTED_CHAIN: defaultChain ... differs from ... |
chain and config.defaultChain disagree |
Use one of them, or make them the same chain |
| CORS error for API requests in the browser console | The app origin is not in the app’s allowed origins (applicationOrigins) |
Register the exact origin, port included (for example http://localhost:5173) |
NOT_AUTHENTICATED: sign in first |
A transfer or signature was called before sign-in | Call only when ready && authenticated |
NOT_READY: another transaction is waiting for approval |
An earlier approval is still open | Call again after the earlier sendTransaction settles |
USER_REJECTED (rpcCode 4001) |
The user closed or declined the approval screen | Treat it as a cancel and offer a retry |
API_ERROR with apiCode SPONSORSHIP_OPERATION_LIMIT |
The per-operation gas sponsorship limit is below the real gas cost | Raise the per-operation limit in the console. Use the current gas estimate for the operation; do not hardcode a cost |
Next steps
Section titled “Next steps”- Delegated payments: get one approval, then repeat within limits
- Connect an AI agent: agents in an MCP client pay within limits
- AI development tools: let a coding agent do these steps for you
- Security model: key shares and recovery
- React SDK reference: selected public APIs and support status