ERC-7579 modules
The Kernel account and its modules - OwnableValidator, AgentGuard, and Smart Sessions - and how each signing lane works.
Every Botanary account is the owner's self-custodial ERC-7579 smart account - counterfactual until its first operation, deployed the moment that operation is signed and relayed. It's built on Kernel, and its behavior comes almost entirely from three ERC-7579 modules installed on it.
Kernel and the root validator
The account's root validator is an OwnableValidator - a Kernel module that turns the account into an
M-of-N multisig over device keys, with the owner's own Privy embedded EOA as the first device. A
gas-permit schema in the contract cites a Kernel version of 0.3.3 as an example, and a signing-code
comment is specific about the version this targets: "Kernel v3.3's root ECDSA validator" - "nonce mode
00 routes to the root validator." That detail matters for how signatures get assembled; see
Signing envelopes below.
Devices: the OwnableValidator set
GET /account/devicesreads the live co-signer set and its M-of-N threshold directly on-chain. A counterfactual (never-deployed) account reads back as a dormant 1-of-1, whose sole device is the owner's own signer.POST /account/devicesbuilds anOwnableValidator.addOwnercall.POST /account/devices/thresholdbuilds anOwnableValidator.setThresholdcall.POST /account/devices/{device}/removebuilds anOwnableValidator.removeOwnercall, with the linked-list predecessor it needs derived live on-chain rather than tracked separately.
AgentGuard: the on-chain policy hook
AgentGuard is the module that enforces the account's own rules - caps, allowlists, denylists, and the
freeze switch - and its own docs put the enforcement boundary bluntly: the account "reverts anything
outside the mandate on-chain." POST /account/freeze builds an AgentGuard.setFrozen(true) call: an
account-wide kill switch that's risk-reducing and gasless, routed through a revoke-only paymaster.
POST /account/unfreeze builds the corresponding setFrozen(false) call.
"Hook" here describes how AgentGuard behaves - it isn't a cited formal ERC-7579 module-type classification. Whether AgentGuard is formally registered as a hook module versus an executor module isn't confirmed in the source available to us.
Smart Sessions: delegations and the panic lane
Delegations - scoped, revocable session-key grants - are built on the SmartSession module.
POST /delegations/build and POST /delegations build and relay a client-signed enableSessions
call that installs a new session with its bounds. A hired agent's mandate goes further: MandateBuild
describes its enableUserOp as "ONE op, and it is the WHOLE grant" - a single Kernel batched execute
that writes every bound (venue, recipient, per-action cap) into the mandate executor and then calls
enableSessions, atomically, so there's never a window where a session key is live before its bounds
are in place.
The account's optional "panic" lane reuses the same module for a different purpose: installing it
builds a scoped Smart Sessions session - a 1-of-N over the account's own device keys - bound to call
nothing but AgentGuard.setFrozen(true). Once installed, any single device key can freeze the account
by signing that one pre-scoped action, without waiting on the account's normal signing threshold.
MandateExecutor is a separate on-chain module from the session mechanism itself: it's where a mandate's real bounds (which venues, which recipients, which per-action caps) actually live, set through its own configuration calls rather than as ordinary session policy. A delegation record also carries whether an account-wide freeze stops it too (its freeze policy) - see Delegations for the full grant/revoke lifecycle from a product angle.
The frontend's v1 session-key model is a testnet demonstration seam, not production key management:
a fresh session key is generated in the browser before a grant, and its private key is persisted in
localStorage, keyed by its own address. There's no autonomous agent signing here yet - a human
approves every session-key action.
Chain capability tiers
Not every chain an account can use supports every module. Chains are grouped into three tiers, and
the same grouping drives both GET /chains and the authority check on every build path:
- Botanary-tier - AgentGuard is deployed, so freezing, delegating, and rules all work.
- Basic-tier - a bundler is available but AgentGuard isn't, so sends work but delegation doesn't.
- Watch-tier - reads only.
Signing envelopes
Three different signers touch a Botanary account, and each one signs a slightly different envelope:
-
Owner-lane operations (deploy, grant, freeze, revoke) are signed by the Privy embedded EOA over the raw
userOpHash, EIP-191-wrapped. For Kernel v3.3's root validator, the resultinguserOp.signatureis exactly that bare 65-byte signature - nonce mode00routes straight to the root validator, no extra prefix needed. -
Session-lane operations are signed by the session key over the raw
userOpHashdirectly, then wrapped in the Smart Sessions USE-mode envelope before being attached to the operation:export async function signSessionOp( sessionPrivateKey: Hex, userOpHash: Hex, permissionId: Hex, ): Promise<Hex> { const account = privateKeyToAccount(sessionPrivateKey); const raw = await account.sign({ hash: userOpHash }); // SmartSessionMode.USE = 0x00 - encodePacked(bytes1, bytes32, bytes). return encodePacked(['bytes1', 'bytes32', 'bytes'], ['0x00', permissionId, raw]); }The account's Smart Sessions validator recovers that raw hash and re-checks the delegation's policy against it during validation.
-
The USDC gas-permit lane is the odd one out, and it's worth getting right. Paying gas in USDC goes through a Circle Paymaster, authorized by an EIP-2612 permit whose
owneris the smart account itself - verified through Kernel's ERC-1271 wrapper rather than the permit's own EIP-712 hash. That wrapper recovers over the raw digest, with no EIP-191 wrap, because it'sOwnableValidatoracting as the root validator, not the legacy validator it replaced. Sign the permit digest raw, only then assemble the paymaster data, recompute the userOp hash from that, and sign that one EIP-191-wrapped - the order matters, and it's a two-signature flow, not one.
Sign order and wrapping matter here: sign the gas-permit digest raw, and the userOp hash EIP-191. Getting either one backwards makes the root validator's signature recovery fail during validation (the operation dies with an AA33 revert) - this has been directly observed on testnet, not just theorized.