Development of Session Keys System for Gasless UX
Standard Web3 app UX: each user action — separate MetaMask signature. Playing a game — sign each move. Trading on DEX — sign each swap. This is unacceptable for mass product. Session keys solve the problem: user signs one transaction (opening session), then application acts on their behalf within set limits, without constant confirmations.
This is only possible with Account Abstraction (ERC-4337) — because Smart Account can have multiple authorized signers with different rights, unlike EOA where there's only one private key.
How Session Keys Work
Architecture: validator plugin in Smart Account. Account checks signature through validator — main ECDSA validator requires user key. Session key validator requires only session key, but checks restrictions:
User's main key:
→ Validator: ECDSAValidator(userKey)
→ Can do anything
Session key:
→ Validator: SessionKeyValidator
→ Checks: correct signer + restrictions met
Session key validator implementations:
Kernel (ZeroDev) — most used. Session key validator with built-in permission modules: contract limitation, function selection, parameter ranges, spending limits.
Biconomy Smart Account — own Session Key Manager module.
Safe + safe-modules — Session Keys via Safe Plugin.
Implementation on ZeroDev Kernel
import {
createKernelAccount,
createKernelAccountClient,
createZeroDevPaymasterClient,
} from '@zerodev/sdk';
import {
signerToSessionKeyValidator,
ParamOperator,
oneAddress,
} from '@zerodev/session-key';
import { signerToEcdsaValidator } from '@zerodev/ecdsa-validator';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { parseAbi, encodeFunctionData } from 'viem';
// 1. Create temporary session key (ephemeral keypair)
const sessionPrivateKey = generatePrivateKey();
const sessionKeySigner = privateKeyToAccount(sessionPrivateKey);
// 2. Define session permissions
const sessionKeyValidator = await signerToSessionKeyValidator(publicClient, {
signer: sessionKeySigner,
validatorData: {
validUntil: Math.floor(Date.now() / 1000) + 86400, // 24 hours
validAfter: 0,
paymaster: oneAddress, // allow any paymaster
permissions: [
{
target: GAME_CONTRACT_ADDRESS,
valueLimit: BigInt(0), // can't send ETH
abi: parseAbi(['function makeMove(uint8 x, uint8 y) external']),
functionName: 'makeMove',
args: [
{ operator: ParamOperator.LESS_THAN, value: 8n }, // x < 8
{ operator: ParamOperator.LESS_THAN, value: 8n }, // y < 8
],
},
],
},
});
// 3. Create account with session key validator
const account = await createKernelAccount(publicClient, {
plugins: {
sudo: await signerToEcdsaValidator(publicClient, { signer: userSigner }),
regular: sessionKeyValidator,
},
kernelVersion: KERNEL_V3_1,
});
// 4. Save session key (in IndexedDB or memory)
const serializedSessionKey = await sessionKeyValidator.serializeSessionKey();
// → pass to backend or store locally
After session creation — backend (or browser itself) can sign transactions with session key without user interaction:
// Using saved session (e.g., on server)
const restoredValidator = await deserializeSessionKeyValidator(
publicClient,
{ serializedSessionKey },
);
const kernelClient = createKernelAccountClient({
account,
chain: arbitrum,
bundlerTransport: http(BUNDLER_RPC),
paymaster: createZeroDevPaymasterClient({ ... }),
});
// Transaction without user signature
const txHash = await kernelClient.sendTransaction({
to: GAME_CONTRACT_ADDRESS,
data: encodeFunctionData({
abi: parseAbi(['function makeMove(uint8 x, uint8 y) external']),
functionName: 'makeMove',
args: [3n, 4n],
}),
});
// Gas paid by Paymaster, user doesn't sign
Gasless: Paymaster Integration
Session keys remove need to confirm each operation. Paymaster removes need to hold native token for gas. Together — completely gasless UX.
ERC-4337 Paymaster: smart contract sponsoring gas for UserOperations. Two types:
Verifying Paymaster: calls your backend before each UserOp for check. Backend signs permission. Flexible: you control which operations to sponsor.
ERC-20 Paymaster: accepts payment in ERC-20 tokens (USDC) instead of ETH. User pays gas in USDC, paymaster converts and pays in ETH.
// Verifying Paymaster backend logic
export async function signPaymasterRequest(
userOp: UserOperation,
): Promise<{ paymasterData: Hex; paymasterValidationGasLimit: bigint }> {
// Check: can we sponsor this operation?
const user = await getUserBySmartAccount(userOp.sender);
// Restriction: no more than 100 sponsored operations per day
const dailyCount = await getDailySponsoredCount(user.id);
if (dailyCount >= 100) throw new Error('Daily limit exceeded');
// Restriction: only whitelisted contracts
const callData = decodeCallData(userOp.callData);
if (!isWhitelisted(callData.to)) throw new Error('Contract not whitelisted');
// Sign permission
const validUntil = Math.floor(Date.now() / 1000) + 300; // 5 minutes
const signature = await paymasterSigner.signTypedData({
domain: PAYMASTER_DOMAIN,
types: PAYMASTER_TYPES,
message: { userOp, validUntil },
});
return {
paymasterData: encodeAbiParameters(
[{ type: 'uint48' }, { type: 'bytes' }],
[validUntil, signature],
),
paymasterValidationGasLimit: 100_000n,
};
}
Sponsorship cost: on Arbitrum One — $0.001–$0.005 per sponsored UserOp. On Ethereum mainnet — $0.50–$2.00. For gaming apps — L2 is mandatory.
Paymaster-as-a-Service providers: Pimlico (Alto bundler + paymaster), ZeroDev, Biconomy. Pimlico Verifying Paymaster + Alto bundler — most used combination in production.
Session Keys Limitations and Security
What to Restrict
Session key validator must enforce restrictions:
- Contracts: only specified addresses. Can't call arbitrary contracts with session key.
-
Functions: only specific functions. Session key for game shouldn't call
transferin ERC-20. - Parameters: value ranges (x < 8, amount <= maxAmount).
- Value limit: max ETH to send. For most games — 0.
- Spending limit: total ERC-20 tokens spent during session.
- Expiry: session lifetime. For game — 1–8 hours. For trading bot — up to 24 hours.
Session Key Storage
Session key — private key with limited rights. But compromising it is still dangerous within permissions scope.
Browser: sessionStorage (lives until tab close) or indexedDB with encryption. Don't use localStorage (accessible between sessions, XSS risk).
Backend: if session managed by server (for server-side automation) — store encrypted (KMS), bind to user session token.
// Secure session key storage in browser (encrypted with user-derived key)
async function storeSessionKey(
sessionPrivateKey: Hex,
serializedPermissions: string,
userAuthKey: CryptoKey,
): Promise<void> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const data = new TextEncoder().encode(
JSON.stringify({ sessionPrivateKey, serializedPermissions }),
);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
userAuthKey,
data,
);
sessionStorage.setItem('session_key', JSON.stringify({
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted)),
}));
}
Practical Example: GameFi Session
Typical flow for Play-to-Earn game:
1. User clicks "Start Game"
2. One approval in wallet: open session for 4 hours
Permissions: call makeMove(x,y), claimReward(), only GameContract
3. User plays — each move signed by session key automatically
4. Moves sent via bundler, gas paid by Paymaster
5. After 4 hours session expires — need new approval
Result: user sees game interface without constant MetaMask popups. Onboarding close to Web2.
Tooling
Core: @zerodev/sdk + @zerodev/session-key — most mature. Support: Arbitrum, Optimism, Base, Polygon, Ethereum mainnet. Alternative: Biconomy SDK v4 with session modules. Bundler: Alto (Pimlico), Stackup (Alchemy), Etherspot. Monitoring UserOps: Jiffyscan, Pimlico dashboard.
| Task | Tool |
|---|---|
| Session key validator | ZeroDev Kernel / Biconomy |
| Paymaster | Pimlico / ZeroDev |
| Bundler | Alto (Pimlico) |
| AA wallet | Kernel v3 / Safe |
| Frontend | wagmi v2 + @zerodev/wagmi |
Timeline Guidelines
Basic implementation (session keys + verifying paymaster, one chain): 3–4 weeks. Full system with multi-chain support, custom permission modules, ERC-20 paymaster and sponsorship analytics: 6–8 weeks.







