---
name: functor-agentic-wallet
description: Use when building agents, bots, or apps that need non-custodial wallets with on-chain session-key delegation. Triggers on requests involving agent wallets, scoped permissions, session keys, programmatic spending, passkey wallets, cross-agent authorization checks, or "an AI that can act on my wallet". The SDK lives at @functornetwork/agentic-wallet; this skill teaches how to wire it.
---

# Functor Agentic Wallet

Functor gives apps non-custodial smart-account wallets and a **public, on-chain registry of who is authorized to act on each wallet**. Permissions are first-class on-chain objects — any agent, app, or chain can verify them; no platform sits in the middle.

The wallet's admin key signs once to grant a scoped session; the session signs every action after. Revocation is one tx, effective immediately.

## When to reach for this skill

Recognize these patterns:

- "I want an AI agent that can trade / pay / mint on my behalf"
- "Grant this bot a $50/day spending limit on USDC"
- "How do two agents verify each other on-chain"
- "Build a wallet that recovers from a passkey"
- "Non-custodial wallet for my app, but I don't want users to handle seed phrases"
- "Revoke this key — make sure it can't sign anymore"
- "Check whether <address> is allowed to act on <wallet> right now"

If the user is operating an existing wallet from Claude itself (one-off transactions, granting sessions to other agents), prefer the **MCP server** (`@functornetwork/mcp`) — it exposes the SDK as tools/slash commands. If the user is **writing code** that uses Functor, use this SDK directly.

## The SDK: a client and its methods

Everything goes through a client. Create one with `createClient`, configured with the chains it should support, then call its methods.

```ts
import { createClient, BNB_TESTNET, SEPOLIA, BASE_SEPOLIA } from "@functornetwork/agentic-wallet";

const client = createClient({ chains: [BNB_TESTNET] });
// Wallet execution: BNB_TESTNET (default) or SEPOLIA.
// Cross-chain verification cache: BASE_SEPOLIA.
// client.createWallet        — smart account from a local private key (CLI, script, agent)
// client.createPasskeyWallet — smart account from a passkey (Face ID / Touch ID), browser
// client.execute             — run calls as wallet admin OR as a session
// client.grantSession        — admin authorizes a scoped session key on-chain
// client.revokeSession       — admin pulls authority; effect is immediate
// client.recoverFromPasskey  — browser: rebuild wallet handle from any saved passkey
// client.balances            — read native + token balances for a wallet
```

The private key lives wherever your code runs — your laptop, your agent's process, an OS keychain. **Functor never sees it.** Custody is local to the integrator.

The same wallet address is provisioned on every chain the client lists. `client.execute` accepts either an admin pair (`wallet` + `signer`) or a `session`, plus the `calls`; pass `chainId` to pick a chain (defaults to the client's first).

## Workflows

### Local wallet from a private key

```ts
import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet";

const client = createClient({ chains: [BNB_TESTNET] });

// Key is read from wherever your code keeps it — env var, OS keychain,
// encrypted file. It never leaves the process.
const signer = signerFromPrivateKey(process.env.AGENT_PRIVATE_KEY as `0x${string}`);
const wallet = await client.createWallet({ signer });

// Send 0.001 BNB. First execute also registers the admin key in Keystore —
// happens transparently inside the same userOp.
const result = await client.execute({
  wallet,
  signer,
  calls: { to: "0xRecipient...", value: 1_000_000_000_000_000n }, // 0.001 BNB in wei
});
console.log(result.status, result.transactionHash);
```

### Browser wallet with passkey

```ts
import { createClient, BNB_TESTNET } from "@functornetwork/agentic-wallet";

const client = createClient({ chains: [BNB_TESTNET] });
const wallet = await client.createPasskeyWallet({ name: "MyApp", rpId: "myapp.example" });
// `wallet.signer` is the PasskeySigner — used the same way as any other signer.
```

### Grant a session to an agent

```ts
const session = await client.grantSession({
  wallet,
  signer: adminSigner,
  permissions: {
    calls: [{ to: "0xUniswapRouter..." }],          // only this contract
    spend: [{
      limit: 100_000_000n,                          // 100 USDC (6 decimals)
      period: "day",
      token: "0xUSDC...",
    }],
  },
  expiry: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, // 7 days
});

// Hand `session` to whichever process runs the agent. Persist these fields:
//   walletAddress, publicKey, permissions, expiry, and the signer's private key
// (signer.export() if your signer is a private-key signer). The agent needs
// the exact permissions+expiry at execute time — the on-chain validator
// matches them byte-for-byte against the authorization committed at grant.
```

### Agent acts using a session

```ts
const result = await client.execute({
  session,
  calls: [{ to: "0xUniswapRouter...", data: "0x...", value: 0n }],
});
```

### Verify any key on-chain (from any tool)

```ts
import { createPublicClient, http, keccak256 } from "viem";
import { sepolia } from "viem/chains";
import { SEPOLIA } from "@functornetwork/agentic-wallet";

const publicClient = createPublicClient({ chain: sepolia, transport: http() });
const KEYSTORE_ABI = [{
  name: "getActiveKeys", type: "function", stateMutability: "view",
  inputs: [{ name: "user", type: "address" }],
  outputs: [{ type: "bytes32[]" }],
}] as const;

const active = await publicClient.readContract({
  address: SEPOLIA.keyStore,
  abi: KEYSTORE_ABI,
  functionName: "getActiveKeys",
  args: [walletAddress],
});
const authorized = active.includes(keccak256(sessionPublicKey));
```

This is the killer feature: a wallet that has never heard of your app can still verify whether a given key is authorized. No vendor lock-in.

### Revoke a session

```ts
await client.revokeSession({ wallet, signer: adminSigner, session });
// or, if you only kept the public key:
await client.revokeSession({ wallet, signer: adminSigner, session: sessionPublicKey });
```

Revocation revokes the key in Keystore **and** pulls the session's on-chain authority in the same userOp. The session's next signed call reverts at validation. Revocation is monotonic in Keystore v1.0.0 — to restore access, grant a fresh session.

### Recover a passkey wallet

```ts
const wallet = await client.recoverFromPasskey({ rpId: "myapp.example" });
// Browser shows the passkey picker; user picks one, biometric prompt, done.
// Two on-chain reads, no server, no localStorage required.
```

## When to use the MCP server vs the SDK directly

| You are… | Use |
|---|---|
| Writing TypeScript/JS code that needs wallet ops | `@functornetwork/agentic-wallet` (this SDK) |
| Operating wallets interactively from Claude Code | `@functornetwork/mcp` server, tools like `create_wallet`, `grant_session` |
| Building a UI that signs from the browser | `@functornetwork/agentic-wallet` with `client.createPasskeyWallet` |
| Running a local agent that holds its own session | `@functornetwork/agentic-wallet` with `client.execute({ session, calls })` |

The MCP server is a thin wrapper around this SDK — anything the MCP does, you can do directly with the SDK.

## Notes

- **Funding.** Fund `wallet.address` with native tokens before the first `execute`. On Sepolia, use a testnet faucet. On BNB testnet, use https://testnet.bnbchain.org/faucet-smart.
- **First execute registers the admin.** The Keystore `initialRegisterKey` is auto-prepended on the wallet's first admin-signed action. Don't pre-call it. The wallet is "live" but not on-chain until that first tx.
- **Sessions must be byte-exact on execute.** The on-chain validator matches `permissions + expiry + role + publicKey` exactly to the hash committed at grant time. Re-serializing through a sloppy JSON path (bigints → number, period reordering) breaks the match. Persist the `Session` object verbatim or reconstruct it identically.
- **Empty calls means no calls.** `client.execute({ wallet, signer, calls: [] })` is rejected. Pass at least one call.
- **`permissions.calls` omitted = unrestricted.** If you don't pass `calls`, the session can call any contract within its spend cap. Set both unless that's truly what you want.
- **Pick chains at the client.** `createClient({ chains })` takes one or more chains; the same wallet address works on all of them. Select per operation with `chainId`.

## Networks

```ts
import { SEPOLIA, BNB_TESTNET, BASE_SEPOLIA } from "@functornetwork/agentic-wallet";
// SEPOLIA      — Ethereum Sepolia (chain 11155111), L1 Keystore source of truth
// BNB_TESTNET  — BNB Smart Chain Testnet (chain 97)
// BASE_SEPOLIA — Base Sepolia (chain 84532), L2 Keystore cache
//
// SEPOLIA.keyStore           = 0xfBDe00E03Bf281bAc666043B14dBb8FAbcf22b14 (v1.0.0)
// SEPOLIA.keyStoreController = 0x8bBabE825EcFCBB32f7B60D973FbA0B923b8e782 (v1.0.0)
```

## What never changes

- Functor never sees the private key. Custody follows the signer the integrator brings.
- Every authorization is on-chain in Keystore — readable by any tool, on any chain that bridges to it.
- Revoke is one tx, immediate.
