Skip to content

How to Build Wallet Permissions with Session Keys

Joan Alavedra, Co-Founder at Openfort•7 min read
Wallet permissions architecture for delegated agent execution

Wallet permissions let you give an agent spending power without giving up control. The pattern is straightforward: register a temporary session key on a user's smart account, scope its permissions, let the agent sign transactions with it, and let it expire. No custody transfer. No infinite approvals. Just time-boxed, revocable delegation.

See Openfort's security model — non-custodial key management with sharded signers and self-hostable infrastructure.

This guide walks through the architecture and implementation. We'll build a system where a backend agent executes DCA (Dollar-Cost Averaging) trades on behalf of users using 5-minute session keys.

The Permission Model

Smart accounts store a registry of authorized keys. Each key has three properties packed into a single uint256:

  • isAdmin (bit 200): Whether the key has full account control
  • expiration (bits 160-199): Unix timestamp when the key stops working
  • hook (bits 0-159): Optional validator contract for custom logic

An admin key can do anything—register other keys, change settings, move funds. A non-admin key can only sign transactions within its scope. For agent delegation, you always want non-admin.


_10
// Pack key settings into a single uint256
_10
function packSettings(settings: KeySettings): bigint {
_10
const admin = settings.isAdmin ? BigInt(1) : BigInt(0)
_10
const exp = BigInt(settings.expiration)
_10
const hook = BigInt(getAddress(settings.hook))
_10
return (admin << BigInt(200)) | (exp << BigInt(160)) | hook
_10
}

The key itself is identified by a hash of its type and public key: keccak256(abi.encode(keyType, keccak256(publicKey))). This means you can register multiple keys of different types—passkeys (P256), browser wallets (WebAuthnP256), or backend signers (Secp256k1)—all on the same account.

How It Works

The flow has two sides: the user grants permission, and the agent uses it.


_14
┌──────────────┐ ┌───────────────────┐ ┌─────────────────┐
_14
│ User │────→│ Smart Account │←────│ Agent (Backend) │
_14
│ (Owner key) │ │ (Key Registry) │ │ (Session key) │
_14
└──────────────┘ └───────────────────┘ └─────────────────┘
_14
│ │ │
_14
│ 1. Register key │ │
_14
│ 2. Set expiration │ │
_14
│─────────────────────→│ │
_14
│ │ 3. Sign UserOp │
_14
│ │←───────────────────────│
_14
│ │ 4. Verify key │
_14
│ │ 5. Check expiration │
_14
│ │ 6. Execute │
_14
│ │───────────────────────→│

Step 1-2: The user registers the agent's address as a Secp256k1 key on their account and sets a 5-minute expiration. This happens in a single transaction containing two calls:


_19
// Register the agent's address as a session key
_19
const agentKey = {
_19
keyType: KeyType.Secp256k1,
_19
publicKey: padHex(agentAddress, { size: 32 }),
_19
}
_19
_19
const registerCall = encodeRegisterKey(agentKey)
_19
const keyHash = hashKey(agentKey)
_19
_19
// Set permissions: non-admin, 5-minute expiration, no hook
_19
const updateCall = encodeUpdateKeySettings(keyHash, {
_19
isAdmin: false,
_19
expiration: Math.floor(Date.now() / 1000) + 300,
_19
hook: ZERO_ADDRESS,
_19
})
_19
_19
// Execute both in a single transaction
_19
const txData = encodeExecute([registerCall, updateCall])
_19
await sendTransactionAsync({ to: accountAddress, data: txData })

Step 3-6: Within the permission window, the agent signs UserOperations using its session key. The smart account validates the signature, confirms the key hasn't expired, and executes the operation. All gas is sponsored by a paymaster—users pay nothing.

Backend Agent Execution with Session Keys

On the backend, a cron job picks up active agents and executes trades. The agent creates a "session account"—a Viem smart account instance configured with the session key instead of the owner key. This is powered by an Openfort backend wallet:


_10
// Create a session account for the agent
_10
const sessionAccount = await createCaliburSessionAccount({
_10
client: publicClient,
_10
signer: agentSigner, // Backend wallet from Openfort
_10
address: userAddress, // The user's smart account
_10
keyHash: agentKeyHash, // Hash of the registered session key
_10
})

This session account can construct and sign UserOperations that will be accepted by the user's smart account—but only while the session key is valid.

The actual execution bundles multiple calls into a single UserOperation:


_22
// Execute DCA: transfer USDC out, receive tokens back
_22
const userOp = await bundlerClient.sendUserOperation({
_22
account: sessionAccount,
_22
calls: [
_22
{
_22
to: USDC_ADDRESS,
_22
data: encodeFunctionData({
_22
abi: erc20Abi,
_22
functionName: 'transfer',
_22
args: [treasuryAddress, dcaAmount],
_22
}),
_22
},
_22
{
_22
to: TOKEN_ADDRESS,
_22
data: encodeFunctionData({
_22
abi: mintAbi,
_22
functionName: 'mint',
_22
args: [userAddress, tokenAmount],
_22
}),
_22
},
_22
],
_22
})

The bundler submits the operation, the paymaster sponsors gas, and the smart account validates that the agent's session key is still active before executing.

Security Properties of Wallet Permissions

This model gives you three things simultaneously:

Autonomy: The agent executes without the user approving each transaction. Once the key is registered, the cron job handles everything.

Control: The user sets the rules. Five-minute expiration means the worst-case exposure window is five minutes. Non-admin status means the agent can't register other keys or change settings. The user can revoke the key at any time with a single transaction.

Non-custody: The platform never holds user funds. The agent's signing key lives in a backend wallet provisioned by Openfort, but it can only sign operations on the user's account within the permission window. The user's owner key always retains ultimate authority.

The key insight: permissions are enforced at the smart account level, not by the agent's good behavior. Even if the agent's backend is compromised, the attacker can only act within the permission scope—and only until the key expires.

Extending Wallet Permissions Beyond Time-Based Expiration

The DCA example uses a simple time-based expiration, but the programmable wallet controls model supports richer constraints:

Hooks: The hook field in key settings points to a validator contract. This contract runs custom logic before every transaction the key signs. You could enforce spending limits, restrict which contracts the key can interact with, or require additional signatures for high-value operations.

Multiple keys: An account can have many active keys simultaneously. You might have one session key for a trading agent (5-minute window), another for a subscription service (30-day window), and your owner passkey for manual transactions.

Programmatic revocation: Keys can be revoked proactively. If your monitoring detects unusual behavior from an agent, revoke its key immediately—don't wait for expiration.

The Wallet Permissions Technology Stack

The example uses:

  • Openfort for embedded wallets, backend wallet provisioning, and gas sponsorship
  • Calibur smart accounts (EIP-7702 + EIP-4337) for the key registry and permission enforcement
  • Next.js for the frontend and API routes
  • Vercel Cron for automated agent execution
  • Upstash Redis for DCA configuration storage

The architecture separates concerns cleanly: the frontend handles authentication and permission granting, the backend handles agent execution, and the smart account handles permission enforcement.


_21
┌─────────────────────────────────────────┐
_21
│ FRONTEND (React 19 + Wagmi) │
_21
│ • Email OTP authentication │
_21
│ • Passkey wallet creation │
_21
│ • Permission granting UI │
_21
├─────────────────────────────────────────┤
_21
│ BACKEND (Next.js API Routes) │
_21
│ • Agent wallet provisioning (Openfort) │
_21
│ • Cron-triggered DCA execution │
_21
│ • DCA config management (Redis) │
_21
├─────────────────────────────────────────┤
_21
│ SMART ACCOUNT (Calibur) │
_21
│ • Key registry │
_21
│ • Permission enforcement │
_21
│ • UserOp validation + execution │
_21
├─────────────────────────────────────────┤
_21
│ INFRASTRUCTURE │
_21
│ • Bundler (UserOp submission) │
_21
│ • Paymaster (gas sponsorship) │
_21
│ • Base Sepolia (testnet) │
_21
└─────────────────────────────────────────┘

Wallet permissions turn the custody problem into a configuration problem. Instead of choosing between user control and agent autonomy, you define the boundary—and the smart account enforces it.

Ready to implement wallet permissions in your application? Explore the Openfort documentation or dive into the session keys guide for a deeper look at permission scoping.

Share this article

Related Articles

  1. Agent wallet identity, permissions, and compliance

    Three questions decide whether an agent can be trusted with money: who it acts as, what it may do, and what you can prove afterwards.

  2. How to Build an Agent Wallet

    Build an agent wallet with Openfort backend wallets: create a TEE-secured wallet, add EIP-7702 delegation, sponsor USDC gas, and send on Base Sepolia.

  3. Technical Dive: Key Permissions for Accounts

    A guide to onchain and offchain permission systems: granular access control with spending limits, contract whitelisting, and time-based restrictions.

  4. AI Agent Wallet Compliance: Custody and Control Requirements

    The controls an enterprise AI agent wallet needs: custody, policy, identity, audit, screening. Openfort, Circle, Coinbase, Fireblocks, and Ledger scored.

  5. Onboard new users with Guest accounts

    You no longer need to ask users to authenticate to have a wallet pregenerated. Let users dive into your app instantly, no login required.

  6. How to use Token Bound Account (ERC-6551)

    Learn the ins and outs of the ERC6551 with Openfort's smart accounts and learn how to make them both together for your game.

  7. How to Build a Telegram Bot with Built-In Crypto Wallets

    Build a Telegram bot where /start creates a wallet and /send moves USDC gas-free, using Openfort backend wallets, plus the Mini App path for React.

  8. How to build Telegram mini-app with Unity WebGL

    Learn how to integrate Openfort into a Unity WebGL Telegram Mini-App. A practical guide to real-time wallet flows and onchain features in gaming apps

Ship your first wallet in minutes

No credit card required • 2,000 operations free