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 on-chain. 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
type Session = {
walletAddress: Address; // the wallet this session can act on
signer: Signer; // the session key (agent signs with this)
publicKey: Hex; // identifier on-chain
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:
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:
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 | Write. Session public key registered. |
| Use | execute(session, calls) | None. |
| Verify | Any client reads getActiveKeys | None. Free, unlimited. |
| Revoke | revokeSession | 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 on-chain validator matches
permissions + expiry + role + publicKeyexactly to the hash committed at grant time. Persist theSessionobject verbatim. Sloppy JSON round-trips (bigints to numbers, key reordering) break the match. permissions.callsomitted = unrestricted. If you don't passcalls, the session can call any contract within its spend cap. Set both unless that's truly what you want.