Stateless Subscriptions
Description
Section titled “Description”This example demonstrates getSubscribedTransactions for serverless patterns.
- Use the stateless function instead of the AlgorandSubscriber class
- Manage watermark externally between calls
- Verify no overlap between consecutive calls
Prerequisites
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository’s examples/subscriber directory:
cd examples/subscribernpx tsx 14-stateless-subscriptions.ts/** * Example: Stateless Subscriptions * * This example demonstrates getSubscribedTransactions for serverless patterns. * - Use the stateless function instead of the AlgorandSubscriber class * - Manage watermark externally between calls * - Verify no overlap between consecutive calls * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */import { algo, AlgorandClient } from '@algorandfoundation/algokit-utils';import type { AlgodClient } from '@algorandfoundation/algokit-utils/algod-client';import { getSubscribedTransactions } from '@algorandfoundation/algokit-subscriber';import type { TransactionSubscriptionParams } from '@algorandfoundation/algokit-subscriber/types/subscription';import { printHeader, printStep, printInfo, printSuccess, printError, shortenAddress, formatAlgo,} from './shared/utils.js';
/** * Simulates a serverless/cron handler that receives a watermark, * calls getSubscribedTransactions, and returns results + new watermark. */async function statelessPoll( algod: AlgodClient, watermark: bigint, senderAddr: string,): Promise<{ transactions: string[]; newWatermark: bigint; roundRange: [bigint, bigint] }> { const params: TransactionSubscriptionParams = { filters: [ { name: 'payments', filter: { sender: senderAddr, }, }, ], watermark, syncBehaviour: 'sync-oldest', maxRoundsToSync: 100, };
const result = await getSubscribedTransactions(params, algod);
return { transactions: result.subscribedTransactions.map(txn => txn.id), newWatermark: result.newWatermark, roundRange: result.syncedRoundRange, };}
async function main() { printHeader('14 — getSubscribedTransactions (Stateless)');
// Step 1: Connect to LocalNet printStep(1, 'Connect to LocalNet'); const algorand = AlgorandClient.defaultLocalNet(); const algod = algorand.client.algod; const status = await algod.status(); printInfo(`Current round: ${status.lastRound.toString()}`); printSuccess('Connected to LocalNet');
// Step 2: Create and fund sender account printStep(2, 'Create and fund sender account'); const sender = await algorand.account.fromEnvironment('STATELESS_SENDER', algo(10)); const senderAddr = sender.addr.toString(); printInfo(`Sender: ${shortenAddress(senderAddr)}`);
// Step 3: Send first batch of 2 payments printStep(3, 'Send first batch of payments (2 transactions)'); const txn1 = await algorand.send.payment({ sender: sender.addr, receiver: sender.addr, amount: algo(1), note: 'stateless batch-1 txn-1', }); printInfo(`Txn 1 ID: ${txn1.txIds.at(-1)}`); printInfo(`Txn 1 round: ${txn1.confirmation!.confirmedRound!.toString()}`);
const txn2 = await algorand.send.payment({ sender: sender.addr, receiver: sender.addr, amount: algo(2), note: 'stateless batch-1 txn-2', }); printInfo(`Txn 2 ID: ${txn2.txIds.at(-1)}`); printInfo(`Txn 2 round: ${txn2.confirmation!.confirmedRound!.toString()}`); printSuccess('Sent 2 payments');
// Step 4: First stateless call — watermark = firstRound - 1 to capture batch 1 printStep(4, 'First stateless call (watermark = firstRound - 1)'); const initialWatermark = txn1.confirmation!.confirmedRound! - 1n; printInfo(`Input watermark: ${initialWatermark.toString()}`);
const firstCall = await statelessPoll(algod, initialWatermark, senderAddr);
printInfo(`Transactions found: ${firstCall.transactions.length.toString()}`); printInfo(`Round range: [${firstCall.roundRange[0]}, ${firstCall.roundRange[1]}]`); printInfo(`New watermark: ${firstCall.newWatermark.toString()}`); for (const txId of firstCall.transactions) { printInfo(` Matched txn: ${txId}`); }
if (firstCall.transactions.length !== 2) { printError(`Expected 2 transactions in first call, got ${firstCall.transactions.length}`); throw new Error(`Expected 2 transactions in first call, got ${firstCall.transactions.length}`); } printSuccess('First call returned 2 transactions');
// Step 5: Send second batch of 2 payments printStep(5, 'Send second batch of payments (2 transactions)'); const txn3 = await algorand.send.payment({ sender: sender.addr, receiver: sender.addr, amount: algo(3), note: 'stateless batch-2 txn-3', }); printInfo(`Txn 3 ID: ${txn3.txIds.at(-1)}`); printInfo(`Txn 3 round: ${txn3.confirmation!.confirmedRound!.toString()}`);
const txn4 = await algorand.send.payment({ sender: sender.addr, receiver: sender.addr, amount: algo(4), note: 'stateless batch-2 txn-4', }); printInfo(`Txn 4 ID: ${txn4.txIds.at(-1)}`); printInfo(`Txn 4 round: ${txn4.confirmation!.confirmedRound!.toString()}`); printSuccess('Sent 2 more payments');
// Step 6: Second stateless call — uses newWatermark from first call printStep(6, 'Second stateless call (watermark from first call)'); printInfo(`Input watermark: ${firstCall.newWatermark.toString()}`);
const secondCall = await statelessPoll(algod, firstCall.newWatermark, senderAddr);
printInfo(`Transactions found: ${secondCall.transactions.length.toString()}`); printInfo(`Round range: [${secondCall.roundRange[0]}, ${secondCall.roundRange[1]}]`); printInfo(`New watermark: ${secondCall.newWatermark.toString()}`); for (const txId of secondCall.transactions) { printInfo(` Matched txn: ${txId}`); }
if (secondCall.transactions.length !== 2) { printError(`Expected 2 transactions in second call, got ${secondCall.transactions.length}`); throw new Error( `Expected 2 transactions in second call, got ${secondCall.transactions.length}`, ); } printSuccess('Second call returned only new transactions');
// Step 7: Verify no overlap — second call should NOT contain first batch txns printStep(7, 'Verify no overlap between calls'); const firstCallIds = new Set(firstCall.transactions); const overlap = secondCall.transactions.filter(id => firstCallIds.has(id)); if (overlap.length > 0) { printError(`Found ${overlap.length} overlapping transactions between calls`); throw new Error('Overlap detected between first and second call'); } printSuccess('No overlap — second call returned only new transactions');
// Step 8: Contrast with AlgorandSubscriber class printStep(8, 'Contrast: getSubscribedTransactions vs AlgorandSubscriber'); console.log(); console.log(' getSubscribedTransactions (stateless):'); console.log(' - No class instantiation, no event system'); console.log(' - Caller manages watermark externally (DB, file, env var)'); console.log(' - Single function call: params in -> result out'); console.log(' - Ideal for serverless functions, cron jobs, Lambda/Cloud Functions'); console.log(' - No polling loop — caller controls when/how often to call'); console.log(); console.log(' AlgorandSubscriber (stateful):'); console.log(' - Class with start/stop, event emitters (on, onBatch)'); console.log(' - Built-in watermark persistence (get/set callbacks)'); console.log(' - Built-in polling loop with configurable frequency'); console.log(' - Ideal for long-running services and real-time subscriptions'); console.log();
// Summary printStep(9, 'Summary'); printInfo(`First call watermark: ${initialWatermark} -> ${firstCall.newWatermark}`); printInfo(`Second call watermark: ${firstCall.newWatermark} -> ${secondCall.newWatermark}`); printInfo( `Total transactions: ${firstCall.transactions.length + secondCall.transactions.length}`, ); printSuccess('Stateless subscription pattern demonstrated successfully');
printHeader('Example complete');}
main().catch(err => { printError(err.message); process.exit(1);});Other examples
Section titled “Other examples”- Basic Poll Once
- Continuous Subscriber
- Payment Filters
- Asset Transfer Subscription
- App Call Subscription
- Multiple Named Filters
- Balance Change Tracking
- ARC-28 Event Subscription
- Inner Transaction Subscription
- Batch Handling & Data Mappers
- Watermark Persistence
- Sync Behaviours
- Custom Filters
- Stateless Subscriptions
- Lifecycle Hooks & Error Handling