Skip to main content

How Each Heuristic Works

Brief look at the core detection logic for each of the 13 heuristics. All heuristics are pure functions that take a ScanContext and return PrivacySignal[].


1. Fee Payer Reuse

Checks who pays the fee for each transaction. Flags when someone other than the wallet owner pays, or when fees are never self-paid.

// Build set of all fee payers from transaction data
const feePayers = context.feePayers;
const targetIsFeePayer = feePayers.has(target);

// Case: Target never pays own fees → HIGH
if (!targetIsFeePayer && feePayers.size > 0) { ... }

// Case: External fee payers alongside target → MEDIUM/HIGH
if (feePayers.size > 1 && targetIsFeePayer) { ... }

Source: packages/core/src/heuristics/fee-payer-reuse.ts


2. Signer Overlap

Builds a frequency map of signers across all transactions. Flags signers that appear in many transactions, repeated multi-sig sets, and authority hubs.

// Count how often each signer appears
const signerFrequency = new Map<string, number>();
for (const tx of context.transactions) {
for (const signer of tx.signers) {
signerFrequency.set(signer, (signerFrequency.get(signer) || 0) + 1);
}
}

// Flag signers appearing in 30%+ of transactions (excluding target)
const frequentSigners = Array.from(signerFrequency.entries())
.filter(([signer]) => signer !== target)
.filter(([_, count]) => count >= Math.ceil(context.transactionCount * 0.3));

Source: packages/core/src/heuristics/signer-overlap.ts


3. Memo Exposure

Scans memo instructions for PII patterns (emails, phone numbers, names) using regex matching.

// Find all memo program instructions
const memoInstructions = context.instructions
.filter(inst => MEMO_PROGRAMS.includes(inst.programId));

// Check content against PII patterns
const PII_PATTERNS = [
{ pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/,
type: 'email', severity: 'HIGH' },
{ pattern: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/,
type: 'phone', severity: 'HIGH' },
// ... more patterns
];

Source: packages/core/src/heuristics/memo-exposure.ts


4. Known Entity Interaction

Cross-references transfer counterparties with a database of labeled addresses (exchanges, bridges, protocols).

// Group interactions by entity type
for (const [address, label] of context.labels.entries()) {
let interactionCount = 0;
for (const transfer of context.transfers) {
if (transfer.from === address || transfer.to === address) {
interactionCount++;
}
}
// Group by type: 'exchange' → HIGH, 'bridge' → MEDIUM, etc.
}

Source: packages/core/src/heuristics/known-entity.ts


5. Identity Metadata Exposure

Checks if the wallet has interacted with Metaplex (NFT metadata) or Bonfida Name Service (.sol domains).

const METAPLEX = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s';
const BONFIDA = 'namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX';

// Count interactions with identity-linking programs
const metaplexInstructions = context.instructions
.filter(inst => inst.programId === METAPLEX);
const bonfidaInstructions = context.instructions
.filter(inst => inst.programId === BONFIDA);

// Bonfida → HIGH (direct name linkage)
// Metaplex → MEDIUM (NFT metadata)

Source: packages/core/src/heuristics/identity-metadata.ts


6. ATA Linkage

Cross-references token account creation events with transaction fee payers to find wallets that create accounts for others.

// For each create event, find who paid the fee
for (const event of createEvents) {
const tx = context.transactions.find(t => t.signature === event.signature);
if (tx) {
const feePayer = tx.feePayer;
const owner = event.owner;
// If feePayer != owner → someone else set up this account
if (feePayer !== owner) {
creatorOwnerMap.get(feePayer)!.add(owner);
}
}
}
// Flag fee payers who created accounts for 2+ different owners → HIGH

Source: packages/core/src/heuristics/ata-linkage.ts


7. Address Reuse

Categorizes transaction activity into types (DeFi, NFT, Gaming, DAO, Exchange, P2P) and flags wallets that span many categories.

// Categorize by checking program labels against known categories
const DEFI_PROGRAMS = new Set(['JUP', 'Raydium', 'Orca', ...]);
const NFT_PROGRAMS = new Set(['Magic Eden', 'Tensor', ...]);

for (const inst of context.instructions) {
const programName = context.labels.get(inst.programId)?.name || '';
if (DEFI_PROGRAMS.has(programName)) activityTypes.add('DeFi');
if (NFT_PROGRAMS.has(programName)) activityTypes.add('NFT');
// ...
}

// 4+ activity types → HIGH, 3 → MEDIUM

Source: packages/core/src/heuristics/address-reuse.ts


8. Counterparty & PDA Reuse

Counts interactions per counterparty, program, and PDA. Flags concentrated patterns.

// Track counterparty frequency
for (const transfer of context.transfers) {
const counterparty = transfer.from === target ? transfer.to : transfer.from;
interactionCounts.set(counterparty,
(interactionCounts.get(counterparty) || 0) + 1);
}

// Flag counterparties with 3+ interactions
// Also tracks PDA reuse and counterparty-program combos

Source: packages/core/src/heuristics/counterparty-reuse.ts


9. Instruction Fingerprinting

Builds a fingerprint from the sequence of program IDs in each transaction. Flags repeated sequences.

// Create fingerprint per transaction
for (const tx of context.transactions) {
const txInstructions = context.instructions
.filter(inst => inst.signature === tx.signature)
.map(inst => inst.programId);
const sequence = txInstructions.join('->');
sequenceFingerprints.set(sequence,
(sequenceFingerprints.get(sequence) || 0) + 1);
}

// Flag sequences appearing in 20%+ of transactions

Source: packages/core/src/heuristics/instruction-fingerprinting.ts


10. Token Account Lifecycle

Tracks create/close events for token accounts. Flags rent refund patterns that link temporary accounts back to owners.

// Find create and close events
const createEvents = context.tokenAccountEvents.filter(e => e.type === 'create');
const closeEvents = context.tokenAccountEvents.filter(e => e.type === 'close');

// Track where rent refunds go
for (const event of closeEvents) {
if (event.rentRefund) {
refundDestinations.set(event.owner,
(refundDestinations.get(event.owner) || 0) + event.rentRefund);
}
}
// Multiple refunds to same address → MEDIUM

Source: packages/core/src/heuristics/token-account-lifecycle.ts


11. Priority Fee Fingerprinting

Groups transactions by priority fee amount. Flags when the same fee appears in >50% of transactions.

// Count how often each priority fee appears
const feeCounts = new Map<number, number>();
for (const tx of context.transactions) {
if (tx.priorityFee !== undefined) {
feeCounts.set(tx.priorityFee,
(feeCounts.get(tx.priorityFee) || 0) + 1);
}
}

// Flag if any single fee value appears in >50% of txs (min 3)
const [topFee, topCount] = [...feeCounts.entries()]
.sort((a, b) => b[1] - a[1])[0];
if (topCount >= txCount * 0.5 && topCount >= 3) { ... }

Source: packages/core/src/heuristics/priority-fee-fingerprinting.ts


12. Staking Delegation Patterns

Extracts vote account (validator) from stake instructions and flags concentrated delegation.

// Find stake instructions and extract validator
const stakeInstructions = context.instructions
.filter(inst => inst.category === 'stake');

for (const inst of stakeInstructions) {
const voteAccount = inst.data?.voteAccount || inst.accounts?.[1];
if (voteAccount) {
validatorCounts.set(voteAccount,
(validatorCounts.get(voteAccount) || 0) + 1);
}
}

// Flag if 1-2 validators receive all delegations → MEDIUM

Source: packages/core/src/heuristics/staking-delegation.ts


13. Timing Patterns

Calculates transaction rate, inter-transaction gaps, and hour-of-day distribution.

// Burst detection: high tx rate in short time
const txRate = context.transactionCount / timeSpanHours;
if (txRate > 10) { /* HIGH */ }

// Regular interval detection: low variance in gaps
const gaps = timestamps.map((t, i) => t - timestamps[i - 1]);
const coefficientOfVariation = stdDev / avgGap;
if (coefficientOfVariation < 0.3) { /* regular pattern */ }

// Timezone detection: hour-of-day clustering
const hours = timestamps.map(ts => new Date(ts * 1000).getUTCHours());

Source: packages/core/src/heuristics/timing-patterns.ts