Skip to main content
An agent (a cron job, an AI loop, a bot) acts on a user’s wallet without holding the user’s key. The user approves it once with their passkey. After that, the agent signs with a session key that the wallet contract limits on its own: which functions it can call, how many times, until when, and how much of each token it can move. This guide builds that flow with @chipi-stack/nextjs in the browser and @chipi-stack/backend on the server. It needs 14.14.0 or later.

What runs where

The owner key never leaves the browser, and the session key never leaves the server.
Session keys work on CHIPI wallets and on SHHH wallets with a STARK or ED25519 signer, which includes every SHHH wallet the SDK creates with a passkey. Check supportsSession from useChipiSession, or walletSupportsSessions(wallet).

1. Server: create the session key

The session private key is encrypted with a secret only your server knows. Store the whole SessionKeyData in your database; send only the public key and expiry to the browser.

2. Browser: register the session and its caps with one passkey prompt

getEncryptKey runs only when an action needs the owner key, so the passkey prompt appears when the user clicks, not when the page loads. It runs before getBearerToken, so the JWT is fresh after the prompt. Every action resolves it again: if you also execute from the browser (executeWithSession), each execution prompts. That is why the agent here executes on the server. With spendingPolicies, the session and every cap are registered in one owner-signed transaction: the session never exists on-chain without its limits.
  • maxPerWindow: 0n means no cap, not “nothing”. Always set a positive window cap.
  • The whitelist is not scoped to a contract: "transfer" allows transfer on every token the wallet holds. Set a policy for every token the wallet may hold (for example USDC, ETH and STRK), not only the one the agent is meant to use.
  • Policies meter transfer, approve and increase_allowance. A swap is capped through its approve.
Function names ("transfer") are converted to selectors for you. Hex selectors work too.

3. Server: decide and execute

The agent reads the market with sdk.ai, checks the decision, builds the swap and signs it with the session key. sdk.ai uses your secret key and refuses to run without one.
sdk.getWallet with the server SDK returns the wallet row, including walletType and signerKind, which is what the session execution routes on.

4. Get paid by the agent (x402 without a facilitator)

If the agent pays your API per call, let it send the transfer with its session key and hand you the hash. SessionTxVerifier accepts it only if the transaction succeeded, its Transfer event moved at least the price from the agent’s wallet to you, and the hash was never used before.
Hashes and Transfer events are public. Take agentWallet from the caller’s authenticated identity, not from the request body: otherwise anyone watching your treasury can submit an agent’s hash first and use the payment. Anonymous payers are first-come-first-served, so keep that to low-value calls.

5. Browser: stop the agent

Revoking needs the owner key, so it is one more passkey prompt. Pass the session public key: the browser never held the session itself.
After revocation the contract zeroes the session, so getSessionData reads validUntil: 0 and isActive: false.

Check the session from the server

getSessionData reads the latest accepted block. Right after registering, executing or revoking, wait for that transaction with waitForTransaction(txHash) before reading: until its block is accepted on L2, the read returns the previous state (or, for a wallet the transaction deploys, fails because the contract is not there yet).

Without React

The same registration is available as pure call builders, so you can sign it with any owner path:
sdk.sessions.setupSession(params, bearerToken) builds and signs the same calls with a PIN-encrypted owner key on the server.
✅ Verified against the packed 14.14.0 types on 2026-09-28.