npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

x402-sessions

v0.2.0

Published

Sign once, settle many times — session-based x402 payments on Stellar. One on-chain SAC approve unlocks unlimited micropayments via transfer_from until the cap or expiry is reached. No per-request wallet popup.

Readme

x402-sessions

Sign once, settle many times — session-based x402 payments on Stellar.

A tiny TypeScript SDK that turns a single Stellar Asset Contract (SAC) approve into an unlimited stream of x402 micropayments. Pay $1 upfront, then every API call automatically settles $0.10 on-chain via transfer_from. No per-call wallet popup. No off-chain bookkeeping tricks. The cap and expiry are enforced on-chain by the SAC itself.

Classic x402 = 1 request, 1 signature, 1 settlement. x402-sessions = 1 signature, N settlements.

Why

If you're building an AI agent, a dapp game, a pay-per-inference API, or anything where a user makes many small payments in a row, classic x402 becomes friction theatre — one wallet popup per request. Thirdweb built a session model for EVM using Permit2 + off-chain facilitator bookkeeping. This package is the Stellar-native equivalent, and it's arguably simpler and stronger:

  • On-chain enforcement of the cap via SEP-41 approve. Not a facilitator-side ledger hack.
  • Zero escrow. Unused allowance stays in the user's wallet. No refund dance when a session expires.
  • Works with existing x402 infra. Registers as a new scheme (session) alongside Coinbase's exact. Drop-in on the resource-server side via @x402/core's x402ResourceServer.register().
  • Tiny wire format. The retry PaymentPayload.payload is just { sessionId }.

Install

npm install x402-sessions @stellar/stellar-sdk
# if you're also building the resource-server side:
npm install @x402/core @x402/next

Peer deps: @stellar/stellar-sdk ^14 || ^15, @x402/core ^2.8.0 (optional — only if you use the server-side scheme plugin).

You also need a running x402-sessions facilitator — a small service that verifies sessions and performs the on-chain transfer_from. A public testnet facilitator is live at:

https://courteous-emotion-production.up.railway.app

To self-host your own, use the scaffold: npm create x402-sessions-facilitator@latest my-facilitator.

30-second quickstart

1. Client side (Node)

import { Keypair } from "@stellar/stellar-sdk";
import { createSession } from "x402-sessions";

const session = await createSession({
  signer: Keypair.fromSecret(process.env.USER_SECRET!),
  facilitatorUrl: "https://courteous-emotion-production.up.railway.app",
  network: "stellar:testnet",
  asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", // USDC testnet SAC
  spendingCap: "1.00",   // $1 total
  expiresIn: 3600,       // 1 hour
  recipient: "GBWYMS7R...", // your resource server's wallet
});

// session.fetch is a drop-in fetch that transparently pays per call.
for (let i = 0; i < 10; i++) {
  const res = await session.fetch("https://yourapi.example/inference");
  console.log(await res.json()); // each call settles $0.10 on-chain
}

Under the hood createSession signs and submits one SAC approve(user, facilitator, cap, expiration_ledger) tx, registers the approval with the facilitator, and returns a SessionHandle whose fetch() method transparently handles 402 responses.

2. Client side (browser with Freighter)

import { signTransaction, requestAccess } from "@stellar/freighter-api";
import { createSession } from "x402-sessions";

const { address } = await requestAccess();

const session = await createSession({
  signer: {
    publicKey: () => address,
    signTransaction: async (xdr, opts) => {
      const r = await signTransaction(xdr, {
        networkPassphrase: opts?.networkPassphrase,
        address,
      });
      if (r.error) throw new Error(r.error.message ?? "sign failed");
      return r.signedTxXdr;
    },
  },
  facilitatorUrl: "https://courteous-emotion-production.up.railway.app",
  network: "stellar:testnet",
  asset: process.env.NEXT_PUBLIC_USDC_SAC_ID!,
  spendingCap: "1.00",
  expiresIn: 3600,
  recipient: process.env.NEXT_PUBLIC_RECIPIENT!,
});

// Use it anywhere you'd use fetch()
await session.fetch("/api/chat", {
  method: "POST",
  body: JSON.stringify({ prompt: "hi" }),
  headers: { "Content-Type": "application/json" },
});

Any signer with publicKey() + either sign(tx) (Node Keypair) or signTransaction(xdr, opts) (browser wallet adapter) works.

3. Resource-server side (Next.js + @x402/next)

import { paymentProxy, x402ResourceServer } from "@x402/next";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { SessionStellarScheme } from "x402-sessions";

const facilitator = new HTTPFacilitatorClient({
  url: process.env.SESSION_FACILITATOR_URL ?? "https://courteous-emotion-production.up.railway.app",
});

const server = new x402ResourceServer(facilitator).register(
  "stellar:testnet",
  new SessionStellarScheme({
    assetContractId: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
    facilitatorUrl: process.env.SESSION_FACILITATOR_URL,
  }),
);

export const handler = paymentProxy(
  {
    "/api/chat": {
      accepts: [
        {
          scheme: "session" as const,
          price: "0.10",
          network: "stellar:testnet",
          payTo: process.env.SERVER_STELLAR_ADDRESS!,
        },
      ],
      description: "AI chat, settled per message via session",
    },
  },
  server,
);

That's it. Any request to /api/chat without a session payload gets a 402. With one, it passes through and $0.10 settles on-chain.

How it works

┌──────────────┐                                        ┌───────────────┐
│  user wallet │──1. approve(facilitator, $1, 1h) ─────▶│   USDC SAC    │
│  (Freighter) │                                        │  (on-chain)   │
└──────┬───────┘                                        └───────▲───────┘
       │                                                        │
       │ 2. POST /sessions {approvalTxHash, cap, …}              │
       ▼                                                        │
┌──────────────┐    3. sessionId                                 │
│ x402-sessions│◀─────────────────┐                              │
│  facilitator │                  │                              │
│  (HTTP)      │                  │                              │
└──────┬───────┘                  │                              │
       │                          │                              │
       │                          │                              │
       │           4. POST /api/chat (per call)                  │
       │           ┌──────────────┴───────────────┐              │
       │           │                              │              │
       │           │       ┌──────────────┐       │              │
       │           └──────▶│   resource   │       │              │
       │                   │   server     │       │              │
       │                   │ (@x402/next) │       │              │
       │                   └──────┬───────┘       │              │
       │                          │               │              │
       │◀── 5. /verify ────────── │               │              │
       │    /settle                                              │
       │──── 6. transfer_from(facilitator, user, server, $0.10) ─▶
       │                                                         │
       │                   7. ok + reply                         │
       │                   └──────────────▶ user                 │
       └──── decrement session (sqlite) ─────────────────────────┘
  1. User signs one on-chain approve(spender=facilitator, amount=cap, expiration_ledger).
  2. SDK registers the approval with the facilitator (POST /sessions).
  3. Facilitator verifies on-chain allowance via allowance(), stores the session, returns a sessionId.
  4. User hits protected endpoint with session.fetch(...). SDK handles the 402 dance automatically:
    • Reads the PAYMENT-REQUIRED header (x402 v2)
    • Builds a payment payload { sessionId }
    • Retries with PAYMENT-SIGNATURE header
  5. Resource server's x402ResourceServer calls facilitator /verify then /settle.
  6. Facilitator runs transfer_from(spender, from, to, amount) on-chain. The SAC itself enforces the cap and expiry.
  7. Response flows back to the user. Spent counter decrements.

API reference

createSession(options)

Signs + submits the on-chain approve, registers the session with the facilitator, and returns a SessionHandle.

function createSession(opts: CreateSessionOptions): Promise<SessionHandle>

interface CreateSessionOptions {
  signer: ClientSigner;              // Keypair or browser wallet adapter
  facilitatorUrl: string;            // e.g. "https://courteous-emotion-production.up.railway.app"
  network?: "stellar:testnet" | "stellar:pubnet"; // default testnet
  asset: string;                     // SAC contract id (C...)
  spendingCap: string;               // human units, e.g. "1.00"
  decimals?: number;                 // default 7 (USDC)
  expiresIn: number;                 // seconds; converted to ledger count
  recipient: string;                 // payTo address (G...)
  sorobanRpcUrl?: string;            // override default
}

interface SessionHandle {
  sessionId: string;
  user: string;
  spender: string;
  asset: string;
  recipient: string;
  cap: string;            // base units (stroops for 7-dec USDC)
  spent: string;          // base units
  expirationLedger: number;
  network: Network;
  facilitatorUrl: string;
  fetch: typeof fetch;    // auto-paying fetch
}

wrapFetch(sessionId)

Lower-level helper. Returns a fetch-compatible function that, on receiving a 402, reads the PAYMENT-REQUIRED header, builds a PaymentPayload with the given sessionId, and retries with PAYMENT-SIGNATURE. Use this if you want to manage the session handle yourself (e.g. persist it across pages).

function wrapFetch(sessionId: string): typeof fetch

SessionStellarScheme

Resource-server plugin for @x402/core's x402ResourceServer.register(). Advertises the session scheme, parses prices, and enriches 402 responses with facilitator metadata.

class SessionStellarScheme {
  constructor(config: {
    assetContractId: string;   // SAC contract id
    decimals?: number;         // default 7
    facilitatorUrl?: string;   // exposed to clients in 402.extra
  });
}

Structurally compatible with @x402/core's SchemeNetworkServer interface. (It doesn't implements literally, to avoid an @x402/core/types module-resolution requirement — your TypeScript will happily pass it to register(network, new SessionStellarScheme(...)) via structural typing.)

ClientSigner

Minimal signer interface. A Keypair from @stellar/stellar-sdk satisfies it natively (via its sign(Buffer) method). For browser wallets, provide the signTransaction shape.

interface ClientSigner {
  publicKey(): string;
  sign?(data: Buffer): Buffer;
  signTransaction?(
    xdr: string,
    opts?: { networkPassphrase: string },
  ): Promise<string>;
}

Helpers

import {
  decimalToBaseUnits,     // "1.50" + 7 decimals -> 15000000n
  defaultRpcUrlFor,       // network -> public Soroban RPC URL
  networkPassphraseFor,   // network -> stellar-sdk Networks constant
} from "x402-sessions";

Wire format (session scheme)

  • Scheme: "session"
  • Network: "stellar:testnet" | "stellar:pubnet"
  • PaymentPayload.payload (the scheme-specific slot): { sessionId: string }
  • Facilitator HTTP surface (added to the standard x402 triplet):
    • GET /supported — standard x402
    • POST /verify — standard x402
    • POST /settle — standard x402
    • POST /sessionsnew: register a session from a signed approval tx hash
    • GET /sessions/:idnew: inspect remaining cap / spent / expiry

Trust model

| Limit | Enforced by | Hardness | |---|---|---| | Total cap (e.g. $1) | SAC approve + transfer_from reverts past cap | On-chain | | Expiry (ledger) | expiration_ledger parameter in SAC approve | On-chain | | Per-call price (e.g. $0.10) | Facilitator /settle refuses amounts > policy | Off-chain (trust your facilitator) | | Recipient binding | Facilitator only pays the pre-registered recipient | Off-chain | | Session reuse control | Facilitator sqlite bookkeeping | Off-chain | | Unused balance handling | Funds never escrowed — they stay in the user's wallet | Native |

In short: on-chain handles the money-safety invariants (total and expiry). Off-chain handles the application policy (per-call price, recipient, bookkeeping). This is the same trust model as thirdweb's session x402 on EVM, except the cap enforcement is strictly better because ours is on-chain.

If you need on-chain per-call policy enforcement (e.g. "no more than $0.10 per call, no matter what"), upgrade to a Soroban custom account / smart wallet with a policy-signer session key — out of scope for this package.

No refund dance

Because approve is a permission, not an escrow, tokens never leave the user's wallet until transfer_from is called:

| Scenario | What happens | |---|---| | Session expires unused | Still in user's wallet. Allowance becomes unusable. No refund call. | | Session partially used | The untouched portion never moved. No refund call. | | User wants to end early | Optional: sign approve(..., amount=0) to revoke. Not required — expiry handles it. |

Compare this to escrow-based session models where you must explicitly claim refunds.

Security notes

  • Treat the sessionId as a bearer token — anyone who steals it can spend the full cap to the pre-registered recipient. Keep it in memory or secure storage; don't log it.
  • The recipient is bound at session creation. A stolen sessionId cannot redirect funds.
  • Worst-case: an attacker drains the whole cap to your game server. User loses up to cap; funds still end up where they were meant to go.
  • For higher-trust scenarios, use a smaller cap per session and rotate often.

FAQ

Q: Is this the same as Coinbase x402's upto scheme? A: No — upto is a canonical Coinbase-authored scheme that's currently EVM-only and enforces single-use authorizations. This is a new scheme called session, Stellar-native, built on SAC approve.

Q: Is this the same as thirdweb's session x402? A: Same model (sign once, settle many), different mechanism. Thirdweb uses Uniswap Permit2 on EVM with facilitator-side bookkeeping. This package uses Stellar SAC approve/transfer_from with on-chain bookkeeping. No EIP-7702 involved on either side.

Q: Can I use it with real Circle USDC on Stellar? A: Yes — the SAC contract ID for Circle testnet USDC is CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA, and pubnet is CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75. Any SEP-41 token works — native XLM SAC, Circle USDC, or your own issued asset.

Q: What if on-chain transfer_from fails after the facilitator debits sqlite? A: The reference facilitator rolls back the sqlite debit automatically and returns success: false, errorReason: "onchain_transfer_failed" in the payment-response header.

Q: Can I reuse a session across multiple resource servers? A: Only if the recipient matches. The facilitator binds a session to exactly one recipient at creation time.

Q: What happens if the session cap runs out mid-request? A: Facilitator /verify returns invalidReason: "cap_exceeded". The client sees a 402 with the reason in the payment-response header; the resource is not served and no on-chain tx is submitted.

Q: Can I use the browser fetch directly, without wrapFetch? A: Yes — just handle the 402 yourself. Read the PAYMENT-REQUIRED response header (base64 JSON), find the scheme: "session" accept, build a PaymentPayload with { payload: { sessionId } }, base64-encode it, and retry with a PAYMENT-SIGNATURE header.

Live demo

A reference Next.js app ships at x402-sessions/x402-session-app. It includes two x402-protected routes so you can see sessions in action:

| Route | What it does | |---|---| | /test2 | AI Chat — connect Freighter, create a session ($1 cap / 1 hr), and chat with an LLM. Each message settles $0.10 on-chain via transfer_from. | | /slot | Slot Machine — same session flow, but each pull of the lever is a micropayment. Fun way to drain a session cap. |

Both routes use the deployed facilitator at https://courteous-emotion-production.up.railway.app by default.

Run it yourself

git clone https://github.com/x402-sessions/x402-session-app.git
cd x402-session-app
npm install
cp .env.local.example .env.local   # edit values if needed
npm run dev

Open http://localhost:3000/test2 (chat) or http://localhost:3000/slot (slot machine) in a browser with the Freighter wallet extension set to Testnet.

License

MIT © madhav