Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt

Use this file to discover all available pages before exploring further.

Session keys are a SNIP-9 primitive: the user signs a delegation that authorises a server-held key to make specific calls (or any calls) on the user’s wallet for a limited time. After it expires or is revoked, that key is dead. Use cases:
  • Server-driven trades or payments without prompting the user every time
  • Background workers that act on the user’s behalf
  • Time-boxed automations (e.g. “rebalance once an hour for the next 24h”)
This page shows the full lifecycle (create → register → execute → revoke), with code lifted from staging-integration/staging-sessions.test.ts and staging-session-execute.test.ts.
Session keys are powerful — anything the session can do, your server can do without the user present. Always set the shortest reasonable validUntil, restrict allowedEntrypoints when you can, and revoke as soon as you’re done.

Initialise

import { ChipiServerSDK } from "@chipi-stack/backend";

const sdk = new ChipiServerSDK({
  apiPublicKey: process.env.CHIPI_PUBLIC_KEY!,
  apiSecretKey: process.env.CHIPI_SECRET_KEY!,
  baseUrl: process.env.CHIPI_BASE_URL, // omit for production
});

1. Create a session key

Generates a fresh key pair locally and encrypts the private key. Nothing on-chain yet.
const sessionKey = sdk.sessions.createSessionKey({
  encryptKey: userPin,
  durationSeconds: 3600, // 1 hour
});

console.log(sessionKey.publicKey);  // 0x...
console.log(sessionKey.validUntil); // unix timestamp (seconds)

// sessionKey.encryptedPrivateKey is sensitive — keep it in memory or
// in your secrets store, scoped to this session. Never log it.

2. Register on-chain

The user’s wallet signs a transaction that authorises the session key. Chipi’s paymaster covers gas. Wait for confirmation before using the key — Starknet blocks settle in ~20-40s.
const txHash = await sdk.sessions.addSessionKeyToContract(
  {
    encryptKey: userPin,
    wallet: userWallet, // { publicKey, encryptedPrivateKey }
    sessionConfig: {
      sessionPublicKey: sessionKey.publicKey,
      validUntil: sessionKey.validUntil,
      maxCalls: 5,            // hard cap on calls during the window
      allowedEntrypoints: [], // empty = any entrypoint; pass selectors to restrict
    },
  },
  process.env.CHIPI_SECRET_KEY!, // session sub-service requires explicit bearer token
);

console.log("Register tx:", txHash);
// Wait for the register tx to land before executing with the session

3. Execute a transaction with the session key

Once registered, your server can call any allowed entrypoint without the user’s PIN. Below: a self-transfer of 0.001 USDC.
import { STARKNET_CONTRACTS } from "@chipi-stack/types";

const usdc = STARKNET_CONTRACTS["USDC"];
const amount = "0x" + (1000).toString(16); // 0.001 USDC, 6 decimals

const txHash = await sdk.executeTransactionWithSession({
  params: {
    encryptKey: userPin,
    wallet: userWallet,
    session: sessionKey,
    calls: [
      {
        contractAddress: usdc.contractAddress,
        entrypoint: "transfer",
        calldata: [recipientAddress, amount, "0x0"],
      },
    ],
  },
});

console.log("Execute tx:", txHash);
You can pass multiple calls in the same executeTransactionWithSession invocation — they execute as a single multicall.

4. Revoke

When you’re done — or the user logs out — revoke immediately. The on-chain revocation makes the key unusable even if the encrypted private key leaks.
const revokeTxHash = await sdk.sessions.revokeSessionKey(
  {
    encryptKey: userPin,
    wallet: userWallet,
    sessionPublicKey: sessionKey.publicKey,
  },
  process.env.CHIPI_SECRET_KEY!,
);

console.log("Revoke tx:", revokeTxHash);

// Optionally verify
const data = await sdk.sessions.getSessionData({
  walletAddress: userWallet.publicKey,
  sessionPublicKey: sessionKey.publicKey,
});
console.log("isActive:", data.isActive); // false
getSessionData reads through a non-paymaster RPC, which can lag the paymaster RPC used for register/revoke by a few seconds. If you need a definitive on-chain confirmation, wait for the revoke tx to settle (e.g. via Voyager) before calling getSessionData.

Putting it together

import { ChipiServerSDK } from "@chipi-stack/backend";
import { STARKNET_CONTRACTS } from "@chipi-stack/types";

async function timeboxedSendOnUsersBehalf(
  sdk: ChipiServerSDK,
  userPin: string,
  userWallet: { publicKey: string; encryptedPrivateKey: string },
  recipient: string,
  amountUsdc: bigint, // raw units (6 decimals)
) {
  // 1. Create + register a 1-hour session
  const session = sdk.sessions.createSessionKey({
    encryptKey: userPin,
    durationSeconds: 3600,
  });

  const registerTxHash = await sdk.sessions.addSessionKeyToContract(
    {
      encryptKey: userPin,
      wallet: userWallet,
      sessionConfig: {
        sessionPublicKey: session.publicKey,
        validUntil: session.validUntil,
        maxCalls: 1,
        allowedEntrypoints: [],
      },
    },
    process.env.CHIPI_SECRET_KEY!,
  );

  // 2. Wait for the register tx to confirm before executing — otherwise
  //    the session may not yet be active on-chain.
  await waitForTxConfirmed(sdk, registerTxHash);

  // 3. Use the session to send
  const usdc = STARKNET_CONTRACTS["USDC"];
  const txHash = await sdk.executeTransactionWithSession({
    params: {
      encryptKey: userPin,
      wallet: userWallet,
      session,
      calls: [
        {
          contractAddress: usdc.contractAddress,
          entrypoint: "transfer",
          calldata: [recipient, "0x" + amountUsdc.toString(16), "0x0"],
        },
      ],
    },
  });

  // 4. Revoke immediately
  await sdk.sessions.revokeSessionKey(
    {
      encryptKey: userPin,
      wallet: userWallet,
      sessionPublicKey: session.publicKey,
    },
    process.env.CHIPI_SECRET_KEY!,
  );

  return txHash;
}

// Helper: poll Starknet until a tx reaches a terminal status.
// Starknet blocks settle in ~20-40s, so plan for up to ~60s in practice.
async function waitForTxConfirmed(sdk: ChipiServerSDK, txHash: string) {
  const deadline = Date.now() + 90_000; // 90s budget
  while (Date.now() < deadline) {
    const status = await sdk.getTransactionStatus({ txHash });
    if (status.executionStatus === "SUCCEEDED") return;
    if (status.executionStatus === "REVERTED") {
      throw new Error(`tx ${txHash} reverted`);
    }
    await new Promise((r) => setTimeout(r, 3_000));
  }
  throw new Error(`tx ${txHash} did not confirm within 90s`);
}
For production, prefer a webhook (configured at /configure/notifications) over polling — you can register the session key, return a 202 to your user, and trigger executeTransactionWithSession from the webhook handler when the register tx confirms.
Verified by staging-integration/staging-sessions.test.ts at commit 275295f (2026-03-12) and staging-session-execute.test.ts at commit ec48b6b (2026-03-14). Both run in CI on every PR from stagingmain against live staging, with on-chain confirmation via Voyager.