Skip to main content
A session is a scoped, expiring key the app mints so the user can pay within fixed limits without a root prompt per action. The session key is never the account’s root key - it carries on-chain policies (a spend limit, an allowlist) and is revocable at any time.
Sessions are a contract (C...) account feature only. On the classic model, grantSession throws BuckspayError("INVALID_CONFIG") - a classic G... key has no on-chain policy surface to scope.

Lifecycle

import { serializeSession, deserializeSession } from "@buckspay/core";
import { spendLimit, allowlist } from "@buckspay/accounts/policy";

// 1. Grant - root-signed; installs the session key + policies on-chain.
const { session, receipt } = await client.grantSession({
  sessionKey: { type: "ed25519", publicKey: sessionKp.publicKey() },
  policies: [
    spendLimit({ token: USDC_SAC, max: "100", period: "day" }),
    allowlist([APP_CONTRACT])
  ],
  expiresAt: Date.now() + 86_400_000  // 24 h
});

// 2. Serialize to secure storage; rehydrate with deserializeSession.
const blob = serializeSession(session);

// 3. The session key pays within its policies - no per-action root prompts.

// 4. Revoke when done - root-signed; the key authorizes nothing afterward.
await client.revokeSession(session);

Full example

// Recipe 11 - SESSIONS (contract/passkey accounts only). Grant a scoped session key with on-chain
// policies (spend limit + allowlist), serialize it for later, then revoke it. Sessions are refused on
// the classic model: grantSession throws BuckspayError("INVALID_CONFIG").
import { Keypair } from "@stellar/stellar-sdk";
import { createBuckspayClient, createRpcSimContext, serializeSession, type BuckspayConfig } from "@buckspay/core";
import { ozContractAccount } from "@buckspay/accounts/oz-contract";
import { spendLimit, allowlist } from "@buckspay/accounts/policy";
import { passkey } from "@buckspay/signers/passkey";
import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator";

const SPONSOR_G = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
const APP_CONTRACT = "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75";

const config: BuckspayConfig = {
  network: "testnet",
  account: ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }),
  signer: passkey({ rpId: "localhost", rpName: "buckspay" }), // the ROOT signer authorizes the install
  relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }),
  gas: { mode: "sponsored" }
};

const client = createBuckspayClient(
  config,
  createRpcSimContext("https://soroban-testnet.stellar.org", { simSource: SPONSOR_G })
);

export async function runSession(): Promise<void> {
  await client.connect();

  // The session key is the ONE key the app deliberately mints - scoped + revocable, never the root
  // account key. Store its secret in secure storage; the SDK only ever needs its public key.
  const sessionKp = Keypair.random();

  const { session, receipt } = await client.grantSession({
    sessionKey: { type: "ed25519", publicKey: sessionKp.publicKey() },
    policies: [spendLimit({ token: USDC_SAC, max: "100", period: "day" }), allowlist([APP_CONTRACT])],
    expiresAt: Date.now() + 86_400_000 // 24h
  });
  console.log("granted", session.id, receipt.transferTx);

  // Persist the session (e.g. to secure storage) and rehydrate it later with deserializeSession.
  const blob = serializeSession(session);
  console.log("serialized session blob length", blob.length);

  // ...the session key now pays within its policies, with no per-action root prompts...

  // Revoke when done (root-signed); the session key no longer authorizes anything.
  const revokeReceipt = await client.revokeSession(session);
  console.log("revoked", revokeReceipt.transferTx);
}

The native mechanism

The policies compile to policy signers evaluated on-chain in __check_auth. The contract - not the SDK - enforces them, so a stolen session blob still cannot exceed its limits or call outside its allowlist.

Error codes

CodeMeaning
SESSION_EXPIREDThe session is past expiresAt. The SDK rejects it before the relay; the contract would reject it too.
SESSION_POLICY_VIOLATIONA payment breaches a policy - over the spend limit, or a contract outside the allowlist. The facilitator maps the on-chain rejection to this code.
INVALID_CONFIGgrantSession was called on a classic (G...) account.

Install and revoke use the gated relay

grantSession and revokeSession are ordinary root-signed operations over the same gated /relay path as any payment. They inherit its nonce and expiration replay protection. There is no separate, weaker channel for session management.

Policies

Spend limit

Cap cumulative spend of a token over a rolling period (day, week, month, or total).

Allowlist

Restrict which contracts the session key may call.