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