Delegated payments
Pouchi Guard covers delegation and limits. A delegation is one delegate’s signature plus limits the wallet enforces. On the approval screen the user sees the delegate and the limits, and makes one owner signature. After that the delegate signs with its own key to run calls inside the limits, and the wallet contract checks the delegate’s signature and the limits again.
Pouchi does not sign delegated runs, does not hold delegate keys, and can never be a delegate. Software that only requests runs (agents, scheduled jobs, buttons) holds no key and only asks the delegate.
Two ways to run
Section titled “Two ways to run”| Way | Who signs | When to use |
|---|---|---|
| Owner run (approve each call) | The user (two key shares) | Calls the user must check every time. useSendTransaction, the server’s proposeTransaction |
| Delegated run | The delegate (one key) | Repeating the same kind of call inside limits. Subscriptions, games, agent payments |
Approving each call is not a delegation. It is the normal run where the user makes an owner signature for that one call.
Public agent demo
Section titled “Public agent demo”The hosted testnet wallet MCP is available through the single URL in Connect an AI agent, including the bundled shopping tools. It does not require installing the PC wallet product described below. The PC wallet product and the tenant SDK function for creating a new server delegation are separate items still in preparation. For your own service or tenant deployment, request access and setup details from business@pouchi.app.
Delegate types
Section titled “Delegate types”A delegation names its delegate in one of two ways: a key address (key delegate) or a tenant registry entry (tenant delegate).
| Delegate | Named as | Key held by | Used for | SDK |
|---|---|---|---|---|
| Browser session key | Key address | The user (this browser tab) | Repeated actions inside the app | useSessionApproval (available) |
| Pouchi Wallet MCP | Key address | The user (the user’s PC) | AI agent payments on the user’s PC | In preparation |
| Tenant server | Tenant registry entry | The tenant (KMS, HSM) | Subscriptions, scheduled runs, remote MCP | executeDelegatedPolicy (available); the function that creates the delegation is in preparation |
When the delegation names a tenant registry entry, the tenant can rotate its key (rotateDeploymentSigner) without the user approving again. Suspending the tenant key stops only delegations where that tenant is the delegate. The user’s own runs and delegations that name a key address are not affected.
Limits
Section titled “Limits”The limits the user signs go into the wallet contract, and the contract checks them on every run. App code and the Pouchi server cannot raise them.
| Limit | Meaning |
|---|---|
| Target and function | Contract address (with code hash) and function selector. For native transfers, the recipient |
| Amount | Maximum per run, maximum in total |
| Runs | Remaining number of runs |
| Period | Start and end time |
| Window limit | Runs and amount per fixed-length window (subscriptions) |
| Maximum gas per run | Maximum gas allowed for execution. The app pays; the user’s wallet is not charged |
The user can turn a delegation off at any time with an owner signature. If a delegate key leaks, it can be used up to that delegation’s remaining limits, so small limits and short periods are recommended.
In-app session keys
Section titled “In-app session keys”useSessionApproval creates a new key in this browser tab and asks the user to approve a delegation whose delegate is that key address. The user enters user authentication (passkey, PIN, etc.) once; after that, calls inside the session run with one confirm tap. The session key never leaves the tab, and Pouchi never sees it.
import { useSendTransaction, useSessionApproval } from "@shard-lab/nc-waas-react";import { encodeFunctionData, parseAbi, parseEther } from "viem";
const GAME = import.meta.env.VITE_GAME_CONTRACT as `0x${string}`;const abi = parseAbi(["function play(uint256 round) payable"]);
export function SessionPlay() { const { session, active, startSession, endSession } = useSessionApproval(); const { sendTransaction } = useSendTransaction();
// One PIN or biometric approval: this function only, at most 0.05 OKRW in 10 calls, for 1 hour. const start = () => startSession({ durationSeconds: 60 * 60, maxTotalValueWei: parseEther("0.05").toString(), maxValuePerCallWei: parseEther("0.005").toString(), maxCalls: 10, contracts: [{ address: GAME, functions: ["play(uint256)"] }] });
// Inside the session this is a one-tap confirmation; outside it, a normal approval. const play = (round: number) => sendTransaction({ to: GAME, value: parseEther("0.005"), data: encodeFunctionData({ abi, functionName: "play", args: [BigInt(round)] }) });
if (!active) return <button onClick={start}>1시간 세션 시작</button>; return ( <div> <p>남은 횟수 {session?.callsRemaining}, 만료 {new Date(session!.expiresAt).toLocaleTimeString()}</p> <button onClick={() => play(1)}>플레이</button> <button onClick={endSession}>세션 끝내기</button> </div> );}import type { ReactNode } from "react";import { NcWaasProvider } from "@shard-lab/nc-waas-react";import { marooTestnet } from "./chain";
export function Providers({ children }: { children: ReactNode }) { return ( <NcWaasProvider apiUrl={import.meta.env.VITE_POUCHI_API_URL} tenantId={import.meta.env.VITE_POUCHI_TENANT_ID} chain={marooTestnet} config={{ loginMethods: ["email"], // The client the tenant registered for this app (web channel). approvals: { session: { clientId: import.meta.env.VITE_POUCHI_SESSION_CLIENT_ID } } }} > {children} </NcWaasProvider> );}| Item | Value |
|---|---|
| Required setting | config.approvals.session.clientId: the web client ID the tenant registered for this app |
startSession(input) |
durationSeconds (60 seconds to 24 hours, default 1 hour), maxTotalValueWei, maxValuePerCallWei, maxCalls (1 to 1,000), contracts ({ address, functions }), nativeRecipients |
| Scope | Up to 16 functions. A function is a selector (0x12345678) or a signature (play(uint256)) |
| Functions not allowed | Functions that move or allow tokens, such as transfer, transferFrom, approve, permit, setApprovalForAll. A session does not restrict arguments, so these calls are approved each time |
| Returns | { session, active, startSession, endSession }. session has expiresAt, callsRemaining, spentWei, valueAllowanceWei, permissions |
useSendTransaction runs calls that fit the session with the session key automatically. Calls outside the session (another function, over the limit, expired) open the owner approval screen as usual.
Tenant server delegation
Section titled “Tenant server delegation”To run when the user is not in the app, such as a subscription, make the tenant server the delegate. The server runs with executeDelegatedPolicy from @nc-waas/tenant-server-sdk. Pouchi prepares the exact run and returns a digest, the server’s delegate key signs it, and Pouchi submits it. Even if the server is compromised, requests outside the limits fail on chain.
// Tenant server: run one payment inside a delegation whose delegate is this server.// The wallet contract checks this server's signature and every limit the user signed.import { executeDelegatedPolicy, type PolicyDelegateSigner } from "@nc-waas/tenant-server-sdk";import { encodeFunctionData, parseAbi, parseUnits, type Address, type Hex } from "viem";
const erc20 = parseAbi(["function transfer(address to, uint256 amount) returns (bool)"]);
// Production: a KMS or HSM key that signs digests. The private key never leaves it.declare function kmsSignDigest(keyId: string, digest: Hex): Promise<Hex>;const signer: PolicyDelegateSigner = { address: process.env.DELEGATE_ADDRESS as Address, signDigest: (digest) => kmsSignDigest(process.env.DELEGATE_KMS_KEY_ID!, digest)};
export async function chargeMonthly(input: { connectionToken: string; policyId: string; invoiceId: string; merchant: Address }) { const result = await executeDelegatedPolicy({ apiUrl: process.env.POUCHI_API_URL!, connectionToken: input.connectionToken, // app connection token with scope policy:execute policyId: input.policyId, requestId: `invoice-${input.invoiceId}`, // stable per payment: a retry never pays twice calls: [{ target: process.env.TOKEN_ADDRESS as Address, data: encodeFunctionData({ abi: erc20, functionName: "transfer", args: [input.merchant, parseUnits("9900", 18)] }) }], signer }); if (!result.ok) throw new Error(`charge ${result.status}: ${result.code ?? ""} ${result.failureReason ?? ""}`); return result.transactionHash;}| Input | Meaning |
|---|---|
apiUrl |
Pouchi API URL |
connectionToken |
App connection token for the user’s wallet (scope policy:execute) |
policyId |
ID of the delegation the user approved |
requestId |
An ID fixed per payment. Sending the same ID again does not charge twice |
calls |
{ target, value?, data }[]. Must be inside the delegation’s targets and functions |
signer |
PolicyDelegateSigner ({ address, signDigest(digest) }). KMS or HSM in production |
responseMode |
"completed" (wait for the chain result) or "submitted" |
The result is { ok, status, operationId?, transactionHash?, userOperationHash?, failureReason?, code? }.
Agents on the user’s PC
Section titled “Agents on the user’s PC”When the user installs the Pouchi Wallet MCP on their own PC, the delegate key is created on that PC and never leaves it. The approval screen shows it as “Pouchi Wallet MCP on this PC”, and the delegate is the user. The tools are wallet_status, pay_intent, request_budget, payment_status and revoke_mandate. The product release is in preparation.
The developer MCP (docs and examples) for people writing integration code is a different tool. AI development tools