Skip to content
Tenzro
← All tutorials
Tutorial · Keys and identity

Install an ERC-7579 session key

Give an agent or backend a short-lived key, certified by your passkey, that can only call the contracts and functions you allow, within value caps and a time window.

Advanced20 min

Your passkey should not be asked to approve every call an agent makes, and an agent should never hold your root key. A session key solves both. It is an ordinary key generated in memory by the agent, and it becomes valid on your smart account only after your passkey certifies it. The certificate states exactly what the key may do:

  • which contracts it may call (target allowlist);
  • which functions it may call (4-byte selector allowlist);
  • how much value it may move per call and in total;
  • when it becomes valid and when it expires.

The session-key validator is an ERC-7579 validator module on your smart account. Every UserOperation signed by the session key is checked against those limits, and when valid_until passes the key stops working without any further action.

Prerequisites

  • A passkey smart account from Build a passkey wallet, and its helpers (hybridAssert, hex, fromHex, client).
  • A contract the agent should be allowed to call, and the selectors it needs.
  • Node.js 20 or later with @noble/curves installed.
bash
npm install @noble/curves

1. Generate the session key in memory

The agent generates its own key. It never leaves the agent process and it is never written to disk; when the agent restarts it generates a new one and asks you to certify it again.

ts
import { ed25519 } from "@noble/curves/ed25519.js";

const sessionSecret = crypto.getRandomValues(new Uint8Array(32));
const sessionPub = ed25519.getPublicKey(sessionSecret); // 32 bytes
console.log("session key:", hex(sessionPub));

The agent sends only sessionPub to the wallet that will certify it.

2. Choose the scope

Decide what the key may do. This example lets an agent pay an inference provider's settlement contract and nothing else:

ts
const now = Math.floor(Date.now() / 1000);

const scope = {
  allowed_targets: ["0x<settlement-contract>"],
  allowed_selectors_hex: ["<4-byte selector>"],  // e.g. the contract's pay function, no 0x
  max_value_per_call_wei: "1000000000000000000", // 1 TNZO
  max_total_value_wei: "20000000000000000000",   // 20 TNZO over the session
  valid_after_unix: now,
  valid_until_unix: now + 60 * 60,               // one hour
  label: "inference agent, 1h",
};

Keep the window short and the caps tight. An empty allowed_targets list means any target, so always name the contracts.

3. Certify the key with your passkey

Granting a session key is a custody change. The node issues a single-use challenge bound to your account, the operation and this exact session key; your passkey signs it with both legs, P-256 and ML-DSA-65, in one touch.

ts
const ch = await client.passkeyRpc.createCustodyChallenge({
  account_address: ACCOUNT,
  operation: "grant_session_key",
  target_hex: hex(sessionPub),
});

const proof = await hybridAssert(credentialId, fromHex(ch.challenge_hex));

const grant = await client.passkeyRpc.grantSessionKey({
  account_address: ACCOUNT,
  session_pubkey_hex: hex(sessionPub),
  ...scope,
  authorization: {
    challenge_id: ch.challenge_id,
    credential_id_hex: hex(credentialId),
    assertion: proof.assertion,
    ml_dsa_signature_hex: proof.mlDsaSignatureHex,
  },
});
console.log(grant);

Expected output:

json
{
  "account_address": "0x...",
  "session_pubkey_hex": "...",
  "valid_after_unix": 1790000000,
  "valid_until_unix": 1790003600
}

Because the challenge names the session key, a certificate for one key cannot be replayed to install another. Without a valid passkey authorisation the call is refused.

The CLI exposes the same grant:

bash
tenzro passkey grant-session-key \
  --account-address $ACCOUNT \
  --session-pubkey-hex <session-pub> \
  --targets 0x<settlement-contract> \
  --selectors <4-byte-selector> \
  --max-per-call 1000000000000000000 \
  --max-total 20000000000000000000 \
  --valid-until $(( $(date +%s) + 3600 )) \
  --label "inference agent, 1h" \
  --rpc https://rpc.tenzro.xyz

4. Confirm the module is installed

bash
tenzro passkey get-smart-account --account-address $ACCOUNT --rpc https://rpc.tenzro.xyz

installed_validators lists every validator module on the account with its module address and ERC-7579 type id (1 for validators). Check tenzro erc7579 is-installed for a specific module address.

5. Sign UserOperations with the session key

The agent builds a UserOperation that calls an allowed target and selector, signs the operation hash with the session key and submits it with eth_sendUserOperation:

ts
const sig = ed25519.sign(userOpHash, sessionSecret);
// place `sig` in userOp.signature as the session-key validator expects,
// then submit with eth_sendUserOperation

Every installed validator on the account must approve an operation. The session-key validator rejects it if the target or selector is not on the list, if the value exceeds either cap, or if the current time is outside the window. Operations the session key cannot approve still need your passkey.

6. Revoke early

Let the key expire, or withdraw it at once. Revocation is a custody change too, so it is authorised by your passkey:

bash
tenzro passkey revoke-session-key --account-address $ACCOUNT --rpc https://rpc.tenzro.xyz
json
{ "account_address": "0x...", "revoked": true }

When the agent is done, it should also zero its copy of the secret:

ts
sessionSecret.fill(0);

Next steps

  • Set account-wide per-transaction and daily caps: Smart-account policies.
  • Let an agent pay for inference with stablecoins: Payments and x402.
  • See how validators use the same pattern: votes are signed with an in-memory session key the hardware certifies once per epoch. Hardware-rooted keys.