# Functor > Non-custodial agentic wallets with onchain session-key delegation. ## Acknowledgments Functor Keystore sits on top of wallet infrastructure: smart accounts, session keys, relayers, validator hooks. For the EVM ecosystem, [**Porto Wallet**](https://porto.sh) is the best implementation of wallet infrastructure we've tested, and the Functor SDK was built on top of it with our own modifications to have Keystore compatibility. Thanks to the Porto team for the work that made this possible. ## Claude Skill Functor ships a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) so any Claude-powered agent can build with `@functornetwork/agentic-wallet` correctly out of the box. The skill teaches Claude when to reach for the SDK, the client and its methods, the common workflows, and the gotchas that bite at integration time. ### Install Save the skill into your project (or your user-level skills directory) at `.claude/skills/functor-agentic-wallet/SKILL.md`: ```bash mkdir -p .claude/skills/functor-agentic-wallet curl -fsSL https://docs.functor.sh/skill.md \ -o .claude/skills/functor-agentic-wallet/SKILL.md ``` Restart Claude Code (or open a new conversation) and Claude will load the skill automatically when a prompt matches its triggers — agent wallets, scoped permissions, session keys, passkey wallets, cross-agent authorization checks, or "an AI that can act on my wallet." The full skill content is reproduced below for reference. The canonical source is [`packages/wallet/SKILL.md`](https://docs.functor.sh/skill.md) in the SDK repo. *** ### 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 `
` is allowed to act on `` 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**](/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](/concepts/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 [testnet.bnbchain.org/faucet-smart](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. ## Why Functor Functor enables a **global registry of permissions onchain, accessible by any agent**. Traditional agentic wallets store permissions locally or in centralized servers.
Functor's **Keystore** infrastructure makes composable permissions accessible across any chain and any wallet, enabling:
Agent-to-agent verification Two AIs acting on the same wallet can verify each other's authority onchain. No platform in between.
Cross-app authorization Any DEX, orderbook, or protocol can read whether an agent is authorized, without integrating with the specific wallet vendor.
A new class of agent services Users hire AI agents through onchain employment contracts. Anyone can verify what an agent is allowed to do, and revoke is one transaction.
[See how Functor compares](/concepts/comparison) ### What Functor solves

Permissions without a middleman

Authorization rules live onchain, not in a vendor's database. Any protocol can verify whether an agent is allowed to act, with no integration required.

Policy enforced before every transaction

Spend caps, contract allowlists, and expiry windows are validated at the execution layer, not in application code that can be bypassed.

Self-custodial by architecture

The user signs. The user revokes. Functor never holds keys. Grant or revoke in one transaction, with no support ticket and no counterparty risk.

### Who it's for

App developers

Ship agentic features with a wallet your users actually control. Give an agent a scoped key, not full custody, and let the chain enforce the limit.

AI agent builders

Equip your agent with a wallet and a capped, revocable session. The agent operates within the bounds the user set, and those bounds are publicly verifiable.

Protocols and integrators

Read agent authority from the chain before executing. Know exactly what an agent is allowed to do without trusting the agent's own claims.

import { useState } from 'react' import { UseCaseMeta } from '../../components/UseCaseMeta' export function CodeWithPlaceholder({ code }) { const parts = code.split('"0xYourContract"') return (
        
          {parts.map((part, i) => (
            
              {part}
              {i < parts.length - 1 && "0xYourContract"}
            
          ))}
        
      
) } export function CopyAddr({ address }) { const [copied, setCopied] = useState(false) return ( {address} ) }

Build with Functor

## Give an agent a wallet and a policy createWallet, grantSession, execute, and revokeSession. }, { label: "Prerequisites", value: <>SDK installed with a private key and BNB testnet funds. See BNB Testnet to get set up. }, ]} /> *** You own the wallet. The agent gets a scoped key: it can only call the contracts you allow, only up to the spend cap you set, and it expires automatically. You can cut access with one transaction at any time. ### Step 1: Create your wallet This is your wallet. You're the admin. The agent never sees this key. ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); const wallet = await client.createWallet({ signer }); console.log(wallet.address); ``` **Fund `wallet.address` with testnet BNB before step 2:** [testnet.bnbchain.org/faucet-smart](https://testnet.bnbchain.org/faucet-smart) ### Step 2: Grant the agent a session This is the policy. You define which contracts the agent can call, the daily spend cap, and when the key expires. `grantSession` writes that policy onchain and returns a session object to hand to your agent.
Replace 0xYourContract with your contract address.
To test on BNB testnet, try WBNB: · Use Case 2 for DEX trading.
Pass `session` to your agent process. It holds the session key; treat it like a private key. ### Step 3: The agent executes The agent uses `session` to act. Your admin key is not involved. A call within the policy goes through. An oversized amount or a call to a different contract reverts at the onchain validator, not at Functor's backend, at the contract itself. ### Step 4: Revoke the session One call cuts the agent's access. Its next execute reverts immediately. ```ts await client.revokeSession({ wallet, signer, session }); ``` ### Why it's different The policy is onchain. Any tool, block explorer, or smart contract can verify whether the session key is still active by reading the keystore directly. Nothing to trust on Functor's side. *** * [Use Case 1b](/use-cases/1b-passkey-delegates-to-agent): the human side: a passkey wallet that delegates to an agent. * [Use Case 2](/use-cases/2-agent-trades-dex): point this session at a real DEX. import { UseCaseMeta } from '../../components/UseCaseMeta' import { PasskeyAgentDemo } from '../../components/PasskeyAgentDemo'

Build with Functor

## Use a passkey wallet as admin Consumer apps. Runs parallel to Use Case 1, not strictly after it. }, { label: "What you'll use", value: <>createPasskeyWallet, grantSession, execute, revokeSession. }, ]} /> ### How it works You hold the admin key inside your device's secure hardware, backed by Face ID or Touch ID. The agent holds a separate, scoped session key with a spend cap, an expiry, and a contract allowlist. You can revoke it in one transaction. The two roles never share a key: | Role | Key type | Held by | Can be revoked? | | ----------- | ------------------- | ------------- | --------------- | | Admin (you) | Passkey (P-256) | Your device | No: it's yours | | Agent | Session (secp256k1) | Agent process | Yes: one tx | ### Try it live Run the complete flow right here — no setup, no code. Each button calls the Functor SDK against BNB testnet. Your passkey stays on your device. *** ### Build this in your own app The steps below are for developers integrating this flow into a browser app (React, Next.js, plain HTML). `createPasskeyWallet` uses WebAuthn and must run in a browser — it cannot run in Node.js or a terminal. :::tip[Testing in Node.js or a script?] Swap `createPasskeyWallet` for `createHeadlessPasskey` — identical wallet shape, P256 key held in memory, no biometric prompt: ```ts import { createClient, BNB_TESTNET, createHeadlessPasskey } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const wallet = await client.createWallet({ signer: createHeadlessPasskey() }); ``` ::: ### Step 1: Create the passkey wallet ```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", // your app's domain }); ``` One biometric prompt. No seed phrase. `wallet.signer` works with every client method. ### Step 2: Grant a session to the agent You (admin) authorize the agent's key onchain. The agent key is separate from your passkey. ```ts const session = await client.grantSession({ wallet, signer: wallet.signer, // your passkey signs this permissions: { calls: [{ to: "0xSomeContract..." }], spend: [{ limit: 50_000_000_000_000_000n, period: "day" }], // 0.05 BNB/day cap }, expiry: Math.floor(Date.now() / 1000) + 24 * 60 * 60, // 1 day }); ``` :::tip[What lands onchain] In a single transaction: the agent's public key is registered in [Keystore](/concepts/keystore) and authorized on your wallet with its permissions hash. Any tool can verify the agent's authority with a free `eth_call`, with no API key and no Functor integration required. ::: ### Step 3: The agent executes, no passkey involved The agent uses its session key. Your passkey is not touched. The onchain validator enforces the permissions before the call goes through. ```ts // This runs in the agent's process — it has the `session` object. const result = await client.execute({ session, calls: [{ to: "0xSomeContract...", data: "0x...", value: 0n }], }); ``` An oversized call, a call to an unauthorized contract, or a call after expiry all revert at validation. The agent literally cannot exceed the leash. ### Step 4: Recover your wallet on any device Recovery matches your passkey's private key (held on your device, synced via iCloud or Google) against the public key registered onchain in the Keystore. Switch devices, come back months later — both halves are still there. ```ts const wallet = await client.recoverFromPasskey({ rpId: "myapp.example" }); // OS shows the passkey picker → biometric → done. ``` ### Step 5: Revoke the agent One transaction. Effect is global and immediate. ```ts await client.revokeSession({ wallet, signer: wallet.signer, session }); // The agent's next call reverts at validation. ``` :::info[Revocation is monotonic] Once revoked, a session key cannot be reactivated. To give the agent access again, grant a fresh session with a new keypair. ::: *** ### What's next * [Use Case 2](/use-cases/2-agent-trades-dex): point the agent session at a real DEX with a spend cap. * [Use Case 4](/use-cases/4-verify-agent-authority): verify any agent's authority with a single free onchain read. import { UseCaseMeta } from '../../components/UseCaseMeta'

Build with Functor

## Let an agent trade on a DEX, capped grantSession scoped to the DEX router with a spend cap and expiry, then execute. }, { label: "Prerequisites", value: <>Use Case 1 wallet, funded on BNB testnet with a testnet stablecoin. }, ]} /> The example below uses **PancakeSwap** on BNB testnet. Swap in any DEX router address to use a different exchange; the pattern is identical. ### Walkthrough ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey, } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); // wallet from Use Case 1 const session = await client.grantSession({ wallet, signer, permissions: { calls: [{ to: "0xPancakeRouter..." }], // only this router spend: [{ limit: 100_000_000_000_000_000_000n, period: "day", token: "0xStable..." }], }, expiry: Math.floor(Date.now() / 1000) + 3 * 24 * 60 * 60, }); // The agent builds swap calldata and executes within the cap. await client.execute({ session, calls: [{ to: "0xPancakeRouter...", data: "0xSwapCalldata...", value: 0n }], }); ``` ### The moment that makes it click A swap inside the cap goes through. An oversized swap is blocked at validation before it ever touches the chain. ### Why it is different The trading bot holds a key that can only hit that one router and only up to the cap. It cannot drain the wallet, cannot touch any other contract, and expires on its own. ### What's next [Use Case 3](/use-cases/3-portfolio-multiple-agents): run a full portfolio with more than one agent on the same wallet. import { UseCaseMeta } from '../../components/UseCaseMeta'

Build with Functor

## Run a portfolio with multiple agents grantSession twice with complementary scopes, execute, the Keystore read for mutual verification, revokeSession. }, { label: "Prerequisites", value: <>Use Case 1 wallet, funded on BNB testnet. }, ]} /> ### Walkthrough ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey, } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); // One shared wallet, the operator is admin. Two agents, separated duties. // Agent A: swaps on PancakeSwap, cap X. const sessionA = await client.grantSession({ wallet, signer, permissions: { calls: [{ to: "0xPancakeRouter..." }], spend: [{ limit: capX, period: "day", token: "0xStable..." }], }, expiry, }); // Agent B: lending / rebalance, cap Y. const sessionB = await client.grantSession({ wallet, signer, permissions: { calls: [{ to: "0xLendingPool..." }], spend: [{ limit: capY, period: "day", token: "0xStable..." }], }, expiry, }); // Before coordinating, Agent B can verify Agent A is still authorized // using the Keystore read from Use Case 4. // Revoke Agent A without touching Agent B: await client.revokeSession({ wallet, signer, session: sessionA }); // Agent B keeps working. ``` ### Why it is different Multiple agents on one wallet with divided, independently verifiable, independently revocable authority. Agent-to-agent verification uses only your own agents, with nothing external to set up. ### What's next * [Use Case 4](/use-cases/4-verify-agent-authority): verify each agent's authority with a single onchain read. import { UseCaseMeta } from '../../components/UseCaseMeta'

Build with Functor

## Verify an agent's authority from anywhere The Keystore read: getActiveKeys plus a keccak256 of the public key. Free, unlimited, from any RPC. }, { label: "Prerequisites", value: <>Use Case 1 wallet with an active session, or later. }, ]} /> ### Walkthrough ```ts import { createPublicClient, http, keccak256 } from "viem"; import { BNB_TESTNET } from "@functornetwork/agentic-wallet"; const client = createPublicClient({ chain: BNB_TESTNET.chain, transport: http(BNB_TESTNET.publicRpcUrl), }); const KEYSTORE_ABI = [{ name: "getActiveKeys", type: "function", stateMutability: "view", inputs: [{ name: "user", type: "address" }], outputs: [{ type: "bytes32[]" }], }] as const; const active = await client.readContract({ address: BNB_TESTNET.keyStore, abi: KEYSTORE_ABI, functionName: "getActiveKeys", args: [walletAddress], }); // One eth_call answers: is this key allowed to act on this wallet right now? const authorized = active.includes(keccak256(sessionPublicKey)); ``` Run this from your own script against your own wallet. This is exactly what a DEX or a counterparty agent would run, for free, from any RPC. ### Why it is different Authorization is a public onchain object. Verification costs nothing, needs no API key, and works for parties who have never heard of your app. **Note on sub-delegation.** Only the wallet admin grants sessions. Do not read this as one agent minting a sub-key for another. It is the admin authorizing both agents, and the agents verifying each other. If session-to-session sub-delegation lands later, the docs will be updated. ### What's next [Use Case 5](/use-cases/5-cross-chain-authorization): take that authorization to another chain without re-granting. import { UseCaseMeta } from '../../components/UseCaseMeta'

Build with Functor

## Authorize across chains grantSession on L1, ensureKeyCached to mirror, then verify from L2. }, { label: "Prerequisites", value: <>Use Case 1 wallet with an active session. }, ]} /> ### How it works Sessions are granted on L1 (Sepolia, the Keystore source of truth). An L2 cache on Base Sepolia can verify that same session via a storage proof against L1 state, without any bridge message or re-granting. `ensureKeyCached` handles the proof: 1. Reads the L2 cache. If the key is already there, returns immediately (`cache-hit`). 2. Polls the L2's `L1Block` predeploy until it anchors past the relevant L1 block (`waiting-for-anchor`, typically 1–3 min) 3. Fetches an `eth_getProof` from L1 and submits it to the L2 cache (`submitting-proof`) 4. Returns once the cache confirms the key is live (`done`) After step 4, any tool on Base Sepolia can call `isValidKey` on the cache, for free, from any RPC. ### Walkthrough ```ts import { createClient, ensureKeyCached, SEPOLIA, BASE_SEPOLIA, signerFromPrivateKey, } from "@functornetwork/agentic-wallet"; import { createPublicClient, createWalletClient, http } from "viem"; import { sepolia, baseSepolia } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; // 1. Grant the session on Sepolia (L1, source of truth). const client = createClient({ chains: [SEPOLIA] }); const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); const wallet = await client.createWallet({ signer }); const session = await client.grantSession({ wallet, signer, permissions: { calls: [{ to: "0xSomeContract..." }], spend: [{ limit: 50_000_000_000_000_000n, period: "day" }], }, expiry: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, }); // 2. Mirror the session key to the Base Sepolia L2 cache. const l1Client = createPublicClient({ chain: sepolia, transport: http(SEPOLIA.publicRpcUrl) }); const l2Client = createPublicClient({ chain: baseSepolia, transport: http(BASE_SEPOLIA.publicRpcUrl) }); // The L2 wallet client pays L2 gas to submit the proof. // Any funded account works — your own EOA, a backend relayer, etc. const l2WalletClient = createWalletClient({ account: privateKeyToAccount(process.env.RELAYER_KEY as `0x${string}`), chain: baseSepolia, transport: http(BASE_SEPOLIA.publicRpcUrl), }); await ensureKeyCached({ l1Client, l2Client, l2WalletClient, l1KeyStore: SEPOLIA.keyStore, l2Cache: BASE_SEPOLIA.keyStoreCache, user: session.walletAddress, publicKey: session.publicKey, onStatus: (s) => console.log(s), // cache-hit | waiting-for-anchor | submitting-proof | done }); // 3. Verify the session from Base Sepolia — free, from any RPC. const { keccak256 } = await import("viem"); const CACHE_ABI = [{ name: "isValidKey", type: "function", stateMutability: "view", inputs: [ { name: "user", type: "address" }, { name: "keyId", type: "bytes32" }, ], outputs: [{ type: "bool" }], }] as const; const keyId = keccak256(session.publicKey); const isValid = await l2Client.readContract({ address: BASE_SEPOLIA.keyStoreCache, abi: CACHE_ABI, functionName: "isValidKey", args: [session.walletAddress, keyId], }); console.log("Session valid on Base Sepolia:", isValid); // true ``` :::info[L2 execution] The current SDK supports wallet execution on BNB testnet and Sepolia. Cross-chain **verification** from Base Sepolia is available today via `ensureKeyCached` + `isValidKey`; additional L2 execution support will be added as it ships. ::: ### Why it is different Authorization crosses chains by storage proof, permissionlessly, with no bridge message and no per-chain re-granting. The first cross-chain action pays the proof cost once; after that, the L2 cache serves reads locally without L1 round-trips. ### What's next [Use Cases overview](/use-cases): see the full map of what you can build with Functor. ## Overview The at-a-glance map. Find your goal, then follow the path. | Goal | SDK functions | MCP commands | Chain | | -------------------------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | | [Give an agent a wallet and a policy](/use-cases/1-agent-wallet-policy) | `createWallet`, `grantSession`, `execute`, `revokeSession` | create-wallet, grant-session, send-tx, revoke-session | BNB testnet | | [Passkey wallet delegates to an agent](/use-cases/1b-passkey-delegates-to-agent) | `createPasskeyWallet`, `grantSession`, `recoverFromPasskey` | (browser SDK) | BNB testnet | | [Agent trades on DEX, capped](/use-cases/2-agent-trades-dex) | `grantSession` (scoped), `execute` | grant-session, session-execute | BNB testnet | | [Portfolio with multiple agents](/use-cases/3-portfolio-multiple-agents) | `grantSession` (×2), `execute`, Keystore read, `revokeSession` | grant-session, session-execute, verify-session, revoke-session | BNB testnet | | [Verify agent authority from anywhere](/use-cases/4-verify-agent-authority) | Keystore read (`getActiveKeys` + `keccak256`) | verify-session | any | | [Authorization across chains](/use-cases/5-cross-chain-authorization) | `grantSession`, `ensureKeyCached`, `execute` | (SDK) | Sepolia → Base Sepolia | *** The paths build on each other. Use Case 1 is the hello-world every other path assumes. Start there if you are new, or jump to whichever goal matches your situation.

SDK

## balances Read a wallet's on-chain balances. This is a plain read: no signer, no userOp, no relay. It works for any address (a `Wallet` you created or a bare `0x…` address), including counterfactual wallets that haven't been deployed yet. ```ts // `client` and `wallet` from earlier const { native } = await client.balances({ wallet }); console.log(native); // bigint — native token balance in wei ``` Pass a bare address if that's all you have: ```ts const { native } = await client.balances({ wallet: "0xabc…" as `0x${string}`, }); ``` Target a specific chain when the client is configured with more than one: ```ts import { createClient, BNB_TESTNET, SEPOLIA } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET, SEPOLIA] }); // BNB testnet (the default — first chain) const bnb = await client.balances({ wallet }); // Ethereum Sepolia const sepolia = await client.balances({ wallet, chainId: 11155111 }); ``` ### Parameters ```ts client.balances(opts: ClientBalancesOptions): Promise; type ClientBalancesOptions = { wallet: Wallet | Address; // a Wallet object or a bare 0x… address chainId?: number; // defaults to the client's default chain }; type BalancesResult = { native: bigint; // native token balance in wei (tBNB on BNB, ETH on Sepolia) }; ``` ### Notes * **Native only, for now.** `balances` returns the native token balance. ERC-20 balances land alongside the v1 DeFi recipes (the recipes know which tokens matter per protocol). * **No funds required.** It's a read against the chain's public RPC, so it doesn't cost gas and doesn't need the wallet to be funded or deployed. * **Units are wei.** Use viem's [`formatEther`](https://viem.sh/docs/utilities/formatEther) to display it: `formatEther(native)`. ## BNB Testnet BNB Smart Chain Testnet (chain id **97**) is Functor's default network. The keystore and the account contracts are deployed there (see [Networks & Addresses](/concepts/networks)), and the SDK ships a ready-made `BNB_TESTNET` config. ### Install ```bash npm install @functornetwork/agentic-wallet viem ``` ### Create a wallet on BNB testnet ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); const wallet = await client.createWallet({ signer }); console.log(wallet.address); ``` **Fund `wallet.address` with testnet BNB before step 2:** [testnet.bnbchain.org/faucet-smart](https://testnet.bnbchain.org/faucet-smart) ### Multi-chain `createClient` accepts multiple wallet execution chains. Add Sepolia alongside BNB and pass `chainId` per call: ```ts import { createClient, BNB_TESTNET, SEPOLIA } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET, SEPOLIA], defaultChainId: 97 }); await client.grantSession({ /* ... */, chainId: 11155111 }); // operate on Sepolia ``` The SDK also exports `BASE_SEPOLIA` for the L2 Keystore cache used by cross-chain verification. ### What's next * [Networks & Addresses](/concepts/networks). Chain ids, RPCs, and contract addresses. * [Grant a session](/sdk/grant-session). Scoped, time-bounded keys for AI agents.

SDK

## createPasskeyWallet `client.createPasskeyWallet` creates a smart-account wallet whose admin authority is a passkey (Face ID, Touch ID, Windows Hello, hardware security key). The private key never leaves the device's secure hardware. **Browser only.** Uses `navigator.credentials`. ```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 a PasskeySigner. Use it like any other signer. ``` Same as [`createWallet`](/sdk/create-wallet): the wallet is counterfactual until the first [`execute`](/sdk/execute), which is when the admin (P-256) public key gets registered in [Keystore](/concepts/keystore). ### Parameters ```ts type ClientCreatePasskeyWalletOptions = { /** Label shown in the OS passkey prompt (e.g. "MyApp"). */ name: string; /** Relying-Party ID. Defaults to the current origin's host. */ rpId?: string; }; ``` The chains the wallet is provisioned on come from the client (`createClient({ chains })`). For wallet execution, use `BNB_TESTNET` or `SEPOLIA`; `BASE_SEPOLIA` is exported for the L2 Keystore cache used by cross-chain verification. ### Returns A standard `CreateWalletResult` whose `signer` is a `PasskeySigner`. ### Notes * The wallet address is **embedded in the passkey's userHandle** at creation time. This is what makes [`recoverFromPasskey`](/sdk/recover-from-passkey) work later: the device knows which wallet each saved passkey belongs to. * Authorization on the chain side uses **P-256 (secp256r1)** signatures, matching WebAuthn. [Keystore](/concepts/keystore) is signature-scheme agnostic, so it stores the P-256 public key the same way it stores secp256k1 keys. * The user sees a single biometric prompt per signature. No seed phrases, no extension required. ### Example: full passkey app flow ```ts import { createClient, BNB_TESTNET } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); async function loginOrSignup() { try { // Returning user: pick from saved passkeys return await client.recoverFromPasskey({ rpId: "myapp.example" }); } catch { // New user: create one return await client.createPasskeyWallet({ name: "MyApp", rpId: "myapp.example", }); } } const wallet = await loginOrSignup(); await client.execute({ wallet, signer: wallet.signer, calls: { to: "0x...", value: 0n }, }); ```

SDK

## createWallet `client.createWallet` creates a smart-account wallet for a signer. The signer's key lives wherever you keep it (env var, OS keychain, hardware wallet). Functor never sees it. First create a client with `createClient`, configured with the chains the wallet should work on. The same wallet address is provisioned on every chain the client lists. ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey("0x..."); const wallet = await client.createWallet({ signer }); ``` The wallet is **counterfactual**. Its address is deterministic, but it isn't a smart account onchain until the first [`execute`](/sdk/execute). That first execute is what registers the admin key in [Keystore](/concepts/keystore). ### Parameters ```ts type ClientCreateWalletOptions = { /** Bring your own signer. If omitted, the SDK generates a fresh private-key signer. */ signer?: Signer; }; ``` The chains the wallet is provisioned on come from the client (`createClient({ chains })`), not from this call. ### Returns ```ts type CreateWalletResult = { address: Address; // the wallet's smart-account address (same on every chain) signer: Signer; // same reference if you passed one in }; ``` ### Notes * Fund `result.address` with native tokens before calling `execute` — the wallet is counterfactual and has no balance until you send some. * If you omit `signer`, the SDK generates a fresh private-key signer and returns it on the result. Persist `result.signer` however your app stores keys. * The address is identical on every chain the client was configured with, so one wallet handle works across all of them: pass `chainId` per operation to pick which one. ### Private key helpers Two functions handle the private-key signer path: **`signerFromPrivateKey(key)`**: reconstructs a signer from a key you already have. Use this when the key is stored in an env var, OS keychain, or secrets manager. ```ts import { signerFromPrivateKey } from "@functornetwork/agentic-wallet"; const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); ``` **`createPrivateKeySigner()`**: generates a fresh random secp256k1 key and returns the signer. Use this when you want the SDK to create the key. **You are responsible for persisting it**: it is not stored anywhere automatically. ```ts import { createPrivateKeySigner, createClient, BNB_TESTNET } from "@functornetwork/agentic-wallet"; const signer = createPrivateKeySigner(); console.log("Save this key:", signer._privateKey); // persist before continuing const client = createClient({ chains: [BNB_TESTNET] }); const wallet = await client.createWallet({ signer }); ``` Calling `createWallet()` with no `signer` argument is equivalent: it calls `createPrivateKeySigner()` internally and attaches the result to `wallet.signer`.

SDK

## execute Submit one or more calls from a wallet. `client.execute` accepts either an admin pair (`wallet` + `signer`) or a `session`, plus the `calls` to run. ### Keystore impact | Scenario | Touches Keystore? | | --------------------------------------- | ----------------------------------------- | | First admin `execute` on a fresh wallet | Yes (admin key registered) | | Subsequent admin `execute` calls | No | | Any session `execute` | No (session was registered at grant time) | ### Admin path The wallet's admin signs the intent. Use this for first-party operations. ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey("0x..."); const wallet = await client.createWallet({ signer }); const result = await client.execute({ wallet, signer, calls: { to: "0xRecipient...", value: 1_000_000_000_000_000n }, // 0.001 BNB on BNB testnet }); console.log(result.status, result.transactionHash); ``` ### Session path A session signs the intent. Use this for agent-driven operations. ```ts // `client` and `session` from grantSession const result = await client.execute({ session, calls: [{ to: "0xUniswapRouter...", data: "0x...", value: 0n }], }); ``` ### Parameters ```ts client.execute(opts: ClientExecuteOptions): Promise; type ClientExecuteOptions = | { wallet: Wallet; signer: Signer; calls: Call | readonly Call[]; feeToken?: Address; // default: native token noWait?: boolean; // return as soon as submitted, don't wait for confirmation chainId?: number; // defaults to the client's default chain } | { session: Session; calls: Call | readonly Call[]; feeToken?: Address; noWait?: boolean; chainId?: number; }; type Call = { to: Address; data?: Hex; value?: bigint; }; ``` ### Returns ```ts type ExecuteResult = { callsId: Hex; status: "CONFIRMED" | "FAILED" | "PENDING"; transactionHash?: Hex; }; ``` ### Notes * **First execute on a fresh wallet auto-registers the admin key** in [Keystore](/concepts/keystore). `initialRegisterKey` is prepended transparently in the same userOp. * **Empty calls is rejected.** `client.execute({ wallet, signer, calls: [] })` errors. Pass at least one call. * **Session execute is byte-exact.** The onchain validator matches the session's `permissions + expiry + role + publicKey` to the hash committed at grant. Persist the `Session` object verbatim. * **`chainId` selects the chain.** Omit it to use the client's default chain; pass one of the client's configured chains to target another. * With `noWait: true`, you get `status: "PENDING"` and a `callsId` to poll later.

SDK

## grantSession Grant a scoped session key for a wallet. The admin signer authorizes the session onchain; from that point forward the session can act on the wallet within its permissions, enforced onchain. ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const admin = signerFromPrivateKey("0x..."); const wallet = await client.createWallet({ signer: admin }); const session = await client.grantSession({ wallet, signer: admin, permissions: { calls: [{ to: "0xUniswapRouter..." }], 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. ``` ### Keystore impact The session's public key is written to [Keystore](/concepts/keystore) via the Controller. From that moment, any tool reading `getActiveKeys` on the wallet sees this session. Reads are free and unlimited. ### Parameters ```ts client.grantSession(opts: ClientGrantSessionOptions): Promise; type ClientGrantSessionOptions = { wallet: Wallet; signer: Signer; // the wallet's admin signer permissions: SessionPermissions; /** Unix epoch seconds. Most apps use Date.now()/1000 + N. */ expiry: number; /** Bring your own session signer, or omit to let the SDK generate one. */ sessionSigner?: Signer; /** Fee token (default: native token). */ feeToken?: Address; /** Target chain. Defaults to the client's default chain. */ chainId?: number; }; ``` See [Sessions](/concepts/sessions) for the full permission shape reference. ### Returns ```ts type Session = { walletAddress: Address; signer: Signer; // session key. Agent signs with this. publicKey: Hex; permissions: SessionPermissions; expiry: number; }; ``` ### What lands onchain In a single userOp: 1. The session's public key is registered in [Keystore](/concepts/keystore), making it discoverable by any tool. 2. The session is authorized on the wallet's smart account with its permissions hash. Both happen atomically. There is no intermediate state where one exists without the other. ### Notes * **Persist the full `Session` object**, not just `publicKey`. The agent needs `permissions + expiry` byte-exact at execute time. * If you generated the session signer (omitted `sessionSigner`), persist `session.signer` securely. Losing it means the session can no longer sign. * **`permissions.calls` omitted = unrestricted.** Always set both `calls` and `spend` unless you specifically want an open-scope session.

SDK

## recoverFromPasskey Recover a passkey-backed wallet using onchain state and the OS keychain. **Browser only.** ```ts import { createClient, BNB_TESTNET } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const wallet = await client.recoverFromPasskey({ rpId: "myapp.example" }); // Browser shows the passkey picker, biometric prompt, done. // Two onchain reads, no server, no localStorage required. ``` ### Keystore impact Recovery is a pure read: two `eth_call`s against Keystore (`getActiveKeys` + `getPublicKey`). No onchain write, no transaction. An app can recover a user's wallet handle a million times a day at zero cost. ### Flow 1. **Discoverable-credential picker.** `allowCredentials: []` tells the OS to show all passkeys saved on this device for the given `rpId`. The user picks one, then completes biometric verification. 2. **Extract the wallet address.** The assertion's `userHandle` carries the 20-byte wallet address that `createPasskeyWallet` baked in at creation time. 3. **Read Keystore.** `getActiveKeys(wallet) + getPublicKey(wallet, keyId)` to retrieve the P-256 public key the wallet authorized. 4. **Rebuild the signer.** From `{ credentialId, publicKey, rpId }`. Two `eth_call`s, one biometric prompt. No server side-channel. ### Parameters ```ts type ClientRecoverFromPasskeyOptions = { /** Relying-Party ID. Must match what was used at creation time. */ rpId?: string; /** Target chain to read from. Defaults to the client's default chain. */ chainId?: number; }; ``` For wallet reads and writes, configure the client with `BNB_TESTNET` or `SEPOLIA`. `BASE_SEPOLIA` is the exported L2 Keystore cache for verifying Sepolia-granted sessions cross-chain. ### Returns A `CreateWalletResult` whose `signer` is a `PasskeySigner`. Drop-in compatible with `client.execute`, `client.grantSession`, etc. ### Notes * **The wallet must have transacted at least once.** Recovery reads the admin key from Keystore, which is populated on the wallet's first `execute`. A wallet that was created but never used isn't yet onchain to recover from. * **The `rpId` must match.** Passkeys are scoped to a relying-party ID, usually a domain. If you used `"myapp.example"` at creation, you must use the same here. * **No fallback to localStorage.** Recovery is purely onchain plus device-resident. This is what makes Functor wallets durable across machines, browser data wipes, and OS migrations, as long as the passkey is in iCloud Keychain or Google Password Manager.

SDK

## revokeSession Revoke a session key from a wallet onchain. After confirmation, the session's next execute attempt reverts at validation. Effect is global and immediate. No off-chain coordination required. ```ts // `client`, `wallet`, `admin`, and `session` from earlier await client.revokeSession({ wallet, signer: admin, session }); ``` You can also pass just the session's public key: ```ts const sessionPublicKey = "0x04..." as `0x${string}`; await client.revokeSession({ wallet, signer: admin, session: sessionPublicKey }); ``` ### Keystore impact Revocation calls Keystore directly, not the Controller. The call is gated onchain by `onlyKeyOwnerOrValidator`, so only the wallet itself (executing inside its own userOp) or a designated validator can revoke a key. A random caller reverts at the modifier. Revocation is **monotonic**: once a key is revoked, it cannot be reactivated. To restore session access for a wallet, grant a new session with a fresh keypair. ### Parameters ```ts client.revokeSession(opts: ClientRevokeSessionOptions): Promise; type ClientRevokeSessionOptions = { wallet: Wallet; signer: Signer; // the wallet's admin signer session: Session | Hex; // the Session object or just its public key feeToken?: Address; chainId?: number; // defaults to the client's default chain }; ``` ### What lands onchain In a single userOp: 1. The session is **revoked in Keystore**. Any tool reading `getActiveKeys` will no longer see it. 2. The session's authority on the wallet's smart account is pulled. Both atomic. After the userOp confirms, the session is dead everywhere simultaneously. ### Notes * Revocation does not require the session signer, only the wallet's admin signer. * You can keep just the session's public key in your records for revocation purposes, then discard the rest of the `Session` object once granted.

SDK

## ensureKeyCached A session you granted via [`grantSession`](/sdk/grant-session) is registered in the [Keystore](/concepts/keystore) on L1 (Sepolia). For another chain to honor that session, the registry state has to be mirrored to an L2 cache on that chain. `ensureKeyCached` does that. It's idempotent: if the L2 cache already has the key, it returns immediately. Otherwise it waits for the L2 to anchor past the relevant L1 block, then submits a storage proof. ```ts import { ensureKeyCached, SEPOLIA, BASE_SEPOLIA } from "@functornetwork/agentic-wallet"; import { createPublicClient, createWalletClient, http } from "viem"; import { sepolia, baseSepolia } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; const l1Client = createPublicClient({ chain: sepolia, transport: http(SEPOLIA.publicRpcUrl) }); const l2Client = createPublicClient({ chain: baseSepolia, transport: http(BASE_SEPOLIA.publicRpcUrl) }); // The L2 wallet client pays L2 gas to submit the proof. This can be ANY // account with L2 native ETH — your own EOA, a backend relayer, a per-user // funded address, etc. The proof is permissionless; no privilege is granted. const l2WalletClient = createWalletClient({ account: privateKeyToAccount(relayerKey), chain: baseSepolia, transport: http(BASE_SEPOLIA.publicRpcUrl), }); await ensureKeyCached({ l1Client, l2Client, l2WalletClient, l1KeyStore: SEPOLIA.keyStore, l2Cache: BASE_SEPOLIA.keyStoreCache, user: session.walletAddress, publicKey: session.publicKey, onStatus: (status) => { // "cache-hit" | "waiting-for-anchor" | "submitting-proof" | "done" console.log(status); }, }); // Session is now valid on Base Sepolia. Subsequent agent actions are instant. ``` ### What it does 1. Reads `isValidKey(user, keyId)` on the L2 cache. If `true`, returns immediately (`cache-hit`). 2. Otherwise polls the L2's `L1Block` predeploy until it points to an L1 block that contains the registration (`waiting-for-anchor`). Base Sepolia anchors \~1–3 min behind L1. 3. Fetches an `eth_getProof` against L1 for the packed Key storage slot, RLP-encodes the L1 block header, and submits `populateKey(...)` to the L2 cache (`submitting-proof`). 4. Reads the cache back and resolves with the populated `CachedKey` (`done`). The first cross-chain action on a given chain pays this cost once. After the cache is populated, the L2 reads it locally without further L1 round-trips. ### Who pays L2 gas The submitter of `populateKey` pays. The op is permissionless on the contract, so anyone can land a valid proof, but *someone* has to send the L2 tx. Practically: * **A relayer EOA you control.** Simplest in v0. Fund a single address on each L2 and have your backend submit proofs on demand. * **The user themselves**, if they already hold L2 native ETH. Pass their `WalletClient`. * **A future bundler-batched flow** that includes `populateKey` as the first call of the L2 userOp. Not wired into `execute` yet. There's no on-protocol relayer. The cache contract is just a verifier. ### Parameters ```ts function ensureKeyCached(args: EnsureKeyCachedArgs): Promise; type EnsureKeyCachedArgs = { /** Public client on the L1 chain (Sepolia). Used for eth_getProof + block lookup. */ l1Client: PublicClient; /** Public client on the L2 chain. Used for L1Block read + receipt wait. */ l2Client: PublicClient; /** Wallet client on the L2 chain — the relayer that pays L2 gas. */ l2WalletClient: WalletClient; /** L1 Keystore address. From SEPOLIA.keyStore. */ l1KeyStore: Address; /** L2 cache address. From BASE_SEPOLIA.keyStoreCache. */ l2Cache: Address; /** Wallet whose key is being mirrored — the smart account that registered on L1. */ user: Address; /** SEC1-encoded public key bytes for the session being mirrored. */ publicKey: Hex; /** Progress callback for the four lifecycle states. */ onStatus?: (status: EnsureKeyCachedStatus) => void; /** Poll cadence while waiting for L2 to anchor. Default 3s. */ anchorPollIntervalMs?: number; /** Max wait for the L1 anchor. Default 5 minutes. */ anchorTimeoutMs?: number; }; type EnsureKeyCachedStatus = | "cache-hit" | "waiting-for-anchor" | "submitting-proof" | "done"; ``` ### Returns ```ts type CachedKey = { publicKey: Hex; revoked: boolean; expiry: number; // unix seconds, 0 = never expires isRoot: boolean; sourceBlockHash: Hex; sourceBlockNumber: bigint; }; ``` The `sourceBlockHash` / `sourceBlockNumber` fields record which L1 block the cache state was proven against, useful for monitoring how stale the L2 view is. ### Related helpers `ensureKeyCached` is the function most apps want. The lower-level helpers are also exported for cases where you want explicit control: ```ts // Always submit a fresh proof regardless of cache state. Use to propagate a // revocation you just landed on L1 without waiting for the next cache miss. syncKeyToL2(args): Promise<{ txHash: Hex; cachedKey: CachedKey }>; // Pure read against the L2 cache. Returns zeroed struct if not yet populated. readCachedKey(l2Client, l2Cache, user, keyId): Promise; // Pure read: cache has the key AND it's not revoked AND not expired. // Equivalent to KeyStoreCache.isValidKey(user, keyId). isCachedKeyValid(l2Client, l2Cache, user, keyId): Promise; ``` ### Notes * **L1 Keystore is the source of truth.** The L2 cache is only as fresh as the last proof submitted. Revocations on L1 do **not** propagate automatically; until someone submits a post-revocation proof, the L2 cache still reports the key as valid. Call `syncKeyToL2` right after `revokeSession` to push the revocation through, or rely on `ensureKeyCached` running again on the next action. * **Monotonic revoke is enforced cache-side too.** Once the cache observes `revoked=true`, it cannot be flipped back to live by a later proof: even one against an older L1 block. This prevents replay attacks during the L1→L2 anchor window. * **The cache is permissionless.** Any address can submit a proof for any `(user, publicKey)` pair. There is no admin and no relayer registry. * **One cache per chain.** For the supported L2 cache today, use `BASE_SEPOLIA.keyStoreCache`; see the [Chains](/concepts/keystore#chains) table. ## MCP Server `@functornetwork/mcp` exposes the SDK as an MCP server. AI hosts like Claude Code, Cursor, and Continue can use it to create wallets, grant sessions, and execute transactions through tools or slash commands. ### When to use it * You want to **operate** wallets from a chat interface ("create a wallet for me", "grant this bot a daily cap of 0.01 ETH"). * You're prototyping agent flows and want to test session lifecycles by hand. * You're building demos where the user is an AI host, not your code. If you're writing code, use [`@functornetwork/agentic-wallet`](/sdk/bnb-testnet) directly. The MCP server is a thin wrapper. ### What it provides * **11 tools** covering wallet creation, balance, verification, session lifecycle, and transactions. * **12 prompts** exposed as slash commands (`/functor-agentic-wallet:create-wallet`, etc.). * **Stateless key handling.** Keys live in your OS keychain, with file/env fallbacks. ### Next * [Install](/mcp/install). Get the server running in your AI host. * [Tools](/mcp/tools). Full reference for every exposed tool.

MCP Server

## Install ### Claude Code ```bash claude mcp add functor -- bunx @functornetwork/mcp ``` That's it. Restart Claude Code; tools and slash commands become available. To remove: ```bash claude mcp remove functor ``` ### Network The server operates on one chain, selected at startup via the `FUNCTOR_CHAIN` environment variable. It defaults to **BNB Smart Chain Testnet**. | `FUNCTOR_CHAIN` | Chain | | ----------------------- | ---------------------------- | | `bnb-testnet` (default) | BNB Smart Chain Testnet (97) | | `sepolia` | Ethereum Sepolia (11155111) | ```bash # Operate on Sepolia instead of the BNB default claude mcp add functor -e FUNCTOR_CHAIN=sepolia -- bunx @functornetwork/mcp ``` One server process serves one chain. Restart with a different `FUNCTOR_CHAIN` to switch. ### Cursor / Continue / other hosts Add this to your host's MCP server config: ```json { "mcpServers": { "functor": { "command": "bunx", "args": ["@functornetwork/mcp"] } } } ``` ### Keys Wallet admin keys and session keys live in **separate namespaces** so they can never collide. The server reads each kind from three places, in order. **Wallet admin keys:** 1. **OS keychain** under service `functor-wallet`. Primary. Written by `create_wallet`. 2. **`~/.functor/keys.json`** → `wallets[]`. File fallback. Mode 0600. 3. **`FUNCTOR_WALLET__PRIVATE_KEY`** env var. Env fallback. For the default wallet, the convention is `FUNCTOR_WALLET_DEFAULT_PRIVATE_KEY`. **Session keys:** 1. **OS keychain** under service `functor-session`. Primary. Written by `grant_session`. 2. **`~/.functor/keys.json`** → `sessions[]`. File fallback. 3. **`FUNCTOR_SESSION__PRIVATE_KEY`** env var. Functor never sees these keys. They stay on your machine. ## Claude Skill Functor ships a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) so any Claude-powered agent can build with `@functornetwork/agentic-wallet` correctly out of the box. ### Install Save the skill into your project (or your user-level skills directory) at `.claude/skills/functor-agentic-wallet/SKILL.md`: ```bash mkdir -p .claude/skills/functor-agentic-wallet curl -fsSL https://docs.functor.sh/skill.md \ -o .claude/skills/functor-agentic-wallet/SKILL.md ``` Restart Claude Code (or open a new conversation) and Claude will load the skill automatically when a prompt matches its triggers. ### When the skill activates 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 onchain" * "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 `
` is allowed to act on `` right now" ### MCP server vs skill | You are… | Use | | ------------------------------------------------ | ------------------------------------------------- | | Writing TypeScript/JS code that needs wallet ops | `@functornetwork/agentic-wallet` SDK (this skill) | | Operating wallets interactively from Claude Code | [`@functornetwork/mcp`](/mcp) server | | Building a UI that signs from the browser | SDK with `client.createPasskeyWallet` | | Running a local agent that holds its own session | SDK with `client.execute({ session, calls })` | The full skill content is at [`packages/wallet/SKILL.md`](https://github.com/functornetwork/sdk/blob/main/packages/wallet/SKILL.md) in the SDK repo.

MCP Server

## Tools The Functor MCP server exposes 11 tools. AI hosts call them by name; users can also invoke them as slash commands via the prompt interface (e.g. `/functor-agentic-wallet:create-wallet`). ### Discovery | Tool | Purpose | | --------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `about_functor` | Returns Functor's positioning, Keystore explainer, and SDK surface. AI hosts call this when the user asks "what is Functor". | ### Wallet lifecycle | Tool | Purpose | | ---------------- | ---------------------------------------------------------------------------------------------------- | | `create_wallet` | Generates a new private key, stores it in the OS keychain under the given name, returns the address. | | `list_wallets` | Lists wallet names available on this machine (across keychain, file, env). | | `wallet_balance` | Reads native + ERC-20 balances for a stored wallet. | | `wallet_execute` | Submits one or more calls signed by the wallet's admin key. | ### Verification | Tool | Purpose | | ---------------------- | ---------------------------------------------------------------------- | | `wallet_verification` | Lists all active keys on a wallet from [Keystore](/concepts/keystore). | | `verify_authorization` | Answers: is this key/session authorized on this wallet right now? | ### Session lifecycle | Tool | Purpose | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `grant_session` | Generates a session key, registers it in Keystore, authorizes it with the given permissions. Returns the session details + keyId. | | `list_sessions` | Lists local session metadata (those granted from this machine). | | `session_execute` | Submits calls signed by a stored session key. | | `revoke_session` | Deactivates the session in Keystore and pulls onchain authority. | ### Slash command equivalents Every tool has a matching prompt, exposed as a slash command: | Slash command | Calls | | ----------------------------------------- | ------------------------------- | | `/functor-agentic-wallet:about` | `about_functor` | | `/functor-agentic-wallet:create-wallet` | `create_wallet` | | `/functor-agentic-wallet:list-wallets` | `list_wallets` | | `/functor-agentic-wallet:wallet-balance` | `wallet_balance` | | `/functor-agentic-wallet:wallet-info` | `wallet_verification` | | `/functor-agentic-wallet:verify-session` | `verify_authorization` | | `/functor-agentic-wallet:grant-session` | `grant_session` | | `/functor-agentic-wallet:list-sessions` | `list_sessions` | | `/functor-agentic-wallet:session-execute` | `session_execute` | | `/functor-agentic-wallet:revoke-session` | `revoke_session` | | `/functor-agentic-wallet:send-tx` | `wallet_execute` | | `/functor-agentic-wallet:demos` | listing of available demo flows | ## Getting Started · BNB Testnet BNB Smart Chain Testnet (chain id **97**) is Functor's default network. The KeyStore and the account contracts are deployed there (see [Networks & Addresses](/concepts/networks)), and the SDK ships a ready-made `BNB_TESTNET` config. ### Install ```bash npm install @functornetwork/agentic-wallet viem ``` ### Create a wallet on BNB testnet ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); const wallet = await client.createWallet({ signer }); console.log(wallet.address); ``` **Fund `wallet.address` with testnet BNB before step 2:** [testnet.bnbchain.org/faucet-smart](https://testnet.bnbchain.org/faucet-smart) ### Grant a session and execute ```ts import { createPrivateKeySigner } from "@functornetwork/agentic-wallet"; import { parseEther } from "viem"; const session = await client.grantSession({ wallet, signer, sessionSigner: createPrivateKeySigner(), permissions: { calls: [{ to: "0xRecipient..." }], spend: [{ limit: parseEther("0.001"), period: "day" }], }, expiry: Math.floor(Date.now() / 1000) + 3600, }); const result = await client.execute({ session, calls: { to: "0xRecipient...", value: 1n, data: "0x" }, }); console.log(result.status, result.transactionHash); ``` ### Multi-chain `createClient` accepts multiple wallet execution chains. Add Sepolia alongside BNB and pass `chainId` per call: ```ts import { createClient, BNB_TESTNET, SEPOLIA } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET, SEPOLIA], defaultChainId: 97 }); await client.grantSession({ /* ... */, chainId: 11155111 }); // operate on Sepolia ``` The SDK also exports `BASE_SEPOLIA` for the L2 Keystore cache used by cross-chain verification. ### What's next * [Networks & Addresses](/concepts/networks). Chain ids, RPCs, and contract addresses. * [Grant a session](/sdk/grant-session). Scoped, time-bounded keys for AI agents. import { useState, useRef, useCallback } from 'react' export function CopyPre({ code }) { const [copied, setCopied] = useState(false) const copy = useCallback(() => { navigator.clipboard.writeText(code) setCopied(true) setTimeout(() => setCopied(false), 2000) }, [code]) return (
        
          {code.split(/(0x[A-Z][a-zA-Z]+)/g).map((part, i) =>
            /^0x[A-Z][a-zA-Z]+$/.test(part)
              ? {part}
              : part
          )}
        
      
) } export function CopyUrl({ url }) { const [copied, setCopied] = useState(false) const copy = useCallback(() => { navigator.clipboard.writeText(url) setCopied(true) setTimeout(() => setCopied(false), 2000) }, [url]) return ( {url} ) } export function ToolSelector() { const [tool, setTool] = useState('claude') const tabs = [ { id: 'claude', label: 'Claude' }, { id: 'codex', label: 'Codex' }, { id: 'other', label: 'Other AI' }, ] const tabStyle = (id) => ({ padding: '0.5rem 1.4rem', border: '1px solid var(--vocs-color_border)', borderRadius: '6px', background: tool === id ? 'var(--vocs-color_backgroundDark)' : 'transparent', color: 'inherit', cursor: 'pointer', fontWeight: tool === id ? '600' : '400', fontSize: '0.95rem', transition: 'background 0.15s', }) return (
{tabs.map(t => ( ))}
{/* ── Claude ─────────────────────────────────────────── */} {tool === 'claude' && (

Operate wallets by chatting (MCP server)

No code. Add the Functor MCP server to Claude Code and create wallets, grant sessions, and send transactions through chat or slash commands.

Then in Claude Code:

  • /functor-agentic-wallet:create-wallet
  • /functor-agentic-wallet:grant-session
  • /functor-agentic-wallet:send-tx
  • /functor-agentic-wallet:revoke-session

See the MCP Server docs for the full tool reference.


Let Claude write correct Functor code (Claude Skill)

Drop the skill into your project and Claude writes against the SDK correctly inside Cursor or Claude Code: correct function signatures, the right API surface, no hallucinated methods.

See the Claude Skill page for details.

)} {/* ── Codex ──────────────────────────────────────────── */} {tool === 'codex' && (

Add Functor context to Codex (AGENTS.md)

Codex reads AGENTS.md files in your project for instructions and context. Download the Functor skill file and append it so Codex writes against the SDK correctly.

> AGENTS.md'} />

After this, ask Codex to scaffold a wallet, grant a session, or build a trading agent. It will use the correct SDK surface and createClient API.


Operate wallets via MCP (Codex CLI)

Codex CLI supports MCP servers. Add the Functor server to your Codex config:

Then prompt Codex to create a wallet, grant a session, or send a transaction using the Functor tools. See the MCP Server docs for the full tool list.

)} {/* ── Other AI ───────────────────────────────────────── */} {tool === 'other' && (

Add Functor context to your AI tool

Any AI tool that can read a context or rules file works. Download the Functor skill file and add it wherever your tool reads project instructions.

Common destinations by tool:

  • Cursor: add to .cursor/rules/ or paste into the AI rules panel
  • Windsurf: add to .windsurfrules
  • Gemini CLI: add to GEMINI.md
  • Any tool with a system prompt: paste the contents directly

Add the full docs as context

For a richer context, use the docs' machine-readable file. Paste this URL into your AI or feed it as a context file:

Then try:

  • "Using these Functor docs, explain step by step how I would give an AI agent a wallet with a 50 USDC per day spending cap on BNB testnet."
  • "Walk me through letting an agent trade on a DEX with a cap, using Functor."
)}
) }

Getting Started

## Connect an AI tool ### Ask your AI (works with any tool) The docs publish `llms-full.txt`: a single machine-readable file any AI can read cleanly. Paste the URL and start asking questions, no setup required. Try prompts like: * "Using these Functor docs, explain how I would give an AI agent a wallet with a 50 USDC per day spending cap on BNB testnet." * "What can I do with Functor that I cannot do with a normal wallet?" * "Walk me through letting an agent trade on a DEX with a cap, using Functor." ### Build in your editor Pick your tool and follow the setup for it. ### Try it Once the MCP is connected, paste any of these into your Claude or Codex chat:
Values in orange are placeholders. Replace them with your actual values before sending.
See the [MCP Tools reference](/mcp/tools) for the full list of available commands. ### What's next [Give an agent a wallet and a policy](/use-cases/1-agent-wallet-policy) is the first concrete use case: create an agentic wallet and hand an AI a capped, expiring, revocable key. import { useState, useEffect } from 'react' import { PasskeyDemo } from '../../components/PasskeyDemo' export function WalletTabSwitcher() { const [tab, setTab] = useState('passkey') useEffect(() => { const show = (id) => { const passkey = document.getElementById('wallet-tab-passkey') const pk = document.getElementById('wallet-tab-private-key') if (passkey) passkey.style.display = id === 'passkey' ? '' : 'none' if (pk) pk.style.display = id === 'private-key' ? '' : 'none' } if (window.location.hash === '#private-key') { show('private-key') setTab('private-key') setTimeout(() => { document.querySelector('[data-wallet-tabs]')?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 50) } else { show('passkey') } }, []) const switchTab = (id) => { const passkey = document.getElementById('wallet-tab-passkey') const pk = document.getElementById('wallet-tab-private-key') if (passkey) passkey.style.display = id === 'passkey' ? '' : 'none' if (pk) pk.style.display = id === 'private-key' ? '' : 'none' setTab(id) history.replaceState(null, '', id === 'passkey' ? location.pathname : `#${id}`) } const btn = (id) => ({ padding: '0.5rem 1.4rem', border: '1px solid var(--vocs-color_border)', borderRadius: '6px', background: tab === id ? 'var(--vocs-color_backgroundDark)' : 'transparent', color: 'inherit', cursor: 'pointer', fontWeight: tab === id ? '600' : '400', fontSize: '0.95rem', transition: 'background 0.15s', }) return (
) }

Getting Started

## Create an agentic wallet An agentic wallet gives any authorized agent permissioned access to your onchain assets through Functor's global [Keystore](/concepts/keystore): a public onchain registry of authorized keys. You own the wallet. Agents get scoped, expiring, revocable keys. Every permission is verifiable by anyone, from any chain, with a free read. :::info[Which admin key type?] **Passkey**: biometric (Face ID, Touch ID, Windows Hello).
Runs in the browser using `navigator.credentials` (WebAuthn).
The private key never leaves your device's secure hardware.
Best for user-facing apps where a human holds the keys. **Private key**: programmatic, server side.
The key lives wherever your code runs: an env var, OS keychain, or encrypted file.
Functor never sees it. No API key, no off-chain service.
Best for autonomous agents and backend scripts. :::
### Try it live Each call hits the Functor SDK against BNB testnet. Your passkey stays on your device. ### [Build this into your own app →](/use-cases/1b-passkey-delegates-to-agent#build-this-in-your-own-app) Step-by-step code walkthrough for integrating a passkey wallet into your browser app, granting a session to an agent, and revoking it.
### Get started with the SDK #### What you need * **Node.js 18+** and npm, bun, or pnpm #### Step 1: Install ```bash npm install @functornetwork/agentic-wallet viem ``` #### Step 2: Generate your wallet The SDK creates a fresh private key and wallet address for you. Run this once. ```ts import { createClient, BNB_TESTNET, createPrivateKeySigner } from "@functornetwork/agentic-wallet"; const signer = createPrivateKeySigner(); console.log("Save this key:", signer._privateKey); // copy and store this somewhere safe const client = createClient({ chains: [BNB_TESTNET] }); const wallet = await client.createWallet({ signer }); console.log("Wallet address:", wallet.address); ``` :::info[Save your key before continuing] Copy the value printed for `signer._privateKey`. The SDK does not store it anywhere. You will need it to sign every future transaction. ::: #### Step 3: Fund your wallet Send BNB testnet funds to your `wallet.address` before executing.
Get testnet BNB from the faucet: testnet.bnbchain.org/faucet-smart
#### Step 4: Run a transaction The first `execute` activates the wallet and registers the admin key in the global [Keystore](/concepts/keystore). All in one transaction. ```ts const result = await client.execute({ wallet, signer, calls: { to: wallet.address as `0x${string}`, value: 0n }, }); console.log(result.status, result.transactionHash); ``` *** ### Already have a private key? If you are bringing an existing Ethereum key, use `signerFromPrivateKey` instead of generating a new one. Store it in a `.env` file at your project root: ```bash # .env — add to .gitignore, never commit this file PRIVATE_KEY=0xYourKeyHere ``` Install dotenv to load it: ```bash npm install dotenv ``` Then use it in your script: ```ts import 'dotenv/config' import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet"; const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); const wallet = await client.createWallet({ signer }); console.log(wallet.address); ```
*** * [Give an agent a wallet and a policy](/use-cases/1-agent-wallet-policy): grant an AI a scoped, expiring key on this wallet. * [Use a passkey wallet as admin, delegate to an agent](/use-cases/1b-passkey-delegates-to-agent): passkey admin grants a capped key to an AI. * [Connect an AI tool](/getting-started/build-with-claude): operate wallets via Claude, Codex, or any AI without writing code.

Getting Started

## Create a passkey wallet For consumer apps — wallets secured by Face ID, Touch ID, or Windows Hello. The user sees one biometric prompt; no seed phrase, no browser extension. The private key is generated and stored on the device's secure hardware and never leaves it. ### What you need * A **frontend project** — React, Vue, vanilla JS, or any framework that runs in a browser * **npm, bun, or pnpm** to install packages * A browser with biometric support — Chrome, Safari, and Firefox on modern devices all qualify This runs entirely in the browser. No backend or server required for basic setup. ### 1. Install ```bash npm install @functornetwork/agentic-wallet viem ``` `viem` is an Ethereum TypeScript library the SDK uses to talk to the blockchain. ### 2. Create the wallet ```ts import { createClient, BNB_TESTNET } from "@functornetwork/agentic-wallet"; // BNB_TESTNET is the default wallet execution network. // SEPOLIA is also supported; BASE_SEPOLIA is the L2 verification cache. const client = createClient({ chains: [BNB_TESTNET] }); const wallet = await client.createPasskeyWallet({ name: "MyApp", // shown in the OS passkey prompt rpId: "myapp.example", // your app's domain }); console.log(wallet.address); // the wallet's onchain address ``` The user sees one biometric prompt. After that, `wallet` is a fully usable smart-account handle and `wallet.signer` works with every client method. ### 3. Run a transaction ```ts const result = await client.execute({ wallet, signer: wallet.signer, calls: { to: "0xRecipient...", value: 1_000_000_000_000_000n }, // 0.001 BNB on BNB testnet }); console.log(result.status, result.transactionHash); ``` Each `execute` triggers one biometric prompt to sign. No popups, no extensions. ### 4. Handle returning users When a user returns — on any device with their passkeys synced — you can rebuild their wallet from onchain state without storing anything yourself: ```ts const wallet = await client.recoverFromPasskey({ rpId: "myapp.example" }); // OS shows the passkey picker, biometric prompt, done. ``` Two onchain reads, one biometric. No localStorage, no server, no seed phrase. ### What's next * [Delegate to an AI agent](/use-cases/1b-passkey-delegates-to-agent) — grant a capped, expiring key to an agent from this passkey wallet. * [Create a private-key wallet](/getting-started/private-key) — the agent-side path for scripts and backends.

Getting Started

## Create a private-key wallet For AI agents, backend scripts, and CLI tools — anywhere a biometric prompt doesn't make sense. You supply a private key; Functor creates and manages a smart account around it. No custody, no Functor API key, no off-chain service involved. ### What you need * **Node.js 18+** and npm, bun, or pnpm * **A private key** — a 32-byte hex string starting with `0x` that acts as the wallet's admin credential. You can use an existing key from MetaMask or any Ethereum wallet, generate one with `cast wallet new` (Foundry), or let the SDK generate one for you (shown below). Keep it secret — never commit it to source control. ### 1. Install ```bash npm install @functornetwork/agentic-wallet viem ``` `viem` is an Ethereum TypeScript library the SDK uses to talk to the blockchain. ### 2. Store your key Put the private key in an environment variable, not in your code: ```bash export PRIVATE_KEY=0xabc123... ``` ### 3. Create the wallet ```ts import { createClient, BNB_TESTNET, signerFromPrivateKey } from "@functornetwork/agentic-wallet"; // BNB_TESTNET is the default wallet execution network. // SEPOLIA is also supported; BASE_SEPOLIA is the L2 verification cache. const client = createClient({ chains: [BNB_TESTNET] }); const signer = signerFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`); const wallet = await client.createWallet({ signer }); console.log(wallet.address); // your wallet's onchain address ``` Don't have a key yet? Let the SDK generate one: ```ts const wallet = await client.createWallet(); // wallet.signer holds a freshly generated key. // Save it — you'll need it to sign future transactions. ``` Send funds to `wallet.address` before running a transaction — the wallet is counterfactual and has no balance until you fund it. On Sepolia, use a faucet like [sepoliafaucet.com](https://sepoliafaucet.com). On BNB testnet, use [testnet.bnbchain.org/faucet-smart](https://testnet.bnbchain.org/faucet-smart). ### 4. Run a transaction ```ts const result = await client.execute({ wallet, signer, calls: { to: "0xRecipient...", value: 1_000_000_000_000_000n }, // 0.001 BNB on BNB testnet }); console.log(result.status, result.transactionHash); ``` The first `execute` on a fresh wallet activates the smart account and registers the admin key onchain, then runs your call — all in one transaction. Every subsequent `execute` is just your calls. ### What's next * [Give an agent a wallet and a policy](/use-cases/1-agent-wallet-policy) — grant an AI a scoped, expiring key on this wallet. * [Create a passkey wallet](/getting-started/passkey) — the end-user path for browser apps.

Introduction

## How Functor is Different There are plenty of "agentic wallet" products. Most of them solve the same surface problem (letting an AI act on a user's wallet) but they all store the authorization state in places only their own stack can read. Functor inverts that. **Permissions are first-class onchain objects.** Any app, any agent, any chain that bridges to it can verify who is authorized to act on a wallet, without integrating with a wallet vendor's proprietary API. ### What "agentic wallet" usually means Most stacks combine: * a smart-account contract (ERC-4337 or similar) that holds funds * a session-key authorization mechanism (the agent signs intents, the wallet validates) * a vendor-side coordinator (a backend or proprietary contract) that knows which keys are authorized The third piece is where the silos live. Two agents on the same user's wallet can't verify each other unless they're both clients of the same vendor. A DEX can't check whether an incoming caller is authorized without integrating per-vendor. ### What Functor does Functor replaces the vendor-side coordinator with **Keystore: a public onchain registry**. | | Most agentic wallets | Functor | | ----------------------------------- | -------------------------------------------- | -------------------------------------- | | Who knows which keys are authorized | The vendor's backend or proprietary contract | Anyone reading the onchain registry | | Cross-agent verification | Requires both agents on the same platform | One `eth_call` from any client | | Cross-app integration | Per-vendor SDK | Read the registry | | Revocation | A vendor API call | One onchain transaction, global effect | | Custody | Often vendor-side or platform-specific | Local. The integrator's signer. | ### Cost model Keystore splits writes and reads. * **Writes** register or revoke a key. Done by the wallet itself, batched into the userOp the user was already submitting. * **Reads are free and unlimited.** Verifying whether a key is authorized is an `eth_call` against any RPC. An agent can check a million times per second at zero cost. This is what makes cross-agent and cross-app verification practical at scale. The party doing the verification never pays. ### Concretely, this enables
Agent-to-agent verification Two AIs acting on the same wallet can verify each other's authority onchain. No platform in between.
Cross-app authorization A DEX, lending protocol, or orderbook can check whether a key is allowed to act on a wallet, without integrating with any wallet vendor.
One-tx revocation Pulling a key from the registry is a single transaction. The effect is global and immediate; no off-chain coordination required.
A new class of agent services Users hire AI agents through onchain employment contracts. Anyone can verify what an agent is allowed to do, and revoke takes one tx.
### What stays the same * You still get a smart-account wallet with the standard session-key features (scoped permissions, time-bounded expiry, onchain validation). * You still use [`grantSession`](/sdk/grant-session), [`execute`](/sdk/execute), [`revokeSession`](/sdk/revoke-session). Same shape as any other session-key SDK. * Funds still live in the wallet's smart account. Functor never touches them. The difference is what surface knows about the authorizations, and that difference is what unlocks the cross-app, cross-agent properties. ### Read more * [Keystore](/concepts/keystore). The public registry, with code for verifying authorizations. * [Sessions](/concepts/sessions). Scoped delegations enforced onchain.

Concepts

## Keystore **Keystore is a public onchain registry.** For every Functor wallet, it stores which keys are currently authorized to act on it. Anyone (any app, any agent, any chain that bridges to it) can read this state and verify authority without a vendor in the middle. ### Why this matters Traditional wallet vendors keep their authorization state inside proprietary contracts or backend services. Two agents acting on the same user's wallet can't verify each other unless they're both clients of the same vendor. Keystore inverts this: authorization is the source of truth, onchain, vendor-neutral. This unlocks: * **Cross-agent verification.** An agent can verify another agent's authority before delegating work to it. * **Cross-app authorization.** A DEX, lending protocol, or orderbook can check whether a key is allowed to act on a wallet, without integrating with any wallet vendor. * **One-tx revocation.** Pulling a key from Keystore is a single transaction. The effect is global and immediate. ### Writes vs reads **Writes (onchain transactions):** * Registering a key (admin on first `execute`, or session on `grantSession`). Goes through the Controller. * Revoking a key (`revokeSession`). Calls Keystore directly. **Gated by access control** (`onlyKeyOwnerOrValidator`); only the wallet itself or a designated validator can revoke, so there is no open write surface. Revocation is **monotonic**: a revoked key cannot be reactivated. **Reads (eth\_call, free, unlimited):** * Checking which keys are active on a wallet. * Verifying whether a given key is currently authorized. * Looking up a key's stored public bytes. Reads are off-chain RPC calls. An agent can verify authorization a million times per second, from any RPC, without paying anything. This is what makes cross-agent and cross-app verification practical at scale. ### How a read works ```ts import { createPublicClient, http, keccak256 } from "viem"; import { bscTestnet } from "viem/chains"; import { BNB_TESTNET } from "@functornetwork/agentic-wallet"; const client = createPublicClient({ chain: bscTestnet, transport: http(BNB_TESTNET.publicRpcUrl), }); const KEYSTORE_ABI = [{ name: "getActiveKeys", type: "function", stateMutability: "view", inputs: [{ name: "user", type: "address" }], outputs: [{ type: "bytes32[]" }], }] as const; const active = await client.readContract({ address: BNB_TESTNET.keyStore, abi: KEYSTORE_ABI, functionName: "getActiveKeys", args: [walletAddress], }); const authorized = active.includes(keccak256(sessionPublicKey)); ``` A single eth\_call answers: *is this key allowed to act on this wallet right now?* ### When the SDK writes to Keystore | SDK call | Write? | Notes | | --------------------------------- | ------ | -------------------------------------------------------------------------------------------------- | | `createWallet` | No | Wallet is counterfactual until first `execute` | | `createPasskeyWallet` | No | Same as above | | First `execute` on a fresh wallet | Yes | Admin key auto-registered via `initialRegisterKey`, batched into your userOp | | Subsequent `execute` calls | No | Just your calls | | `grantSession` | Yes | Session public key registered, batched with onchain authorization | | `revokeSession` | Yes | Pulls the session's authority. Revocation is monotonic: once revoked, a key cannot be reactivated. | | `recoverFromPasskey` | No | Pure read, two eth\_calls | All writes are batched into the same userOp as your actual call. You never call Keystore directly through the SDK. ### Reaching other chains The L1 Keystore is the source of truth. For another chain to honor an authorization, the registry state has to be mirrored to a **cache contract** on that chain. A cache works as a verifier: it accepts a storage proof against the L1 Keystore, walks it from the L1 block hash exposed by the L2's `L1Block` predeploy down to the relevant slot, and stores the result locally. Once cached, the L2 reads authorization with a single `eth_call`, with no L1 round-trip and no bridge message. ### Chains | Network | Chain ID | Role | | ----------------------- | -------- | --------------------------------------------------------------------- | | BNB Smart Chain Testnet | 97 | Standalone Keystore + wallet execution; SDK default | | Ethereum Sepolia | 11155111 | L1 Keystore source of truth for cross-chain proofs + wallet execution | | Base Sepolia | 84532 | L2 Keystore cache for cross-chain verification | All three SDK exports are live today: `BNB_TESTNET`, `SEPOLIA`, and `BASE_SEPOLIA`. BNB testnet and Sepolia support wallet writes. Base Sepolia is the supported L2 cache/read target for verifying Sepolia-granted sessions cross-chain. ## Networks & Addresses Functor is multi-chain. The SDK ships a config per network, importable from `@functornetwork/agentic-wallet`. **BNB Smart Chain Testnet is the default**; Sepolia remains supported for wallet execution and L1 cross-chain proofs; Base Sepolia is supported as the L2 Keystore cache for cross-chain verification. ### Supported SDK exports | Export | Network | Chain ID | Use | | -------------- | ----------------------- | -------- | ---------------------------------------------------------------------- | | `BNB_TESTNET` | BNB Smart Chain Testnet | 97 | Default wallet execution network with a standalone Keystore | | `SEPOLIA` | Ethereum Sepolia | 11155111 | Wallet execution network and L1 Keystore source for cross-chain proofs | | `BASE_SEPOLIA` | Base Sepolia | 84532 | L2 Keystore cache for cross-chain session verification | ### BNB Smart Chain Testnet: `BNB_TESTNET` * **Chain id:** 97 * **Public RPC:** `https://bsc-testnet-rpc.publicnode.com` * **Explorer:** [https://testnet.bscscan.com](https://testnet.bscscan.com) * **Relay:** `https://relay.functor.sh` KeyStore contracts: | Contract | Address | | ------------------ | -------------------------------------------- | | KeyStore | `0x2F77991da4a66D1EE83f0622a4e6A2E94c89BCbE` | | KeyStoreController | `0x30b34f10F0a271dAFe6a0A900bCB2Cb94927e39d` | ### Ethereum Sepolia: `SEPOLIA` * **Chain id:** 11155111 * **Public RPC:** `https://ethereum-sepolia-rpc.publicnode.com` * **Explorer:** [https://sepolia.etherscan.io](https://sepolia.etherscan.io) * **Relay:** `https://rpc.porto.sh` | Contract | Address | | ------------------ | -------------------------------------------- | | KeyStore | `0xfBDe00E03Bf281bAc666043B14dBb8FAbcf22b14` | | KeyStoreController | `0x8bBabE825EcFCBB32f7B60D973FbA0B923b8e782` | ### Base Sepolia: `BASE_SEPOLIA` Used for cross-chain session-key verification when operating on Sepolia. Not used by BNB. | Contract | Address | | ------------- | -------------------------------------------- | | KeyStoreCache | `0x127D61dff47cC4CCC073d5134830f32d01BAeEB3` | Chain id 84532 · RPC `https://base-sepolia-rpc.publicnode.com`. > Addresses are the source of truth in the Keystore repo's deployment manifests > (`deployments/*.json`) and the SDK's `packages/wallet/src/config.ts`. If contracts are > redeployed, update both in lockstep.

Concepts

## Sessions A **session** is a scoped, time-bounded delegation from a wallet's admin key to another key. The session key can act on the wallet, but only within the granted permissions, and only until the expiry. Permissions are enforced onchain. A session that tries to call a contract outside its allowlist, or spend beyond its cap, reverts at validation time. There is no off-chain trust assumption. ### Anatomy of a session ```ts type Session = { walletAddress: Address; // the wallet this session can act on signer: Signer; // the session key (agent signs with this) publicKey: Hex; // identifier onchain permissions: SessionPermissions; expiry: number; // unix epoch seconds }; type SessionPermissions = { calls?: readonly CallPermission[]; // allowed contracts / signatures spend?: readonly SpendPermission[]; // per-token rolling caps }; ``` ### Permission shapes **Call permissions** restrict which contracts and methods the session can hit: ```ts calls: [ { to: "0xUniswapRouter..." }, // any method on this contract { signature: "transfer(address,uint256)" }, // any contract, this method { signature: "swap(...)", to: "0xPool" }, // both, AND semantics ] ``` **Spend permissions** cap how much the session can move per token, per rolling period: ```ts spend: [ { limit: 100_000_000n, period: "day", token: "0xUSDC..." }, // 100 USDC/day { limit: 10n ** 16n, period: "hour" }, // 0.01 ETH/hour (native) ] ``` ### Lifecycle | Stage | Function | Keystore impact | | ------ | ----------------------------------------- | ----------------------------------------------------------------------------------------------- | | Grant | [`grantSession`](/sdk/grant-session) | Write. Session public key registered. | | Use | [`execute(session, calls)`](/sdk/execute) | None. | | Verify | Any client reads `getActiveKeys` | None. Free, unlimited. | | Revoke | [`revokeSession`](/sdk/revoke-session) | Write (gated by `onlyKeyOwnerOrValidator`). Session revoked (monotonic: cannot be reactivated). | | Expire | Automatic at `expiry` | None. No transaction. | Reads are free and unlimited, which is what makes cross-agent and cross-app verification practical. ### Notes * **Sessions must be byte-exact on execute.** The onchain validator matches `permissions + expiry + role + publicKey` exactly to the hash committed at grant time. Persist the `Session` object verbatim. Sloppy JSON round-trips (bigints to numbers, key reordering) break the match. * **`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.