Published 12/4/2025 · 8 min read
Tags: solana , javascript , x402 , pricing , sessions
Basic x402 works: pay per request, get content. But real products need more - subscription-like sessions, dynamic pricing, usage tracking.
Let’s build these patterns.
Session Tokens
Paying for every single request is annoying. Better: pay once, get a session token for multiple requests.
// server.ts - Session-based x402
interface Session {
walletAddress: string;
createdAt: number;
expiresAt: number;
requestsRemaining: number;
}
// In-memory sessions (use Redis in production)
const sessions = new Map<string, Session>();
// Create a session token
function createSessionToken(walletAddress: string): string {
const token = crypto.randomUUID();
sessions.set(token, {
walletAddress,
createdAt: Date.now(),
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
requestsRemaining: 100, // 100 requests per session
});
return token;
}
// Verify session
function verifySession(token: string): Session | null {
const session = sessions.get(token);
if (!session) return null;
if (Date.now() > session.expiresAt) {
sessions.delete(token);
return null;
}
if (session.requestsRemaining <= 0) {
return null;
}
return session;
}
// Use a request from the session
function useSessionRequest(token: string): boolean {
const session = sessions.get(token);
if (!session || session.requestsRemaining <= 0) return false;
session.requestsRemaining--;
return true;
}
Session-Aware Endpoint
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/api/premium") {
// Check for existing session
const sessionToken = req.headers.get("x-session-token");
if (sessionToken) {
const session = verifySession(sessionToken);
if (session && useSessionRequest(sessionToken)) {
// Valid session - serve content without payment
return Response.json({
message: "Session access granted",
remainingRequests: session.requestsRemaining,
data: {
/* your content */
},
});
}
}
// No valid session - require payment
const paymentHeader = req.headers.get("x-payment");
if (!paymentHeader) {
return Response.json(
{
x402Version: 1,
accepts: [
{
scheme: "exact",
network: "solana-devnet",
maxAmountRequired: "100000", // $0.10 for session
description: "24-hour access (100 requests)",
payTo: TREASURY_ADDRESS,
// ... other fields
},
],
},
{ status: 402 }
);
}
// Verify payment
const isValid = await verifyPayment(paymentHeader);
if (isValid) {
// Extract wallet from payment
const walletAddress = extractWalletFromPayment(paymentHeader);
const newSessionToken = createSessionToken(walletAddress);
return Response.json(
{
message: "Payment received! Session created.",
sessionToken: newSessionToken,
expiresIn: "24 hours",
totalRequests: 100,
data: {
/* your content */
},
},
{
headers: {
"X-Session-Token": newSessionToken,
},
}
);
}
return Response.json({ error: "Invalid payment" }, { status: 402 });
}
return new Response("Not found", { status: 404 });
},
});
Client-Side Session Handling
// Svelte store for session management
// src/lib/stores/session.ts
import { writable, get } from "svelte/store";
import { browser } from "$app/environment";
interface SessionState {
token: string | null;
expiresAt: number | null;
remainingRequests: number | null;
}
function createSessionStore() {
const stored = browser ? localStorage.getItem("x402-session") : null;
const initial: SessionState = stored
? JSON.parse(stored)
: {
token: null,
expiresAt: null,
remainingRequests: null,
};
const { subscribe, set, update } = writable<SessionState>(initial);
// Persist to localStorage
subscribe((state) => {
if (browser) {
if (state.token) {
localStorage.setItem("x402-session", JSON.stringify(state));
} else {
localStorage.removeItem("x402-session");
}
}
});
return {
subscribe,
setSession(token: string, expiresAt: number, remainingRequests: number) {
set({ token, expiresAt, remainingRequests });
},
updateRemaining(remaining: number) {
update((s) => ({ ...s, remainingRequests: remaining }));
},
clear() {
set({ token: null, expiresAt: null, remainingRequests: null });
},
isValid(): boolean {
const state = get({ subscribe });
if (!state.token || !state.expiresAt) return false;
if (Date.now() > state.expiresAt) {
this.clear();
return false;
}
return (state.remainingRequests ?? 0) > 0;
},
};
}
export const session = createSessionStore();
Dynamic Pricing
Price based on usage, time of day, or demand:
// pricing.ts
interface PricingConfig {
basePrice: number; // Base price in micro-USDC
peakMultiplier: number; // Multiplier during peak hours
bulkDiscount: number; // Discount for high-volume users
}
const PRICING: Record<string, PricingConfig> = {
"/api/basic": {
basePrice: 10000, // $0.01
peakMultiplier: 1.5,
bulkDiscount: 0.8,
},
"/api/premium": {
basePrice: 100000, // $0.10
peakMultiplier: 2.0,
bulkDiscount: 0.7,
},
"/api/compute": {
basePrice: 500000, // $0.50
peakMultiplier: 3.0,
bulkDiscount: 0.5,
},
};
// Track usage per wallet
const walletUsage = new Map<string, number>();
function calculatePrice(route: string, walletAddress?: string): number {
const config = PRICING[route];
if (!config) return 10000; // Default $0.01
let price = config.basePrice;
// Peak hours (UTC 14:00 - 22:00)
const hour = new Date().getUTCHours();
if (hour >= 14 && hour <= 22) {
price *= config.peakMultiplier;
}
// Bulk discount for repeat users
if (walletAddress) {
const usage = walletUsage.get(walletAddress) ?? 0;
if (usage > 100) {
price *= config.bulkDiscount;
}
}
return Math.round(price);
}
// Record usage
function recordUsage(walletAddress: string) {
const current = walletUsage.get(walletAddress) ?? 0;
walletUsage.set(walletAddress, current + 1);
}
Time-Based Access
Charge for time periods instead of per request:
interface TimeAccess {
walletAddress: string;
tier: "hour" | "day" | "week";
expiresAt: number;
}
const TIME_PRICES = {
hour: 50000, // $0.05
day: 200000, // $0.20
week: 1000000, // $1.00
};
const timeAccess = new Map<string, TimeAccess>();
function hasTimeAccess(walletAddress: string): boolean {
const access = timeAccess.get(walletAddress);
if (!access) return false;
if (Date.now() > access.expiresAt) {
timeAccess.delete(walletAddress);
return false;
}
return true;
}
function grantTimeAccess(walletAddress: string, tier: "hour" | "day" | "week") {
const durations = {
hour: 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000,
week: 7 * 24 * 60 * 60 * 1000,
};
timeAccess.set(walletAddress, {
walletAddress,
tier,
expiresAt: Date.now() + durations[tier],
});
}
Usage Analytics
Track what’s being used:
// analytics.ts
interface UsageEvent {
timestamp: number;
walletAddress: string;
endpoint: string;
amountPaid: number;
responseTime: number;
}
const usageEvents: UsageEvent[] = [];
function trackUsage(event: UsageEvent) {
usageEvents.push(event);
// In production: send to analytics service
// await analytics.track(event);
}
function getUsageStats(walletAddress?: string) {
const events = walletAddress
? usageEvents.filter((e) => e.walletAddress === walletAddress)
: usageEvents;
const last24h = events.filter(
(e) => e.timestamp > Date.now() - 24 * 60 * 60 * 1000
);
return {
total: events.length,
last24h: last24h.length,
revenue: events.reduce((sum, e) => sum + e.amountPaid, 0),
avgResponseTime:
events.reduce((sum, e) => sum + e.responseTime, 0) / events.length,
};
}
Rate Limiting
Prevent abuse even with payments:
// rateLimit.ts
interface RateLimitBucket {
tokens: number;
lastRefill: number;
}
const rateLimits = new Map<string, RateLimitBucket>();
const RATE_LIMIT = {
maxTokens: 60, // Max requests
refillRate: 1, // Tokens per second
refillInterval: 1000, // Refill check interval
};
function checkRateLimit(walletAddress: string): boolean {
let bucket = rateLimits.get(walletAddress);
if (!bucket) {
bucket = { tokens: RATE_LIMIT.maxTokens, lastRefill: Date.now() };
rateLimits.set(walletAddress, bucket);
}
// Refill tokens based on time passed
const now = Date.now();
const elapsed = now - bucket.lastRefill;
const tokensToAdd = Math.floor(elapsed / 1000) * RATE_LIMIT.refillRate;
bucket.tokens = Math.min(RATE_LIMIT.maxTokens, bucket.tokens + tokensToAdd);
bucket.lastRefill = now;
if (bucket.tokens < 1) {
return false; // Rate limited
}
bucket.tokens--;
return true;
}
Complete Advanced Server
Putting it all together:
// server.ts
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
const startTime = Date.now();
if (url.pathname.startsWith("/api/")) {
// Check for session token first
const sessionToken = req.headers.get("x-session-token");
let walletAddress: string | undefined;
if (sessionToken) {
const session = verifySession(sessionToken);
if (session && useSessionRequest(sessionToken)) {
walletAddress = session.walletAddress;
// Check rate limit
if (!checkRateLimit(walletAddress)) {
return Response.json(
{ error: "Rate limited. Slow down." },
{ status: 429 }
);
}
// Track usage
trackUsage({
timestamp: Date.now(),
walletAddress,
endpoint: url.pathname,
amountPaid: 0,
responseTime: Date.now() - startTime,
});
return serveContent(url.pathname, session);
}
}
// Check for time-based access
const paymentHeader = req.headers.get("x-payment");
if (paymentHeader) {
walletAddress = extractWalletFromPayment(paymentHeader);
if (walletAddress && hasTimeAccess(walletAddress)) {
if (!checkRateLimit(walletAddress)) {
return Response.json({ error: "Rate limited" }, { status: 429 });
}
return serveContent(url.pathname);
}
}
// No valid access - return 402
const price = calculatePrice(url.pathname, walletAddress);
if (!paymentHeader) {
return Response.json(
{
x402Version: 1,
accepts: [
{
scheme: "exact",
network: "solana-devnet",
maxAmountRequired: String(price),
description: `Access to ${url.pathname}`,
payTo: TREASURY_ADDRESS,
asset: { address: USDC_MINT },
// ... other fields
},
],
pricing: {
base: calculatePrice(url.pathname),
current: price,
isPeakHours: isPeakHours(),
},
},
{ status: 402 }
);
}
// Verify and process payment
const isValid = await verifyPayment(paymentHeader);
if (isValid && walletAddress) {
recordUsage(walletAddress);
trackUsage({
timestamp: Date.now(),
walletAddress,
endpoint: url.pathname,
amountPaid: price,
responseTime: Date.now() - startTime,
});
return serveContent(url.pathname);
}
return Response.json({ error: "Invalid payment" }, { status: 402 });
}
return new Response("Not found", { status: 404 });
},
});
What You Learned
- Session tokens for multi-request access
- Dynamic pricing based on time and usage
- Time-based access tiers
- Usage analytics and tracking
- Rate limiting to prevent abuse
- Combining multiple patterns
Next Up
Time to deploy. We’ll get your x402 app running in production with proper infrastructure.
Related Articles
- Understanding requestAnimationFrame
A practical guide to browser animation timing. Learn what requestAnimationFrame actually does, why it beats setInterval, and how to use it properly.
- Lambda Expressions vs Anonymous Functions
When learning a functional programming style you will often come across the term Lambda Expressions or Lambda Functions. In simple terms they are just functions that can be used as data and therefore declared as a value. Let's explore a few examples.
- JavaScript typeof Number
Often you will need to check that you have a number before using it in your JavaScript, here's how.