Keystore
Keystore is a public on-chain 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, on-chain, 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 (on-chain transactions):- Registering a key (admin on first
execute, or session ongrantSession). 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.
- 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
import { createPublicClient, http, keccak256 } from "viem";
import { sepolia } from "viem/chains";
import { SEPOLIA } from "@functornetwork/agentic-wallet";
const client = 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 client.readContract({
address: SEPOLIA.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 on-chain 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 — no L1 round-trip, no bridge message.
Chains
| Network | Chain ID | Role |
|---|---|---|
| Ethereum Sepolia | 11155111 | L1 Keystore (source of truth) |
| Base Sepolia | 84532 | L2 cache |
Sepolia + Base Sepolia are live today. Additional cache deployments on other EVM chains land here as they ship.