Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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

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 callWrite?Notes
createWalletNoWallet is counterfactual until first execute
createPasskeyWalletNoSame as above
First execute on a fresh walletYesAdmin key auto-registered via initialRegisterKey, batched into your userOp
Subsequent execute callsNoJust your calls
grantSessionYesSession public key registered, batched with on-chain authorization
revokeSessionYesPulls the session's authority. Revocation is monotonic — once revoked, a key cannot be reactivated.
recoverFromPasskeyNoPure 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

NetworkChain IDRole
Ethereum Sepolia11155111L1 Keystore (source of truth)
Base Sepolia84532L2 cache

Sepolia + Base Sepolia are live today. Additional cache deployments on other EVM chains land here as they ship.