Blog

Developer Guide

Building Compliance-Friendly Stellar Apps: Logging, Auditing, and Transaction Traceability

If you are building a fintech application, an exchange, or a tokenized asset platform on Stellar, compliance is not something you bolt on later. Regulators want to see that your application was designed with traceability, auditability, and record-keeping from day one. Stellar's architecture makes this easier than most blockchains — every transaction is public, deterministic, and final within seconds. But you still need to build the right logging and audit infrastructure on your side.

Why Stellar Is Compliance-Friendly by Design

Stellar has several properties that regulators and compliance teams appreciate:

PropertyWhy It Helps
**Deterministic finality**A settled transaction is final. No chain reorganizations. No waiting for block confirmations.
**Account-based model**Every account has a known public key. Easier to track than UTXO models.
**Built-in asset controls**Issuers can require authorization, freeze accounts, and clawback tokens.
**Memo field**Attach reference IDs, customer identifiers, or compliance tags to every transaction.
**Low fees**Sub-cent transaction costs make detailed logging economically feasible.
**Public ledger**All transactions are auditable by anyone with a Horizon node.

The Three Pillars of Compliance Logging

1. Transaction Logging

Every transaction your application sends or receives should be logged with:

const transactionLog = {
  // Blockchain data
  txHash: 'abc123...',
  ledger: 61500000,
  timestamp: '2026-06-08T14:30:00Z',
  successful: true,
  feeCharged: 100,

  // Application data
  internalId: 'PAY-2026-00142',
  customerId: 'USR-789',
  purpose: 'remittance',
  complianceStatus: 'cleared',

  // Counterparty data
  sourceAccount: 'GABC...',
  destinationAccount: 'GXYZ...',
  amount: '1000.00',
  asset: 'USDC',

  // Audit metadata
  initiatedBy: 'api-user-456',
  approvedBy: 'compliance-officer-12',
  ipAddress: '192.168.1.100',
  userAgent: 'MyApp/2.1',
};

2. Audit Trail

An audit trail records who did what, when, and why. It must be append-only — entries can never be modified or deleted:

async function logAuditEntry(entry) {
  const record = {
    id: generateUUID(),
    timestamp: new Date().toISOString(),
    actor: entry.actor,
    action: entry.action,
    target: entry.target,
    details: entry.details,
    ipAddress: entry.ipAddress,
    // Hash chain for tamper detection
    previousHash: await getLastAuditHash(),
    hash: null,
  };

  record.hash = await computeHash(
    record.previousHash +
    record.timestamp +
    record.actor +
    record.action +
    JSON.stringify(record.details)
  );

  await db.auditLog.create({ data: record });
  return record;
}

The hash chain ensures that if any entry is tampered with, the chain breaks and the modification is detectable.

3. Transaction Traceability

Regulators need to trace the full lifecycle of a payment:

Customer request → KYC check → Compliance screening → Transaction built →
Transaction signed → Transaction submitted → Ledger inclusion →
Confirmation → Settlement record → Reporting

Each step should have a timestamp and a reference to the previous step.

Implementing Transaction Logging

Capturing Transactions from Horizon

async function logIncomingTransaction(txHash) {
  const tx = await fetch(`${HORIZON}/transactions/${txHash}`).then(r => r.json());
  const ops = await fetch(`${HORIZON}/transactions/${txHash}/operations`).then(r => r.json());

  const logEntry = {
    txHash: tx.hash,
    ledger: tx.ledger,
    timestamp: tx.created_at,
    successful: tx.successful,
    fee: parseInt(tx.fee_charged),
    memo: tx.memo || null,
    memoType: tx.memo_type,
    sourceAccount: tx.source_account,
    operations: ops._embedded.records.map(op => ({
      type: op.type,
      amount: op.amount,
      asset: op.asset_type === 'native' ? 'XLM' : op.asset_code,
      from: op.from,
      to: op.to,
    })),
    capturedAt: new Date().toISOString(),
  };

  await db.transactionLog.create({ data: logEntry });
  return logEntry;
}

Streaming for Real-Time Capture

Do not rely on periodic polling for compliance-critical logging. Use SSE streaming to capture every transaction as it happens:

function startComplianceStream(accountId) {
  const es = new EventSource(
    `${HORIZON}/accounts/${accountId}/transactions?cursor=now`
  );

  es.onmessage = async (event) => {
    const tx = JSON.parse(event.data);
    await logIncomingTransaction(tx.hash);
    await runComplianceChecks(tx);
  };

  es.onerror = () => {
    console.error('Stream disconnected, reconnecting...');
    setTimeout(() => startComplianceStream(accountId), 5000);
  };
}

Using Stellar's Built-In Compliance Tools

Memo Field for Reference IDs

The memo field lets you attach a reference to every transaction. This is critical for matching on-chain activity to off-chain records:

// When building a payment transaction
const transaction = new StellarSdk.TransactionBuilder(account, {
  fee: '100',
  networkPassphrase: StellarSdk.Networks.PUBLIC,
})
  .addOperation(StellarSdk.Operation.payment({
    destination: recipientId,
    asset: StellarSdk.Asset.native(),
    amount: '1000',
  }))
  .addMemo(StellarSdk.Memo.text('PAY-2026-00142'))
  .setTimeout(30)
  .build();

Use structured memo formats:

  • PAY-{year}-{sequence} for payments
  • WDR-{year}-{sequence} for withdrawals
  • TRF-{year}-{sequence} for internal transfers
  • Authorization Flags

    For tokenized assets that require compliance, use Stellar's authorization flags:

    FlagEffect
    `AUTH_REQUIRED`Accounts must be approved before holding the asset
    `AUTH_REVOCABLE`Issuer can revoke authorization (freeze)
    `AUTH_CLAWBACK_ENABLED`Issuer can claw back tokens
    `AUTH_IMMUTABLE`Flags cannot be changed (locks the configuration)

    For regulated assets, AUTH_REQUIRED + AUTH_REVOCABLE + AUTH_CLAWBACK_ENABLED gives you full control. You approve accounts after KYC, freeze accounts under investigation, and clawback tokens if required by a court order.

    Building a Compliance Report

    Regulators typically want periodic reports showing:

    async function generateComplianceReport(startDate, endDate) {
      const transactions = await db.transactionLog.findMany({
        where: {
          timestamp: { gte: startDate, lte: endDate },
        },
        orderBy: { timestamp: 'asc' },
      });
    
      return {
        period: { start: startDate, end: endDate },
        summary: {
          totalTransactions: transactions.length,
          successfulTransactions: transactions.filter(t => t.successful).length,
          failedTransactions: transactions.filter(t => !t.successful).length,
          totalVolume: transactions.reduce((sum, t) =>
            sum + t.operations.reduce((s, op) => s + parseFloat(op.amount || '0'), 0), 0),
          uniqueAccounts: new Set(transactions.flatMap(t =>
            t.operations.flatMap(op => [op.from, op.to].filter(Boolean))
          )).size,
        },
        flaggedTransactions: transactions.filter(t => t.complianceStatus === 'flagged'),
        transactions: transactions.map(t => ({
          hash: t.txHash,
          time: t.timestamp,
          amount: t.operations[0]?.amount,
          asset: t.operations[0]?.asset,
          from: t.sourceAccount,
          memo: t.memo,
          status: t.complianceStatus,
        })),
      };
    }

    Database Schema for Compliance

    CREATE TABLE transaction_log (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      tx_hash VARCHAR(64) UNIQUE NOT NULL,
      ledger BIGINT NOT NULL,
      timestamp TIMESTAMPTZ NOT NULL,
      successful BOOLEAN NOT NULL,
      fee_charged INTEGER,
      memo TEXT,
      memo_type VARCHAR(20),
      source_account VARCHAR(56) NOT NULL,
      operations JSONB NOT NULL,
      internal_id VARCHAR(50),
      customer_id VARCHAR(50),
      compliance_status VARCHAR(20) DEFAULT 'pending',
      captured_at TIMESTAMPTZ DEFAULT NOW(),
      INDEX idx_timestamp (timestamp),
      INDEX idx_source (source_account),
      INDEX idx_customer (customer_id),
      INDEX idx_compliance (compliance_status)
    );
    
    CREATE TABLE audit_log (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      actor VARCHAR(100) NOT NULL,
      action VARCHAR(50) NOT NULL,
      target VARCHAR(100),
      details JSONB,
      ip_address VARCHAR(45),
      previous_hash VARCHAR(64),
      hash VARCHAR(64) NOT NULL,
      INDEX idx_audit_time (timestamp),
      INDEX idx_audit_actor (actor)
    );

    Retention Policies

    Different jurisdictions have different retention requirements:

    JurisdictionMinimum Retention
    US (BSA/AML)5 years
    EU (AMLD6)5 years after relationship ends
    UK (MLR 2017)5 years
    Singapore (PSOA)5 years

    Design your database with partitioning by date so you can efficiently manage retention:

    -- Partition by year for efficient retention management
    CREATE TABLE transaction_log (
      ...
    ) PARTITION BY RANGE (timestamp);
    
    CREATE TABLE transaction_log_2026 PARTITION OF transaction_log
      FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

    Integrating with LumenQuery

    LumenQuery's Transaction Intelligence and Analytics features provide pre-built compliance infrastructure:

  • Real-time transaction monitoring with SSE streaming
  • Account profiling and classification
  • Watchlist management for monitored accounts
  • Alert system for suspicious activity patterns
  • Audit logging with hash chain verification
  • These features are designed for the exact use cases described in this article — saving you months of development time.

    Best Practices

  • Log before you transact: Write the intent to your audit log before submitting the transaction. If the transaction fails, you have a record of the attempt.
  • Never delete logs: Use soft deletes or retention policies. Compliance officers need the complete picture.
  • Hash chain your audit log: A simple hash chain makes tampering detectable without the complexity of a separate blockchain.
  • Use structured memos: A consistent memo format lets you join on-chain and off-chain records reliably.
  • Monitor in real time: Batch processing is not sufficient for compliance. Use SSE streaming to catch issues as they happen.
  • Separate compliance data from application data: Compliance logs should be in a separate database or schema with restricted access and its own backup schedule.
  • Next Steps

  • Explore the LumenQuery API Documentation for endpoint details
  • Check the Transaction Intelligence dashboard for real-time monitoring
  • Read about Stellar's regulatory positioning

  • *Build compliance into your Stellar application from day one. LumenQuery provides the API infrastructure, transaction monitoring, and audit tools that regulated businesses need. Start free.*