Skip to content

Stateless Subscriptions

← Back to Examples

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
  • LocalNet running (via algokit localnet start)

From the repository’s examples/subscriber directory:

Terminal window
cd examples/subscriber
npx tsx 14-stateless-subscriptions.ts

View source on GitHub

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)
})