Skip to content
Algorand Developer Portal

Custom Filters

← Back to Examples

This example demonstrates custom filter predicates with multi-condition logic.

  • Use customFilter for complex matching (amount + note + sender allowlist)
  • Compose customFilter with standard filter fields
  • Inspect full SubscribedTransaction fields available in customFilter
  • LocalNet running (via algokit localnet start)

From the repository’s examples/subscriber directory:

Terminal window
cd examples/subscriber
npx tsx 13-custom-filters.ts

View source on GitHub

13-custom-filters.ts
/**
* Example: Custom Filters
*
* This example demonstrates custom filter predicates with multi-condition logic.
* - Use customFilter for complex matching (amount + note + sender allowlist)
* - Compose customFilter with standard filter fields
* - Inspect full SubscribedTransaction fields available in customFilter
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { algo, AlgorandClient } from '@algorandfoundation/algokit-utils';
import {
printHeader,
printStep,
printInfo,
printSuccess,
printError,
shortenAddress,
formatMicroAlgo,
createFilterTester,
type SubscribedTransaction,
} from './shared/utils.js';
async function main() {
printHeader('13 — Custom Filters');
// Step 1: Connect to LocalNet
printStep(1, 'Connect to LocalNet');
const algorand = AlgorandClient.defaultLocalNet();
const status = await algorand.client.algod.status();
printInfo(`Current round: ${status.lastRound.toString()}`);
printSuccess('Connected to LocalNet');
// Step 2: Create 3 accounts (sender, receiver, outsider)
printStep(2, 'Create and fund 3 accounts');
const sender = await algorand.account.fromEnvironment('CUSTOM_SENDER', algo(100));
const receiver = await algorand.account.fromEnvironment('CUSTOM_RECEIVER', algo(100));
const outsider = await algorand.account.fromEnvironment('CUSTOM_OUTSIDER', algo(100));
const senderAddr = sender.addr.toString();
const receiverAddr = receiver.addr.toString();
const outsiderAddr = outsider.addr.toString();
printInfo(`Sender: ${shortenAddress(senderAddr)}`);
printInfo(`Receiver: ${shortenAddress(receiverAddr)}`);
printInfo(`Outsider: ${shortenAddress(outsiderAddr)}`);
printSuccess('3 accounts created and funded');
// Step 3: Send 6 payments with varying amounts, notes, and senders
printStep(3, 'Send 6 payments with varying senders, amounts, and notes');
const allowlist = new Set([senderAddr, receiverAddr]);
const payments = [
// Txn 1: sender->receiver, 5 ALGO, "transfer-urgent" => PASS (allowlisted, >=2 ALGO, "transfer" keyword)
{ sender: sender.addr, receiver: receiver.addr, amount: algo(5), note: 'transfer-urgent' },
// Txn 2: sender->outsider, 1 ALGO, "transfer-low" => FAIL (amount < 2 ALGO)
{ sender: sender.addr, receiver: outsider.addr, amount: algo(1), note: 'transfer-low' },
// Txn 3: receiver->sender, 3 ALGO, "transfer-normal" => PASS (allowlisted, >=2 ALGO, "transfer" keyword)
{ sender: receiver.addr, receiver: sender.addr, amount: algo(3), note: 'transfer-normal' },
// Txn 4: outsider->receiver, 4 ALGO, "transfer-big" => FAIL (outsider not in allowlist)
{ sender: outsider.addr, receiver: receiver.addr, amount: algo(4), note: 'transfer-big' },
// Txn 5: sender->receiver, 2 ALGO, "payment-misc" => FAIL (note doesn't contain "transfer")
{ sender: sender.addr, receiver: receiver.addr, amount: algo(2), note: 'payment-misc' },
// Txn 6: receiver->outsider, 10 ALGO, "transfer-final" => PASS (allowlisted, >=2 ALGO, "transfer" keyword)
{ sender: receiver.addr, receiver: outsider.addr, amount: algo(10), note: 'transfer-final' },
];
const txnResults = [];
for (const [i, p] of payments.entries()) {
const result = await algorand.send.payment({
sender: p.sender,
receiver: p.receiver,
amount: p.amount,
note: p.note,
});
txnResults.push(result);
printInfo(
`Txn ${i + 1}: ${shortenAddress(p.sender.toString())} -> ${shortenAddress(p.receiver.toString())} | ${formatMicroAlgo(p.amount.microAlgo)} | note: "${p.note}"`,
);
}
printSuccess(`Sent ${payments.length} payments`);
// Record watermark before first txn
const watermarkBefore = txnResults[0].confirmation!.confirmedRound! - 1n;
const testFilter = createFilterTester(algorand.client.algod, watermarkBefore);
// Step 4: customFilter only — multi-condition logic
printStep(
4,
'Custom filter: amount >= 2 ALGO AND note contains "transfer" AND sender in allowlist',
);
const THRESHOLD = 2_000_000n; // 2 ALGO in microAlgos
const customOnlyTxns = await testFilter(
'custom-only',
{
customFilter: (txn: SubscribedTransaction) => {
const amount = txn.paymentTransaction?.amount ?? 0n;
const note = txn.note ? Buffer.from(txn.note).toString('utf-8') : '';
const sender = txn.sender;
const amountOk = amount >= THRESHOLD;
const noteOk = note.includes('transfer');
const senderOk = allowlist.has(sender);
return amountOk && noteOk && senderOk;
},
},
3,
'Custom filter matched exactly 3 transactions (txns 1, 3, 6)',
);
console.log();
// Print pass/fail for all 6 transactions
printInfo(`Pass/Fail analysis: all 6 transactions`);
for (const [i, p] of payments.entries()) {
const amount = BigInt(p.amount.microAlgo);
const note = p.note;
const senderStr = p.sender.toString();
const amountOk = amount >= THRESHOLD;
const noteOk = note.includes('transfer');
const senderOk = allowlist.has(senderStr);
const passed = amountOk && noteOk && senderOk;
const reasons = [];
if (!amountOk)
reasons.push(`amount ${formatMicroAlgo(amount)} < ${formatMicroAlgo(THRESHOLD)}`);
if (!noteOk) reasons.push(`note "${note}" missing "transfer"`);
if (!senderOk) reasons.push(`sender ${shortenAddress(senderStr)} not in allowlist`);
const status = passed ? 'PASS' : 'FAIL';
const detail = passed ? 'all conditions met' : reasons.join(', ');
printInfo(` Txn ${i + 1} [${status}]: ${detail}`);
}
// Step 5: Combine customFilter with standard filter fields
printStep(
5,
'Composition: sender=sender account (standard) + customFilter (amount >= 2 ALGO AND note contains "transfer")',
);
// Sender sent txns 1, 2, 5. Of those, only txn 1 has amount >= 2 ALGO AND "transfer" in note
const composedTxns = await testFilter(
'composed',
{
sender: senderAddr,
customFilter: (txn: SubscribedTransaction) => {
const amount = txn.paymentTransaction?.amount ?? 0n;
const note = txn.note ? Buffer.from(txn.note).toString('utf-8') : '';
return amount >= THRESHOLD && note.includes('transfer');
},
},
1,
'Composed filter matched 1 transaction (txn 1: sender, 5 ALGO, "transfer-urgent")',
txn => {
const amount = txn.paymentTransaction?.amount ?? 0n;
const note = txn.note ? Buffer.from(txn.note).toString('utf-8') : '';
printInfo(` Matched: ${txn.id} | ${formatMicroAlgo(amount)} | note: "${note}"`);
},
);
// Step 6: Show customFilter receives full SubscribedTransaction fields
printStep(6, 'Inspect full SubscribedTransaction fields available in customFilter');
const inspectedFields: string[] = [];
await testFilter('inspect', {
customFilter: (txn: SubscribedTransaction) => {
// Collect field names from the first transaction to show what's available
if (inspectedFields.length === 0) {
const fields = Object.keys(txn).filter(
k => txn[k as keyof SubscribedTransaction] !== undefined,
);
inspectedFields.push(...fields);
}
return true; // match all
},
});
printInfo(`Available fields on SubscribedTransaction: ${inspectedFields.length.toString()}`);
printInfo(`Fields: ${inspectedFields.join(', ')}`);
printSuccess('customFilter receives fully decoded SubscribedTransaction with all fields');
// Step 7: Summary
printStep(7, 'Summary');
printInfo(
`Custom filter only: ${customOnlyTxns.length} matched (multi-condition: amount + note + sender allowlist)`,
);
printInfo(
`Composed filter: ${composedTxns.length} matched (standard sender + custom amount/note)`,
);
printInfo(`Key takeaway: customFilter is AND-composed with standard filter fields`);
printInfo(
`Key takeaway: customFilter receives the full SubscribedTransaction with all decoded fields`,
);
printHeader('Example complete');
}
main().catch(err => {
printError(err.message);
process.exit(1);
});