# Delegated payments

The user signs one delegate and its limits once, and that delegate runs calls inside the limits. In-app session keys and tenant server delegation.

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

| 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

The hosted testnet wallet MCP is available through the single URL in [Connect an AI agent](/en/docs/agents/), 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](mailto:business@pouchi.app).

## 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

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

`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.

  **Component**

src/SessionPlay.tsx

```tsx
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>
  );
}
```

  **Provider**

src/providers.tsx

```tsx
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

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.

server/charge.ts

```ts
// 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? }`.

**In preparation**

- The `@nc-waas/tenant-server-sdk` package release is in preparation. For now it lives in `packages/tenant-server-sdk` in the nc-waas repository.
- The server SDK function that creates a delegation with the tenant server as delegate and returns an app connection token is in preparation. The user approves after seeing the delegate name and limits on the Pouchi approval screen.
- `createLocalPolicyDelegateSigner` is for local development. Do not keep production keys in process memory.

## 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](/en/docs/ai/)
