Transaction Event Journal
The transaction-event/v1 contract gives Base components a common way to record business-level transaction events. Each producer appends newline-delimited JSON to a dedicated journal file rather than mixing these records into ordinary application output. The usual stdout/stderr path (and its Kubernetes-to-Datadog pipeline) is reserved for logs and must not double as this journal.
A Vector agent follows those JSONL files and forwards each record to audit-archiver. Because the audit ingest endpoint is collector-facing, it reads exactly one event object per line and does not accept a wrapped JSON batch.
Configuration Fields
Section titled “Configuration Fields”Rust producers are expected to surface the following settings, either at the top level or behind a producer-specific prefix:
| Field | Type | Meaning |
|---|---|---|
enabled | boolean | Enables transaction event journal writes. |
file_path | string | Dedicated JSONL file path tailed by Vector. |
queue_capacity | integer | Upper bound on the in-process event queue. Under backpressure a producer discards events rather than stalling its transaction-serving path. |
flush_interval | duration | How often the background writer flushes to the file. |
required | boolean | When true, startup fails if the writer cannot open the file. A failure to write at runtime stays non-fatal and is still surfaced for observability. |
producer | string | One of the producer identities below. |
network | string | Network label, for example base-mainnet or base-sepolia. |
Go and proxyd builds carry the identical field names in TOML:
[transaction_events]enabled = truefile_path = "/var/log/base/transaction-events.jsonl"queue_capacity = 16384flush_interval = "1s"required = falseproducer = "base-routing/proxyd"network = "base-mainnet"Envelope
Section titled “Envelope”Every line is a single JSON object:
{ "schema_version": "transaction-event/v1", "event_id": "0x7d5c4f...", "event_time": "2026-06-02T00:00:00.000000000Z", "producer": "base-reth-node", "event_type": "TXPOOL_PENDING", "network": "base-mainnet", "tx_hash": "0x1111111111111111111111111111111111111111111111111111111111111111", "block_hash": null, "block_number": null, "payload_id": null, "request_id": null, "data": { "pool": "pending" }}These fields are mandatory:
schema_versionevent_idevent_timeproducerevent_type
In normal operation at least one join key should be set — either tx_hash, the block_hash/block_number pair, or payload_id. The request_id field is not required but helps correlate records across proxies and ingress. Producers should avoid emitting journal records for aggregate operational states that cannot be attributed to one of those join keys: broadcast lag, for instance, stays in logs and metrics because the receiver only sees a skipped count, whereas INGRESS_METERING_SEND_DROPPED fires once per dropped transaction precisely because ingress still holds the original tx_hash.
Anything producer-specific goes inside data. Never journal raw transaction bytes, calldata, full request bodies, API keys, secrets, private keys, bearer tokens, authorization headers, raw forwarding headers, or raw client IP forwarding chains.
A collector sidecar may attach deployment-specific origin metadata under data.observability_source before handing events off to audit-archiver. The archiver persists that object alongside the rest of data, but the shared contract places no schema constraints on it.
On the Rust side, the TransactionEvent::validate helper rejects a mismatched schema version, an empty event_id, and a short, exact denylist of unsafe data keys — among them raw_tx, calldata, request_body, authorization, api_key, headers, and x-forwarded-for. Vector collector pipelines are expected to catch wider case and delimiter variants — such as rawTransaction, requestBody, secret_key, and privateKey — before anything is ingested.
Local Devnet Verification
Section titled “Local Devnet Verification”The ingress devnet brings up a local Postgres, a Vector shipper, and a Postgres-backed audit-archiver ingest path:
just devnet ingressjust devnet tx-observability-smokeTo exercise proxyd transaction events before that code ships in the default proxyd image, point BASE_ROUTING_CONTEXT at a local protocols/base-routing checkout:
BASE_ROUTING_CONTEXT=/path/to/base-routing just devnet ingressjust devnet tx-observability-smokeThe smoke test pushes a single transaction through ingress, waits for Vector to deliver the JSONL records from the ingress, proxyd, txpool-tracing, and builder producers, and then confirms audit-archiver can read the stored events back out of Postgres by transaction hash.
Producer Values
Section titled “Producer Values”base-reth-nodebase-builderingress-rpcbase-routing/proxyd
Txpool Tracing Example
Section titled “Txpool Tracing Example”When --enable-transaction-event-journal and --transaction-event-journal-path are supplied, base-reth-node txpool tracing can route its existing live LRU events into the durable journal:
{"schema_version":"transaction-event/v1","event_id":"0x4d6d...","event_time":"2026-06-02T00:00:00Z","producer":"base-reth-node","event_type":"TXPOOL_PENDING","network":"base-mainnet","tx_hash":"0x1111111111111111111111111111111111111111111111111111111111111111","block_hash":null,"block_number":null,"payload_id":null,"request_id":null,"data":{"event_source":"txpool-tracing","txpool_event":"pending","event_index":0,"node_role":"mempool","pool":"pending"}}Event Vocabulary
Section titled “Event Vocabulary”Edge/proxy:
PROXY_RECEIVEDPROXY_REJECTEDPROXY_VALIDATION_ACCEPTEDPROXY_VALIDATION_REJECTEDPROXY_ROUTED_TO_BACKENDPROXY_BACKEND_SUCCESSPROXY_BACKEND_FAILUREPROXY_INGRESS_RPC_ATTEMPTPROXY_INGRESS_RPC_SUCCESSPROXY_INGRESS_RPC_FAILURE
Ingress/audit:
INGRESS_RECEIVEDSIMULATION_STARTEDSIMULATION_SUCCEEDEDSIMULATION_FAILEDINGRESS_TX_FORWARD_ATTEMPTINGRESS_TX_FORWARD_SUCCESSINGRESS_TX_FORWARD_FAILUREINGRESS_METERING_SEND_ATTEMPTINGRESS_METERING_SEND_SUCCESSINGRESS_METERING_SEND_FAILUREINGRESS_METERING_SEND_DROPPED
Mempool/node:
TXPOOL_PENDINGTXPOOL_QUEUEDTXPOOL_PENDING_TO_QUEUEDTXPOOL_QUEUED_TO_PENDINGTXPOOL_DROPPEDTXPOOL_REPLACEDTXPOOL_TRACKING_OVERFLOWEDTXPOOL_BLOCK_INCLUDEDTXPOOL_FLASHBLOCK_INCLUDED
Forwarding:
TXPOOL_BUILDER_FORWARD_ATTEMPTTXPOOL_BUILDER_FORWARD_SUCCESSTXPOOL_BUILDER_FORWARD_FAILURETXPOOL_BUILDER_FORWARD_DROPPEDTXPOOL_VALIDATED_INSERT_ACCEPTEDTXPOOL_VALIDATED_INSERT_REJECTED
TXPOOL_BUILDER_FORWARD_DROPPED is reserved for transaction-scoped drops where the forwarding task still has the tx_hash in hand, such as a final RPC failure after the retries are exhausted. Broadcast lag is deliberately kept out of the journal and stays visible only through logs and metrics.
Builder:
BUILDER_CONSIDEREDBUILDER_ACCEPTEDBUILDER_REJECTEDBUILDER_INCLUDEDBUILDER_PAYLOAD_FINALIZEDBUILDER_FLASHBLOCK_STARTEDBUILDER_FLASHBLOCK_PUBLISHEDBUILDER_FLASHBLOCK_BUILD_STOPPED
Builder caveat: BUILDER_CONSIDERED, BUILDER_ACCEPTED, and BUILDER_REJECTED fire once per payload-building attempt and, where relevant, carry payload_id, block_number, and flashblock_index. As a result, one transaction can generate several decision events across consecutive flashblocks. BUILDER_INCLUDED is recorded once the builder finalizes the payload it can serve through engine_getPayload, and it sets data.inclusion_signal = "builder_finalized_payload". Nonce and validation skips are recorded by the payload loop as BUILDER_REJECTED rather than fabricating replacement relationships. BUILDER_PAYLOAD_FINALIZED is written once for every built payload — even an empty one with no user transactions — and ties the payload_id to the builder’s block hash and number; it includes data.parent_hash, data.transaction_count, data.gas_used, data.gas_limit, and data.timestamp. BUILDER_FLASHBLOCK_STARTED, BUILDER_FLASHBLOCK_PUBLISHED, and BUILDER_FLASHBLOCK_BUILD_STOPPED are scoped to a payload/flashblock and carry top-level payload_id and block_number plus data.parent_hash, data.flashblock_index, and data.target_flashblock_count. Published events additionally set top-level block_hash and data.transaction_count, data.byte_size, and data.build_duration_ms. Build-stopped events use data.reason to label control-flow stops, for example payload resolution winning the race before publish.
Canonicality caveat: builder events describe local payload construction, not canonical-chain or consensus-finality state. A builder event that carries block_hash means the builder computed or published that payload shape — it is not, on its own, evidence that the block became canonical. Tie records back to canonical history through canonical-state observers such as txpool tracing by matching block_hash, block_number, and transaction hashes.
Event ID Guidance
Section titled “Event ID Guidance”Prefer deterministic event_id values whenever the source has stable inputs. Reasonable components to combine include:
producerevent_type- source timestamp bucket or source sequence
tx_hashrequest_id- backend/node identifier when applicable
- attempt index when applicable
When a source genuinely cannot produce a deterministic ID, record the reason in the producer implementation and pack enough fields into data for audit-archiver to enforce uniqueness on the database side.
proxyd Examples
Section titled “proxyd Examples”Received raw transaction request:
{ "schema_version": "transaction-event/v1", "event_id": "0x1f3f...", "event_time": "2026-06-02T00:00:00.000000000Z", "producer": "base-routing/proxyd", "event_type": "PROXY_RECEIVED", "network": "base-mainnet", "tx_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", "block_hash": null, "block_number": null, "payload_id": null, "request_id": "req-abc", "data": { "rpc_method": "eth_sendRawTransaction" }}Validation rejection:
{ "schema_version": "transaction-event/v1", "event_id": "0x2a4b...", "event_time": "2026-06-02T00:00:00.000000000Z", "producer": "base-routing/proxyd", "event_type": "PROXY_VALIDATION_REJECTED", "network": "base-mainnet", "tx_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", "block_hash": null, "block_number": null, "payload_id": null, "request_id": "req-abc", "data": { "rpc_method": "eth_sendRawTransaction", "validation_service": "tx-validation", "fail_open": false }}Routed to node:
{ "schema_version": "transaction-event/v1", "event_id": "0x3b5c...", "event_time": "2026-06-02T00:00:00.000000000Z", "producer": "base-routing/proxyd", "event_type": "PROXY_ROUTED_TO_BACKEND", "network": "base-mainnet", "tx_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", "block_hash": null, "block_number": null, "payload_id": null, "request_id": "req-abc", "data": { "backend": "reth-mainnet-0", "attempt_index": 0 }}TIPS S3/Postgres Parity Checklist
Section titled “TIPS S3/Postgres Parity Checklist”The journal feeds the audit-archiver/Postgres query path that backs the TIPS UI. Before an environment flips TIPS_UI_QUERY_BACKEND=audit, the Postgres route should return the same answers the existing S3-backed route does. Keep S3 serving traffic until the samples below match against audit-archiver, then cut over.
Sample inputs. Pick a representative set and reuse it across both backends:
- A recent block hash that includes at least one non-system transaction, plus that same block addressed by number.
- A transaction hash from that block carrying a
SIMULATION_SUCCEEDEDevent. - A bundle hash or bundle id drawn from that transaction’s event data.
- A rejected transaction from the trailing 31 days, if one exists.
Route comparisons. Run each sample through both backends and check that they line up:
- On
GET /api/block/<block-hash>, the two paths should agree on block hash, number, gas limits, the ordering of transactions, and the transaction hashes themselves. - Addressing the block by number via
GET /api/block/<block-number>should land on that same canonical block. - Per sampled non-system transaction, both backends should concur on whether simulation data is present; where it is, the bundle hash/id, state block number, total gas used, total execution time, state-root time, and the per-transaction gas/time summaries should all match.
GET /api/txn/<tx-hash>should hand back a matching transaction hash along with at least one bundle id/hash join key.- Events from
GET /api/bundle/<bundle-id-or-hash>should arrive in event-time order, with the accepted simulation event for the sample among them. - Where a rejected sample exists,
GET /api/rejectedshould echo its transaction hash, block number, rejection reason, timestamp, and metering summary.
Operational checks:
- On
audit-archiver,TIPS_AUDIT_POSTGRES_URLis configured, and the audit service port answers both JSON-RPC and transaction-event HTTP ingest — that same port is where Vector delivers NDJSON, at/v1/transaction-events/batch. ingress-rpccarriesTIPS_INGRESS_TRANSACTION_EVENTS_ENABLED=truein the test environment alone, its JSONL path aligned with the Vector sidecar mount.- On the UI side,
TIPS_UI_QUERY_BACKEND=auditis set withTIPS_UI_AUDIT_RPC_URLaimed at the audit service. - Writes to S3 keep flowing across the whole comparison window.
- Nothing in the rollout drops a Kafka, S3, or existing RPC persistence path.