What are Session Keys?
Session keys allow your application to execute transactions on behalf of a user without requiring their owner private key for every action. Instead, a temporary keypair is created and registered on-chain with specific permissions (time limit, call limit, allowed functions).
This enables gasless, frictionless UX for gaming, DeFi automation, social apps, and more.
Session keys only work with CHIPI wallets - OpenZeppelin accounts extended with SNIP-9 session key support.
When to Use Session Keys
| Use Case | Why Sessions Help |
|---|
| Gaming | Players make moves without wallet popups |
| DeFi Automation | Yield optimizers execute strategies automatically |
| Social dApps | Post, like, comment without friction |
| Subscriptions | Recurring payments without manual approval |
| Airdrops | Zero-friction claiming for users |
Requirements
- A CHIPI wallet (not ARGENT)
- Valid bearer token from your auth provider
- User’s
encryptKey (PIN) for signing
Install the Backend SDK
npm install @chipi-stack/backend
Initialize the SDK
import { ChipiSDK } from "@chipi-stack/backend";
const sdk = new ChipiSDK({
apiPublicKey: process.env.CHIPI_PUBLIC_KEY!,
apiSecretKey: process.env.CHIPI_SECRET_KEY!,
});
Create a Session Key
Generate a session keypair locally. The SDK returns the data but does NOT store it - you must persist it on the client side.// Create session key (self-custodial - store in client localStorage)
const session = sdk.sessions.createSessionKey({
encryptKey: "user-secure-pin",
durationSeconds: 3600, // 1 hour (default: 6 hours)
});
console.log(session);
// {
// publicKey: "0x...", // Session public key
// encryptedPrivateKey: "...", // AES encrypted with encryptKey
// validUntil: 1702500000 // Unix timestamp (seconds)
// }
// Store in client-side secure storage (NOT your database!)
localStorage.setItem('chipiSession', JSON.stringify(session));
Never store session keys in your backend database. Store only in client-side secure storage (localStorage, Secure Enclave, Keychain).
Register Session On-Chain
Before using a session key, you must register it on the CHIPI wallet smart contract. This requires the owner’s signature (one-time setup).// Get user's wallet
const wallet = await sdk.getWallet({ externalUserId: "user-123" });
// Register session on contract (uses owner signature via paymaster)
const txHash = await sdk.sessions.addSessionKeyToContract({
encryptKey: "user-secure-pin",
wallet: {
publicKey: wallet.publicKey,
encryptedPrivateKey: wallet.encryptedPrivateKey,
walletType: "CHIPI",
},
sessionConfig: {
sessionPublicKey: session.publicKey,
validUntil: session.validUntil,
maxCalls: 100, // Max transactions allowed
allowedEntrypoints: [], // Empty = all functions allowed
},
}, bearerToken);
console.log("Session registered:", txHash);
Session Configuration Options
| Parameter | Type | Description |
|---|
sessionPublicKey | string | Public key from createSessionKey() |
validUntil | number | Unix timestamp when session expires |
maxCalls | number | Maximum transactions allowed (decrements with each use) |
allowedEntrypoints | string[] | Whitelisted function selectors. Empty = all allowed |
For security, whitelist specific entrypoints when possible:allowedEntrypoints: [
"0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer
"0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c", // approve
]
Execute Transactions with Session
Now you can execute transactions using the session key - no owner private key needed!// Retrieve session from client storage
const session = JSON.parse(localStorage.getItem('chipiSession')!);
// Execute transaction with session signature (4-element format)
const txHash = await sdk.executeTransactionWithSession({
params: {
encryptKey: "user-secure-pin",
wallet: {
publicKey: wallet.publicKey,
encryptedPrivateKey: wallet.encryptedPrivateKey,
walletType: "CHIPI",
},
session,
calls: [
{
contractAddress: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
entrypoint: "transfer",
calldata: [recipientAddress, amount, "0x0"],
},
],
},
bearerToken,
});
console.log("Transaction executed:", txHash);
Signature Format Difference:
- Owner signature:
[r, s] (2 elements)
- Session signature:
[sessionPubKey, r, s, validUntil] (4 elements)
The SDK handles this automatically. Query Session Status
Check if a session is still active and how many calls remain:const sessionData = await sdk.sessions.getSessionData({
walletAddress: wallet.publicKey,
sessionPublicKey: session.publicKey,
});
console.log(sessionData);
// {
// isActive: true,
// validUntil: 1702500000,
// remainingCalls: 95,
// allowedEntrypoints: []
// }
if (sessionData.isActive) {
console.log(`Session has ${sessionData.remainingCalls} calls remaining`);
}
Revoke Session
Always revoke sessions when no longer needed or when the user logs out:const revokeTxHash = await sdk.sessions.revokeSessionKey({
encryptKey: "user-secure-pin",
wallet: {
publicKey: wallet.publicKey,
encryptedPrivateKey: wallet.encryptedPrivateKey,
walletType: "CHIPI",
},
sessionPublicKey: session.publicKey,
}, bearerToken);
// Clear from client storage
localStorage.removeItem('chipiSession');
console.log("Session revoked:", revokeTxHash);
Always revoke sessions on logout to prevent unauthorized access.
Security and Best Practices
Storage Guidelines
Never store session keys in your backend database. This defeats the purpose of self-custodial security.
Store session keys only in client-side secure storage:
| Platform | Recommended Storage |
|---|
| Web | localStorage or sessionStorage |
| iOS | Secure Enclave / Keychain |
| Android | Android Keystore |
| React Native | expo-secure-store or react-native-keychain |
Use sessionStorage instead of localStorage for sessions that should expire when the browser tab closes.
Lifecycle Best Practices
- Always revoke on logout - Never leave active sessions when user signs out
- Set short durations - Use 1-6 hours, not days
- Limit
maxCalls - Set realistic limits based on expected usage
- Whitelist entrypoints - Restrict to only the functions your app needs
Example: Secure Logout Handler
const handleLogout = async () => {
// Retrieve active session
const sessionStr = localStorage.getItem('chipiSession');
if (sessionStr) {
const session = JSON.parse(sessionStr);
try {
// Revoke session on-chain before logout
await sdk.sessions.revokeSessionKey({
encryptKey: userPin,
wallet,
sessionPublicKey: session.publicKey,
}, bearerToken);
} catch (error) {
console.warn("Failed to revoke session:", error);
// Continue with logout even if revoke fails
}
// Clear from local storage
localStorage.removeItem('chipiSession');
}
// Proceed with auth logout
await authProvider.signOut();
};
Example: Session with Restricted Permissions
// Create a session that can ONLY transfer USDC
const restrictedSession = await sdk.sessions.addSessionKeyToContract({
encryptKey: userPin,
wallet,
sessionConfig: {
sessionPublicKey: session.publicKey,
validUntil: session.validUntil,
maxCalls: 10, // Only 10 transfers allowed
allowedEntrypoints: [
"0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer
],
},
}, bearerToken);
Full Example: Gaming Session
import { ChipiSDK } from "@chipi-stack/backend";
const sdk = new ChipiSDK({
apiPublicKey: process.env.CHIPI_PUBLIC_KEY!,
apiSecretKey: process.env.CHIPI_SECRET_KEY!,
});
// Game constants
const GAME_CONTRACT = "0x...";
const MAKE_MOVE_SELECTOR = "0x...";
const CLAIM_REWARD_SELECTOR = "0x...";
async function startGameSession(userId: string, userPin: string, bearerToken: string) {
// Get player's CHIPI wallet
const wallet = await sdk.getWallet({ externalUserId: userId });
// Create 24-hour gaming session
const session = sdk.sessions.createSessionKey({
encryptKey: userPin,
durationSeconds: 24 * 60 * 60, // 24 hours
});
// Register with game-only permissions
await sdk.sessions.addSessionKeyToContract({
encryptKey: userPin,
wallet: { ...wallet, walletType: "CHIPI" },
sessionConfig: {
sessionPublicKey: session.publicKey,
validUntil: session.validUntil,
maxCalls: 1000, // Many moves allowed
allowedEntrypoints: [MAKE_MOVE_SELECTOR, CLAIM_REWARD_SELECTOR],
},
}, bearerToken);
// Store session in client
localStorage.setItem('gameSession', JSON.stringify(session));
return session;
}
async function makeMove(moveData: string, bearerToken: string) {
const session = JSON.parse(localStorage.getItem('gameSession')!);
const wallet = await sdk.getWallet({ externalUserId: currentUserId });
// Execute move instantly - no wallet popup!
return sdk.executeTransactionWithSession({
params: {
encryptKey: userPin,
wallet: { ...wallet, walletType: "CHIPI" },
session,
calls: [{
contractAddress: GAME_CONTRACT,
entrypoint: "make_move",
calldata: [moveData],
}],
},
bearerToken,
});
}
Next Steps