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:
| Property | Why 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 → ReportingEach 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 paymentsWDR-{year}-{sequence} for withdrawalsTRF-{year}-{sequence} for internal transfersAuthorization Flags
For tokenized assets that require compliance, use Stellar's authorization flags:
| Flag | Effect |
|---|---|
| `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:
| Jurisdiction | Minimum 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:
These features are designed for the exact use cases described in this article — saving you months of development time.
Best Practices
Next Steps
*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.*