What Are Session Keys?
Session keys are temporary keypairs that let your app execute transactions on behalf of a user without requiring their private key for every action. Instead of prompting users to sign each transaction, you register a session key on-chain with specific permissions (time limit, call limit, allowed functions) and then use it silently.
This enables gasless, popup-free UX for gaming, DeFi automation, social apps, and subscriptions.
Session keys only work with CHIPI wallets (SNIP-9 compatible). They are not supported on READY wallets.
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 |
How It Works
1. Create → Generate a session keypair locally (client-side only)
2. Register → Register the session on the CHIPI wallet contract (one-time, requires owner signature)
3. Execute → Use the session key to sign transactions (no owner key needed)
4. Monitor → Check remaining calls and expiration
5. Revoke → Revoke the session on logout or expiration
The SDK handles the signature format difference automatically:
- Owner signature:
[r, s] (2 elements)
- Session signature:
[sessionPubKey, r, s, validUntil] (4 elements)
Frontend Implementation
1. Create a Session Key
import { useCreateSessionKey } from "@chipi-stack/nextjs";
function StartSession() {
const { createSessionKeyAsync, isLoading } = useCreateSessionKey();
const handleStart = async () => {
const session = await createSessionKeyAsync({
encryptKey: userPin,
durationSeconds: 3600, // 1 hour
});
// Store in client-side storage only (NEVER in your database)
localStorage.setItem("chipiSession", JSON.stringify(session));
};
}
2. Register On-Chain
import { useAddSessionKeyToContract } from "@chipi-stack/nextjs";
function RegisterSession() {
const { addSessionKeyToContractAsync } = useAddSessionKeyToContract();
const handleRegister = async () => {
const session = JSON.parse(localStorage.getItem("chipiSession")!);
const bearerToken = await getBearerToken();
await addSessionKeyToContractAsync({
params: {
encryptKey: userPin,
wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI" },
sessionConfig: {
sessionPublicKey: session.publicKey,
validUntil: session.validUntil,
maxCalls: 100,
allowedEntrypoints: [], // empty = all functions allowed
},
},
bearerToken,
});
};
}
3. Execute Transactions
import { useExecuteWithSession } from "@chipi-stack/nextjs";
const USDC = "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb";
function TransferWithSession() {
const { executeWithSessionAsync, isLoading } = useExecuteWithSession();
const handleTransfer = async (recipient: string, amount: string) => {
const session = JSON.parse(localStorage.getItem("chipiSession")!);
const bearerToken = await getBearerToken();
const txHash = await executeWithSessionAsync({
params: {
encryptKey: userPin,
wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI" },
session,
calls: [{
contractAddress: USDC,
entrypoint: "transfer",
calldata: [recipient, amount, "0x0"],
}],
},
bearerToken,
});
};
}
4. Check Session Status
import { useGetSessionData } from "@chipi-stack/nextjs";
function SessionStatus() {
const { data: sessionData } = useGetSessionData({
walletAddress: wallet.publicKey,
sessionPublicKey: session.publicKey,
});
if (sessionData?.isActive) {
return <p>{sessionData.remainingCalls} calls remaining</p>;
}
return <p>Session expired or revoked</p>;
}
5. Revoke on Logout
import { useRevokeSessionKey } from "@chipi-stack/nextjs";
function LogoutButton() {
const { revokeSessionKeyAsync } = useRevokeSessionKey();
const handleLogout = async () => {
const session = JSON.parse(localStorage.getItem("chipiSession")!);
try {
await revokeSessionKeyAsync({
params: {
encryptKey: userPin,
wallet: { publicKey: wallet.publicKey, encryptedPrivateKey: wallet.encryptedPrivateKey, walletType: "CHIPI" },
sessionPublicKey: session.publicKey,
},
bearerToken: await getBearerToken(),
});
} catch {
// Continue with logout even if revoke fails
}
localStorage.removeItem("chipiSession");
await authProvider.signOut();
};
}
Session Configuration
| Parameter | Type | Description |
|---|
sessionPublicKey | string | Public key from createSessionKey() |
validUntil | number | Unix timestamp (seconds) when session expires |
maxCalls | number | Maximum transactions allowed (decrements with each use) |
allowedEntrypoints | string[] | Whitelisted function selectors. Empty = all allowed |
Restricting Permissions
For security, whitelist specific entrypoints:
sessionConfig: {
sessionPublicKey: session.publicKey,
validUntil: session.validUntil,
maxCalls: 10,
allowedEntrypoints: [
"0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // transfer
"0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c", // approve
],
}
Security Best Practices
Never store session keys in your backend database. This defeats self-custodial security. Store 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 |
- Always revoke on logout - Never leave active sessions
- Set short durations - 1-6 hours, not days
- Limit maxCalls - Set realistic limits based on expected usage
- Whitelist entrypoints - Restrict to only the functions your app needs
- Monitor remaining calls - Create new sessions before exhaustion