Agentic Payment Audit Logs: Compliance Trails with Streaming SQL

Agentic Payment Audit Logs: Compliance Trails with Streaming SQL

Introduction

An AI agent just bought groceries on behalf of a user. Another agent topped up an OpenAI subscription. A third booked a hotel for next week. By the time a compliance officer reviews these transactions, they want to answer one question: who acted, on whose behalf, under what authority, and was the action permitted?

That question is the core of every payment audit. With human-initiated payments, the answer is short: the user clicked a button. With agentic payments, the answer is longer. An agent acted under a mandate that was issued by a user with specific limits on amount, merchant, and time. The mandate was validated. A payment was attempted. It either succeeded or failed for a reason. Each step needs to be captured, immutable, and queryable.

Compliance frameworks like PCI DSS, SOC 2, and GDPR were written before agents existed, but their core requirements still apply: log every action, retain logs for a defined period, prove controls work, and respond to investigations within hours. The challenge is that agentic payment systems generate orders of magnitude more events per user than traditional checkout flows, and the events arrive on a continuous stream rather than in tidy daily batches.

This post walks through building a compliance-grade audit log for agentic payments using streaming SQL in RisingWave (v2.8.0), with the canonical audit log sinking to Apache Iceberg for long-term retention. Every SQL example was executed against a running RisingWave instance and the real outputs are embedded.

What an Agentic Payment Audit Log Must Capture

A useful audit log answers six questions for every event. Compliance reviewers, fraud analysts, and engineers should be able to read a single row and reconstruct what happened.

QuestionFieldExample
Who acted?agent_idagent_alpha
On whose behalf?user_iduser_001
Under what authority?mandate_idmnd_001
What did they do?action_typepayment
At what merchant, for how much?merchant, amountamazon.com, 89.99
What was the outcome?outcome, detailssuccess, plus structured JSON

The mandate is the central concept. A mandate is a signed authorization that an agent presents to act on behalf of a user. Each mandate has limits: a maximum amount per transaction, a list of allowed merchants, an expiration time, and possibly other constraints like geography or category. When the agent attempts a payment, the system validates the proposed action against the mandate before charging the card.

A complete agentic payment audit log captures four event categories: mandate issuance (user grants authority), mandate validation (system checks proposed action against mandate; both success and failure must be logged because failures are early signals of misbehavior or compromise), payment (the actual financial movement), and refund (money returned, with a link back to the original payment).

Beyond the six core fields, two more are non-negotiable: an event timestamp with timezone (a naive timestamp is worthless during cross-border investigations), and a tamper-evidence hash computed over the immutable fields. We compute the hash inline using md5. In production, you would chain hashes (each row hashes the previous row's hash) for full hash-chain integrity.

Compliance Frameworks Touching Agentic Payments

Agentic payments do not get a single dedicated regulation yet, but they sit at the intersection of several existing frameworks. Any production system should plan for all of these.

PCI DSS v4.0

The Payment Card Industry Data Security Standard governs anyone storing, processing, or transmitting cardholder data. The relevant audit log requirements:

  • Requirement 10.2: Log all access to cardholder data, including identity, type of event, date and time, success or failure indication, origin of event, and the affected data or resource.
  • Requirement 10.7: Retain audit log history for at least 12 months, with at least three months immediately available for analysis.
  • Requirement 10.5: Protect audit logs so they cannot be altered. This is where the tamper-evidence hash comes in.

For agentic payments, the agent counts as the user (origin of event), and the mandate counts as the authentication context. PCI DSS does not currently distinguish agent-initiated from human-initiated transactions, so the same logging rules apply.

SOC 2 Type II

SOC 2 audits assess whether controls operate effectively over a period of time, typically 6 to 12 months. The auditor needs evidence that controls were continuously enforced, not just present at audit time. An audit log of every mandate validation outcome is direct evidence: every payment was checked against the mandate, every check produced a logged result, and the log is queryable for any window the auditor selects.

GDPR

Article 30 of GDPR requires records of processing activities. When an agent acts on behalf of a user, that processing involves personal data: at minimum the user's identity and purchase history, often more. The audit log doubles as the GDPR processing record. It also supports purpose limitation: by recording the mandate, you prove every payment fell within the purpose the user authorized.

MiCA and country-specific rules

If the agent uses stablecoins or other crypto rails, the EU's Markets in Crypto-Assets Regulation (MiCA) applies. The same audit log structure works; the merchant field becomes a wallet address and the amount becomes a token quantity. Country-specific rules add their own logging expectations: PSD2 (EU) for strong customer authentication, the CFPB Open Banking Rule (US) for third-party data access, APRA CPS 234 (Australia) for information security incidents, and the MAS TRM Guidelines (Singapore) for audit trails and change management.

The canonical audit log we build below satisfies the structural requirements of all of these. Retention periods and report formats vary, but the underlying data model is the same.

Architecture: Streaming Capture + Iceberg Long-Term Storage

The pipeline has two storage tiers. RisingWave handles recent activity (queryable in seconds) and Apache Iceberg handles historical activity (cheap, durable, queryable by any engine).

flowchart LR
    A[Agent Runtime] -->|emit events| K[Kafka / Redpanda]
    K --> R[RisingWave]
    R -->|materialized views| L[Live Audit Queries]
    R -->|materialized views| F[Live Compliance Alerts]
    R -->|sink| I[Apache Iceberg]
    I --> S[Spark / Trino / DuckDB]
    I --> Q[Compliance Reports]
    L --> O[Compliance Officer]
    F --> SOC[SOC Analyst]
    Q --> A2[Annual Audit]

The agent runtime emits structured events to Kafka. RisingWave consumes the stream, builds materialized views for the canonical audit log, the per-user activity summary, and the compliance red flags view, and sinks the canonical audit log to Iceberg. Recent queries (last 30 to 90 days) hit the materialized views directly. Historical queries (months or years back) hit Iceberg.

This split matters because compliance has two very different access patterns. Live response (a SOC analyst investigating an alert that fired five minutes ago) needs sub-second freshness over a small recent window. Annual audits and breach investigations need cheap scans over months or years of data. A single store optimized for one is bad at the other; using both costs less than over-provisioning either one.

For more on the streaming-to-Iceberg pattern, see the RisingWave Iceberg integration docs.

Building the Live Audit Log With Streaming SQL

We will build the pipeline with verified SQL on RisingWave v2.8.0. All examples use the aap15_ prefix for object names so they will not collide with anything else in your dev database.

Step 1: The agent events table

The base table receives events from the agent runtime. In production this would be a Kafka source or table; for the walkthrough we use direct inserts.

CREATE TABLE aap15_agent_events (
    event_id VARCHAR PRIMARY KEY,
    agent_id VARCHAR,
    user_id VARCHAR,
    mandate_id VARCHAR,
    action_type VARCHAR,
    merchant VARCHAR,
    amount DECIMAL,
    outcome VARCHAR,
    details JSONB,
    event_time TIMESTAMPTZ
);

In production, replace the table with a Kafka source by adding WITH (connector = 'kafka', topic = 'agent.audit.events', ...) FORMAT PLAIN ENCODE JSON so RisingWave consumes events directly from your agent runtime's event bus.

Insert a representative set of audit events covering issuance, validation, payment, and refund:

INSERT INTO aap15_agent_events VALUES
  -- Mandate issuance: user authorizes an agent
  ('evt_001', 'agent_alpha', 'user_001', 'mnd_001', 'mandate_issuance', NULL, NULL, 'success',
   '{"max_amount": 500.00, "merchants_allowed": ["amazon.com","wholefoods.com"], "expires_at": "2026-06-06"}',
   '2026-05-01 09:00:00+00'),
  -- Mandate validation success and failure
  ('evt_005', 'agent_alpha', 'user_001', 'mnd_001', 'mandate_validation', 'amazon.com', 89.99, 'success',
   '{"check": "merchant_allowed", "amount_within_limit": true}',
   '2026-05-02 14:23:11+00'),
  ('evt_006', 'agent_alpha', 'user_001', 'mnd_001', 'mandate_validation', 'bestbuy.com', 250.00, 'failure',
   '{"check": "merchant_allowed", "reason": "merchant_not_in_allowlist"}',
   '2026-05-02 14:30:00+00'),
  -- Payment success
  ('evt_010', 'agent_alpha', 'user_001', 'mnd_001', 'payment', 'amazon.com', 89.99, 'success',
   '{"payment_id": "pay_abc123", "card_last4": "4242", "auth_code": "X1Y2Z3"}',
   '2026-05-02 14:23:42+00'),
  -- Payment failure
  ('evt_015', 'agent_alpha', 'user_001', 'mnd_001', 'payment', 'amazon.com', 410.00, 'failure',
   '{"reason": "insufficient_funds", "card_last4": "4242"}',
   '2026-05-04 10:00:00+00'),
  -- Refund
  ('evt_017', 'agent_alpha', 'user_001', 'mnd_001', 'refund', 'amazon.com', 89.99, 'success',
   '{"original_payment_id": "pay_abc123", "reason": "customer_request"}',
   '2026-05-06 09:00:00+00');
  -- The full verified dataset contains 26 events covering all four action
  -- types plus off-hours activity, large amounts, and repeated mandate violations.

Step 2: The canonical audit log materialized view

The canonical audit log is an immutable-shape view that any downstream consumer (live queries, Iceberg sink, alert engine) can read. It adds the tamper-evidence hash and pins the column order.

CREATE MATERIALIZED VIEW aap15_audit_log_mv AS
SELECT
    event_id,
    event_time,
    agent_id,
    user_id,
    mandate_id,
    action_type,
    merchant,
    amount,
    outcome,
    details,
    md5(
        event_id || ':' ||
        COALESCE(agent_id, '') || ':' ||
        COALESCE(user_id, '') || ':' ||
        COALESCE(mandate_id, '') || ':' ||
        action_type || ':' ||
        COALESCE(merchant, '') || ':' ||
        COALESCE(amount::VARCHAR, '') || ':' ||
        outcome || ':' ||
        event_time::VARCHAR
    ) AS audit_hash
FROM aap15_agent_events;

Sampling the view confirms the hash is computed for every row:

 event_id |        event_time         |  agent_id   | user_id  |    action_type     |  merchant   | amount | outcome | audit_hash_prefix
----------+---------------------------+-------------+----------+--------------------+-------------+--------+---------+-------------------
 evt_001  | 2026-05-01 09:00:00+00:00 | agent_alpha | user_001 | mandate_issuance   |             |        | success | ed042679ce6d
 evt_002  | 2026-05-01 10:30:00+00:00 | agent_beta  | user_002 | mandate_issuance   |             |        | success | 3e25eb10b478
 evt_003  | 2026-05-01 11:15:00+00:00 | agent_gamma | user_003 | mandate_issuance   |             |        | success | 091bfb014522
 evt_005  | 2026-05-02 14:23:11+00:00 | agent_alpha | user_001 | mandate_validation | amazon.com  |  89.99 | success | 592da48831a0
 evt_010  | 2026-05-02 14:23:42+00:00 | agent_alpha | user_001 | payment            | amazon.com  |  89.99 | success | 5fbdd5e0ee6b
 evt_006  | 2026-05-02 14:30:00+00:00 | agent_alpha | user_001 | mandate_validation | bestbuy.com | 250.00 | failure | fb64e56ad237
 evt_008  | 2026-05-03 09:00:00+00:00 | agent_beta  | user_002 | mandate_validation | openai.com  | 350.00 | failure | 2a5601706705

If anyone modifies a row in the upstream events table, the recomputed hash will not match the hash recorded in Iceberg, and the discrepancy will surface during the next reconciliation run. This is the foundation of tamper evidence.

Step 3: Per-user daily activity summary

Compliance and fraud teams routinely ask "what did this user authorize today?" A daily summary view answers that without scanning the raw event stream every time.

CREATE MATERIALIZED VIEW aap15_per_user_summary_mv AS
SELECT
    user_id,
    DATE_TRUNC('day', event_time) AS audit_date,
    COUNT(*) AS total_events,
    COUNT(*) FILTER (WHERE action_type = 'payment' AND outcome = 'success') AS successful_payments,
    COUNT(*) FILTER (WHERE action_type = 'payment' AND outcome = 'failure') AS failed_payments,
    COUNT(*) FILTER (WHERE action_type = 'mandate_validation' AND outcome = 'failure') AS failed_validations,
    COUNT(*) FILTER (WHERE action_type = 'refund') AS refunds,
    COALESCE(SUM(amount) FILTER (WHERE action_type = 'payment' AND outcome = 'success'), 0) AS total_paid,
    COALESCE(SUM(amount) FILTER (WHERE action_type = 'refund' AND outcome = 'success'), 0) AS total_refunded,
    COUNT(DISTINCT agent_id) AS distinct_agents,
    COUNT(DISTINCT mandate_id) AS distinct_mandates_used
FROM aap15_agent_events
GROUP BY user_id, DATE_TRUNC('day', event_time);

Sample output (selected rows):

 user_id  |        audit_date         | total_events | successful_payments | failed_payments | failed_validations | refunds | total_paid | total_refunded | distinct_agents | distinct_mandates_used
----------+---------------------------+--------------+---------------------+-----------------+--------------------+---------+------------+----------------+-----------------+------------------------
 user_001 | 2026-05-02 00:00:00+00:00 |            4 |                   1 |               0 |                  1 |       0 |      89.99 |              0 |               1 |                      2
 user_001 | 2026-05-04 00:00:00+00:00 |            2 |                   1 |               1 |                  0 |       0 |     199.00 |              0 |               1 |                      1
 user_001 | 2026-05-06 00:00:00+00:00 |            2 |                   1 |               0 |                  0 |       1 |      67.30 |          89.99 |               1 |                      1
 user_002 | 2026-05-05 00:00:00+00:00 |            4 |                   0 |               0 |                  3 |       0 |          0 |              0 |               1 |                      1
 user_003 | 2026-05-04 00:00:00+00:00 |            2 |                   1 |               0 |                  0 |       1 |     920.00 |          45.50 |               1 |                      1
 user_003 | 2026-05-05 00:00:00+00:00 |            1 |                   1 |               0 |                  0 |       0 |     980.00 |              0 |               1 |                      1

Notice the row for user_002 on 2026-05-05: three failed validations and zero successful payments. That is the kind of pattern a compliance officer wants to see surfaced automatically.

Step 4: Compliance red flags view

The red flags view encodes the rules that warrant human review. The exact rules vary by organization; this version flags four common patterns:

  • Mandate violations (validation failures)
  • Payment failures
  • Large amounts (here, 500 or more in a single payment)
  • Off-hours activity (between 22:00 and 06:00 UTC)
CREATE MATERIALIZED VIEW aap15_compliance_redflags_mv AS
SELECT
    event_id,
    event_time,
    agent_id,
    user_id,
    mandate_id,
    action_type,
    merchant,
    amount,
    outcome,
    CASE
        WHEN action_type = 'payment' AND outcome = 'failure' THEN 'PAYMENT_FAILURE'
        WHEN action_type = 'mandate_validation' AND outcome = 'failure' THEN 'MANDATE_VIOLATION'
        WHEN action_type = 'payment' AND amount >= 500 THEN 'LARGE_AMOUNT'
        WHEN EXTRACT(HOUR FROM event_time) < 6 OR EXTRACT(HOUR FROM event_time) >= 22 THEN 'OFF_HOURS'
        ELSE 'OTHER'
    END AS flag_reason,
    CASE
        WHEN action_type = 'mandate_validation' AND outcome = 'failure' THEN 3
        WHEN action_type = 'payment' AND outcome = 'failure' THEN 2
        WHEN action_type = 'payment' AND amount >= 500 THEN 2
        ELSE 1
    END AS severity
FROM aap15_agent_events
WHERE
    (action_type = 'payment' AND outcome = 'failure')
    OR (action_type = 'mandate_validation' AND outcome = 'failure')
    OR (action_type = 'payment' AND amount >= 500)
    OR EXTRACT(HOUR FROM event_time) < 6
    OR EXTRACT(HOUR FROM event_time) >= 22;

Sample output:

 event_id |        event_time         |  agent_id   | user_id  |    action_type     |    merchant    | amount | outcome |    flag_reason    | severity
----------+---------------------------+-------------+----------+--------------------+----------------+--------+---------+-------------------+----------
 evt_006  | 2026-05-02 14:30:00+00:00 | agent_alpha | user_001 | mandate_validation | bestbuy.com    | 250.00 | failure | MANDATE_VIOLATION |        3
 evt_008  | 2026-05-03 09:00:00+00:00 | agent_beta  | user_002 | mandate_validation | openai.com     | 350.00 | failure | MANDATE_VIOLATION |        3
 evt_022  | 2026-05-05 10:00:00+00:00 | agent_beta  | user_002 | mandate_validation | aliexpress.com |  50.00 | failure | MANDATE_VIOLATION |        3
 evt_023  | 2026-05-05 10:05:00+00:00 | agent_beta  | user_002 | mandate_validation | temu.com       |  75.00 | failure | MANDATE_VIOLATION |        3
 evt_024  | 2026-05-05 10:10:00+00:00 | agent_beta  | user_002 | mandate_validation | shein.com      |  60.00 | failure | MANDATE_VIOLATION |        3
 evt_015  | 2026-05-04 10:00:00+00:00 | agent_alpha | user_001 | payment            | amazon.com     | 410.00 | failure | PAYMENT_FAILURE   |        2
 evt_013  | 2026-05-04 16:00:00+00:00 | agent_gamma | user_003 | payment            | airbnb.com     | 920.00 | success | LARGE_AMOUNT      |        2
 evt_021  | 2026-05-05 14:00:00+00:00 | agent_gamma | user_003 | payment            | delta.com      | 980.00 | success | LARGE_AMOUNT      |        2
 evt_019  | 2026-05-04 03:15:00+00:00 | agent_alpha | user_001 | payment            | amazon.com     | 199.00 | success | OFF_HOURS         |        1
 evt_020  | 2026-05-05 02:45:00+00:00 | agent_beta  | user_002 | mandate_validation | openai.com     |  19.99 | success | OFF_HOURS         |        1

The cluster of MANDATE_VIOLATION rows at 10:00 to 10:10 on 2026-05-05 (three failed validations in 10 minutes from the same agent and mandate, all at low-trust merchants) is exactly the kind of signal the SOC team should see immediately, not in next month's audit report.

A simple alert query rolls these up:

SELECT
    flag_reason,
    COUNT(*) AS event_count,
    COUNT(DISTINCT user_id) AS users_affected,
    COUNT(DISTINCT agent_id) AS agents_involved
FROM aap15_compliance_redflags_mv
GROUP BY flag_reason
ORDER BY event_count DESC;

Output:

    flag_reason    | event_count | users_affected | agents_involved
-------------------+-------------+----------------+-----------------
 MANDATE_VIOLATION |           5 |              2 |               2
 PAYMENT_FAILURE   |           2 |              2 |               2
 LARGE_AMOUNT      |           2 |              1 |               1
 OFF_HOURS         |           2 |              2 |               2

This whole pipeline is built from three materialized views over one base table. There is no Java, no Flink job graph, no separate stream processor cluster. RisingWave keeps the views fresh incrementally as events arrive.

Sinking the Audit Log to Apache Iceberg

The materialized views give you fast queries on recent activity. For long-term retention (a year for PCI DSS, longer for many other rules), the canonical audit log should land in Iceberg.

Iceberg gives you ACID writes against object storage, schema evolution as event types change, and time travel queries that let auditors ask "what did the audit log look like as of January 1?" Storage costs are pennies per gigabyte per month on S3, GCS, or Azure Blob, which makes long retention cheap.

The sink syntax for streaming the canonical audit log to Iceberg looks like this:

CREATE SINK aap15_audit_log_iceberg_sink FROM aap15_audit_log_mv
WITH (
    connector = 'iceberg',
    type = 'upsert',
    primary_key = 'event_id',
    warehouse.path = 's3://your-bucket/iceberg-warehouse',
    s3.region = 'us-east-1',
    s3.access.key = 'your-access-key',
    s3.secret.key = 'your-secret-key',
    catalog.type = 'rest',
    catalog.uri = 'http://your-iceberg-rest-catalog:8181',
    catalog.name = 'compliance_catalog',
    database.name = 'audit',
    table.name = 'agent_payment_audit_log'
) FORMAT UPSERT ENCODE JSON;

This sink streams the canonical audit log into an Iceberg table named audit.agent_payment_audit_log. RisingWave handles the streaming commits to Iceberg, including atomic snapshots so readers never see partial writes. To actually run the sink you need a configured Iceberg catalog (REST, Glue, JDBC, or Hive Metastore) and object storage credentials. See the RisingWave Iceberg sink documentation for the full configuration matrix.

A few design notes specific to audit log sinks: use UPSERT (not APPEND) so corrections update the canonical row in place via the event_id primary key; partition the Iceberg table by event date so a query like "all events in March 2026" only scans March partitions; and set Iceberg-level retention to satisfy your strictest rule (12 months for PCI DSS, often 7 years for tax purposes), which Iceberg's expiration policies enforce without rewriting active data.

Querying the Audit Log: Live + Historical

Once the pipeline is running, compliance and engineering teams use the same SQL against two stores. RisingWave answers recent questions in milliseconds; Iceberg answers historical questions in seconds.

Live: per-user audit trail

When a compliance officer needs to review user_001's recent activity, they query the materialized view:

SELECT event_id, event_time, agent_id, mandate_id, action_type,
       merchant, amount, outcome, audit_hash
FROM aap15_audit_log_mv
WHERE user_id = 'user_001'
ORDER BY event_time;

Output:

 event_id |        event_time         |  agent_id   | mandate_id |    action_type     |    merchant    | amount | outcome |            audit_hash
----------+---------------------------+-------------+------------+--------------------+----------------+--------+---------+----------------------------------
 evt_001  | 2026-05-01 09:00:00+00:00 | agent_alpha | mnd_001    | mandate_issuance   |                |        | success | ed042679ce6d0f6ccbb60c10d41dddc3
 evt_004  | 2026-05-02 12:00:00+00:00 | agent_alpha | mnd_004    | mandate_issuance   |                |        | success | 9635d43399bc639d2bf452adc78508d9
 evt_005  | 2026-05-02 14:23:11+00:00 | agent_alpha | mnd_001    | mandate_validation | amazon.com     |  89.99 | success | 592da48831a04a24b0268f1f2d363591
 evt_010  | 2026-05-02 14:23:42+00:00 | agent_alpha | mnd_001    | payment            | amazon.com     |  89.99 | success | 5fbdd5e0ee6bc83d7a802eb4d01a4ba8
 evt_006  | 2026-05-02 14:30:00+00:00 | agent_alpha | mnd_001    | mandate_validation | bestbuy.com    | 250.00 | failure | fb64e56ad23772510d31051ed5cfbc09
 evt_019  | 2026-05-04 03:15:00+00:00 | agent_alpha | mnd_001    | payment            | amazon.com     | 199.00 | success | 161a3d2df617de85d4db476775f6d354
 evt_015  | 2026-05-04 10:00:00+00:00 | agent_alpha | mnd_001    | payment            | amazon.com     | 410.00 | failure | 6f38914d617c835361042708beb8f6fa
 evt_014  | 2026-05-05 18:30:00+00:00 | agent_alpha | mnd_004    | payment            | doordash.com   |  32.45 | success | 7916fba66c0986ef05ff4bc2f6fb27c7
 evt_017  | 2026-05-06 09:00:00+00:00 | agent_alpha | mnd_001    | refund             | amazon.com     |  89.99 | success | d92804455c4e49115c5664100f54f3e2
 evt_025  | 2026-05-06 17:00:00+00:00 | agent_alpha | mnd_001    | payment            | wholefoods.com |  67.30 | success | ed96b526a0da9d5538384bc7458db335

Reading top to bottom tells the full story: the user issued the mandate, the agent validated and paid amazon.com, then attempted bestbuy.com and was correctly blocked, then had a payment fail for insufficient funds before refunding the original Amazon purchase. Every step has a hash that can be reconciled against Iceberg.

Live: detect repeated mandate violations

SELECT user_id, mandate_id, COUNT(*) AS validation_failures
FROM aap15_audit_log_mv
WHERE action_type = 'mandate_validation' AND outcome = 'failure'
GROUP BY user_id, mandate_id
HAVING COUNT(*) >= 3
ORDER BY validation_failures DESC;

Output:

 user_id  | mandate_id | validation_failures
----------+------------+---------------------
 user_002 | mnd_002    |                   4

Four validation failures on the same mandate is a strong signal that either the mandate is being abused or the agent is broken. This kind of query runs continuously in a monitoring tool against the materialized view.

Historical: scan months of data from Iceberg

For an annual SOC 2 audit or a regulatory inquiry about months-old events, the team queries the Iceberg table directly using their query engine of choice (Spark, Trino, DuckDB, Snowflake external tables, or Athena):

-- Annual rollup against the Iceberg table (Trino syntax)
SELECT user_id, COUNT(*) AS total_events,
       SUM(CASE WHEN action_type = 'payment' AND outcome = 'success'
                THEN amount ELSE 0 END) AS total_paid
FROM compliance_catalog.audit.agent_payment_audit_log
WHERE event_time BETWEEN TIMESTAMP '2026-01-01 00:00:00 UTC'
                     AND TIMESTAMP '2026-12-31 23:59:59 UTC'
GROUP BY user_id;

Iceberg's time travel also lets auditors ask "what did the audit log show as of June 30?" by adding FOR TIMESTAMP AS OF TIMESTAMP '2026-06-30 23:59:59 UTC' to the query. If anyone modified the table after the fact, time travel makes that visible. For more on the streaming-to-lakehouse pattern, see our streaming SQL guide.

Cleanup

When you are done experimenting, drop everything:

DROP MATERIALIZED VIEW IF EXISTS aap15_compliance_redflags_mv;
DROP MATERIALIZED VIEW IF EXISTS aap15_per_user_summary_mv;
DROP MATERIALIZED VIEW IF EXISTS aap15_audit_log_mv;
DROP TABLE IF EXISTS aap15_agent_events;

FAQ

What must an agentic payment audit log capture?

Every event needs to answer six questions: which agent acted, on whose behalf, under what mandate, what action, at what merchant for what amount, and with what outcome. Add a timezone-aware timestamp, a structured details JSON for action-specific context, and a tamper-evidence hash. The four core action types to log are mandate issuance, mandate validation, payment, and refund. Validation failures are particularly important to log since they are early signals of abuse or compromise.

Which compliance frameworks apply to agentic payments?

PCI DSS v4.0 governs cardholder data handling and requires 12 months of audit retention. SOC 2 Type II requires evidence that controls operate over time. GDPR Article 30 requires processing records, which the audit log doubles as. MiCA applies to crypto rails. Country-specific rules like PSD2 (EU), the CFPB Open Banking Rule (US), APRA CPS 234 (Australia), and the MAS TRM Guidelines (Singapore) add their own logging expectations. The structural audit log we build above satisfies all of these; what differs is retention period and report format.

How is real-time audit data different from batch audit data?

Batch audit pipelines run daily or hourly and produce data that is queryable hours or days after the event. That is fine for periodic reporting but useless for live response. Real-time audit pipelines materialize views continuously as events arrive, so a SOC analyst investigating a 10:15 alert can query 10:14 data at 10:16. Real-time data also enables live alerting (a query running every minute against the red-flags view fires when a new violation lands), while batch data can only support next-day alerts.

How does RisingWave handle long-term audit retention?

RisingWave keeps recent activity (typically 30 to 90 days) in materialized views inside its own storage tier for fast live queries, and sinks the canonical audit log to Apache Iceberg for long-term retention. Iceberg stores data on cheap object storage like S3 with ACID guarantees, schema evolution, and time travel. Compliance teams query recent activity through RisingWave's Postgres-compatible interface and historical activity through any Iceberg-compatible engine (Spark, Trino, DuckDB, Athena, Snowflake). Retention policies live on the Iceberg table and are enforced without rewriting active data.

Conclusion

Agentic payments need audit logs that are immutable, queryable, and built in real time. The architecture is straightforward: a streaming SQL engine like RisingWave maintains materialized views over the live event stream for fast compliance queries and live alerts, and sinks the canonical audit log to Apache Iceberg for long-term retention and historical queries. The same SQL describes both stages, the same column model serves both compliance officers and SOC analysts, and the tamper-evidence hash makes any later modification detectable.

Building this with streaming SQL takes one base table and three materialized views, no Java code, no separate stream processor cluster, and no batch ETL job graph. As your event volume grows from hundreds per day to millions, the same SQL keeps working because RisingWave updates the views incrementally rather than re-scanning the source table.

Ready to build a compliance-grade audit log for agentic payments? Try RisingWave Cloud free.

Join our Slack community to compare notes with other teams building real-time compliance pipelines.

Best-in-Class Event Streaming
for Agents, Apps, and Analytics
GitHubXLinkedInSlackYouTube
Sign up for our to stay updated.