# 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 `| 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. |
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.
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.
{parts.map((part, i) => (
{part}
{i < parts.length - 1 && "0xYourContract"}
))}
{address}
)
}
Build with Functor
## Give an agent a wallet and a policycreateWallet, 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.
0xYourContract with your contract address.Build with Functor
## Use a passkey wallet as admincreatePasskeyWallet, 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 with Functor
## Let an agent trade on a DEX, cappedgrantSession 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 agentsgrantSession 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 anywheregetActiveKeys 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 chainsgrantSession 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): PromiseSDK
## 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): PromiseSDK
## 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): PromiseSDK
## 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): PromiseSDK
## 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): PromiseMCP 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_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
)}
{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 (
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-sessionSee the MCP Server docs for the full tool reference.
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 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.
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.
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.
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/rules/ or paste into the AI rules panel.windsurfrulesGEMINI.mdFor a richer context, use the docs' machine-readable file. Paste this URL into your AI or feed it as a context file:
Then try:
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.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).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. |
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.