Skip to content
Algorand Developer Portal

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