FPSF-SS-003 — Chain Event Indexer
Table of Contents
- Abstract
- Introduction
- 2.1 Purpose
- 2.2 Scope
- 2.3 Naming Note
- 2.4 Normative References
- 2.5 Conventions and Terminology
- Role Within the Stablecoin Stack
- Startup and Initialization
- Network Scope
- Blockchain Connectivity
- Watched Events
- Event Record Schemas
- Message Queue Distribution
- 9.1 Topic Layout
- 9.2 Message Envelope
- 9.3 Batch Packing
- 9.4 Delivery Guarantees
- On-Demand Historical Query
- 10.1 Request Topic
- 10.2 Request Message Format
- 10.3 Response Topic
- 10.4 Response Message Format
- 10.5 Error Responses
- Configuration Reference
- Operational Requirements
- 12.1 Persistent State
- 12.2 Health and Observability
- 12.3 Graceful Shutdown
- Security Considerations
- Conformance
- Future Work
- Change Log
- License
1. Abstract
This document specifies the Chain Event Indexer (CEI) — the Stablecoin Stack component responsible for monitoring an EVM-compatible blockchain, collecting relevant on-chain events, and distributing them to downstream consumers via a message queue. It is referred to as transfer-history in FPSF-SS-001.
The CEI watches a single blockchain network per instance, monitors all tokens declared as supported by the Basic Data Service, and emits structured event records to well-defined message queue topics. It is the authoritative source for the shape of all event record interfaces in the Stablecoin Stack.
2. Introduction
2.1 Purpose
This specification defines the normative requirements for implementing and deploying a conformant CEI instance. It is the authoritative reference for:
- engineers implementing the CEI component;
- processor operators deploying and operating a CEI instance;
- engineers building downstream consumers that subscribe to CEI topics; and
- auditors reviewing the correctness and completeness of a CEI deployment.
2.2 Scope
This specification covers:
- startup, initialization, and token validation procedures;
- blockchain connectivity and block ingestion strategy;
- the set of events collected and their schema;
- message queue topic layout, message envelope, and batch packing;
- the on-demand historical query interface;
- configuration parameters; and
- operational, persistence, and observability requirements.
This specification does not cover:
- the internal protocols of downstream consumers such as
balance-and-historyorcore-checkout-engine; - the on-chain Settlement Contract interface (specified in FPSF-SS-001, Sections 10–13);
- the Basic Data Service interface (planned: FPSF-SS-008); or
- message queue infrastructure setup and administration.
2.3 Naming Note
FPSF-SS-001 refers to this component as transfer-history. That name was chosen for clarity in the system overview context. This specification uses the component's proper name: Chain Event Indexer (CEI). The identifier transfer-history remains valid in any context where FPSF-SS-001 component names are used, and the two terms are equivalent.
2.4 Normative References
| Reference | Description |
|---|---|
| FPSF-SS-001 v1.0.0 | The Stablecoin Stack — foundational specification |
| ERC-20 | Token Standard — Ethereum Improvement Proposal 20 |
| ERC-2612 | Permit Extension for EIP-20 Signed Approvals |
| EIP-712 | Typed Structured Data Hashing and Signing |
| RFC 2119 | Key words for use in RFCs to Indicate Requirement Levels |
2.5 Conventions and Terminology
The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in RFC 2119.
All terms defined in FPSF-SS-001 Section 2.4 carry the same meaning here. Additional terms introduced in this document:
| Term | Definition |
|---|---|
| Chain Event Indexer (CEI) | The component specified by this document. Equivalent to transfer-history in FPSF-SS-001. |
| Block Cursor | The last block number successfully processed and persisted by the CEI. Used to resume after restart or failure. |
| Confirmation Depth | The minimum number of blocks that must follow a block before it is considered final and eligible for processing. Configurable. |
| Domain Separator Cache | An in-memory map from token contract address to EIP-712 domain separator, populated at startup and refreshed periodically. |
| Live Indexing | The continuous ingestion of new blocks from the tip of the chain. |
| Historical Query | An on-demand request to retrieve events from a specific block range, serviced via a dedicated message queue topic. |
| Event Record | A structured JSON object representing a single on-chain event, enriched with domain separator and indexing metadata. |
| Batch | A message queue message containing one or more Event Records grouped for efficient delivery. |
| Basic Data Service | The basic-data-server component defined in FPSF-SS-001, Section 5.5. The CEI queries it to discover supported tokens and the Settlement Contract address. |
3. Role Within the Stablecoin Stack
The CEI is a read-only observer of the blockchain. It has no authority to submit transactions and holds no user funds. Its sole responsibility is to collect events and make them available to downstream components through the message queue.
Within FPSF-SS-001's component model, the CEI occupies the Event Indexing subsystem (Section 5.4). Its primary consumers within a conformant stack deployment are:
- core-checkout-engine — consumes
PermittedTransferevent records to reconcile payment sessions; - balance-and-history — consumes all event records to maintain real-time wallet balance views and transfer histories; and
- wallet-gateway — relies on
balance-and-historyfor delivery to connected clients, not on the CEI directly.
The CEI is authoritative for the shape of all event record interfaces. Downstream components MUST NOT assume a schema other than the one defined in Section 8.
Blockchain Node
│
│ WebSocket (preferred) / HTTP polling (fallback)
▼
Chain Event Indexer (CEI)
│
│ Message Queue Topics (one per event type)
├──────────────────────────────────────────────────► core-checkout-engine
├──────────────────────────────────────────────────► balance-and-history
└──────────────────────────────────────────────────► (any other consumer)
4. Startup and Initialization
4.1 Token Discovery from the Basic Data Service
On startup, the CEI MUST query the Basic Data Service to retrieve:
- the list of currently supported ERC-2612-compliant token contract addresses; and
- the address of the deployed Settlement Contract on the configured network.
The CEI MUST NOT begin block ingestion until token discovery and on-chain validation (Section 4.2) have completed successfully.
If the Basic Data Service is unreachable at startup, the CEI MUST retry with exponential backoff (base 2 s, maximum 60 s, jitter ±20 %) and MUST NOT proceed with a stale or empty token list.
4.2 On-Chain Token Validation
For each token returned by the Basic Data Service, the CEI MUST perform the following on-chain checks before accepting the token as valid:
-
ERC-2612 conformance — call
nonces(address)on the token contract. If the call reverts, the token is not ERC-2612 compliant and initialization MUST be aborted (see Section 4.4). -
Domain separator retrieval — call
DOMAIN_SEPARATOR()on the token contract and record the returnedbytes32value. -
Domain separator cross-check — recompute the expected EIP-712 domain separator from the token's
name(),version()(if present, defaulting to"1"),chainId, andverifyingContractfields. The recomputed value MUST equal the value returned byDOMAIN_SEPARATOR(). A mismatch MUST cause initialization to be aborted (see Section 4.4).
All three checks MUST pass for every supported token before the CEI proceeds. Partial initialization — where some tokens pass and others fail — is not permitted.
4.3 Domain Separator Cache
After successful validation, the CEI MUST maintain an in-memory Domain Separator Cache mapping each token's contract address to its domain separator. This cache is used to enrich all event records emitted for that token with the domainSeparator field (see Section 8.1).
The cache MUST be treated as authoritative for the lifetime of the process unless a refresh is triggered (Section 4.5).
4.4 Initialization Failure and Abort Conditions
The CEI MUST abort initialization and MUST NOT begin block ingestion if any of the following conditions are detected:
- The Basic Data Service returns an empty token list.
- Any supported token fails the ERC-2612 conformance check.
- Any supported token's on-chain domain separator does not match the recomputed value.
- The Settlement Contract address cannot be resolved or is unreachable.
On abort, the CEI MUST emit a structured error log entry identifying the specific token and the nature of the failure, then exit with a non-zero exit code.
4.5 Periodic Refresh
The CEI SHOULD periodically re-query the Basic Data Service and re-validate on-chain state to detect changes to the supported token list. The refresh interval MUST be configurable (see Section 11, CEI_TOKEN_REFRESH_INTERVAL_SECONDS).
If a refresh reveals a new token, the CEI MUST perform the full validation procedure (Section 4.2) for that token before adding it to the cache and beginning to watch it.
If a refresh reveals that a previously supported token has been removed from the Basic Data Service, the CEI MUST cease watching it immediately and MUST emit a structured log entry recording the removal.
A refresh that encounters a domain separator mismatch for an already-active token MUST be treated as a critical error. The CEI MUST emit a structured error log and MUST stop processing for that token until the discrepancy is resolved by a human operator.
5. Network Scope
Each CEI instance watches exactly one EVM-compatible blockchain network. The target network is determined by the configured RPC endpoint and MUST be validated on startup by checking that the chainId returned by eth_chainId matches the value of CEI_CHAIN_ID.
Running multiple CEI instances to cover multiple networks is supported and expected. Each instance operates independently.
6. Blockchain Connectivity
6.1 WebSocket Provider (Primary)
The CEI MUST attempt to connect to the blockchain node via WebSocket (wss:// or ws://). WebSocket is the preferred transport because it supports event subscriptions (eth_subscribe), enabling low-latency notification of new blocks.
When connected via WebSocket, the CEI MUST subscribe to newHeads to receive notification of new block headers, then fetch full block data and logs as needed.
The CEI MUST implement reconnection logic for WebSocket failures with exponential backoff (base 1 s, maximum 60 s, jitter ±20 %). On reconnection, the CEI MUST resume from the persisted Block Cursor (Section 6.4) rather than from the chain tip, to avoid gaps.
6.2 HTTP Polling Fallback
If the WebSocket connection cannot be established or is not configured, the CEI MUST fall back to HTTP polling using eth_getLogs and eth_getBlockByNumber.
The polling interval MUST be configurable via CEI_POLL_INTERVAL_SECONDS. The CEI MUST also respect the maximum block range per RPC call, configurable via CEI_MAX_BLOCKS_PER_CALL, to avoid exceeding provider limits.
The CEI MUST log a warning when operating in polling mode and MUST attempt to re-establish a WebSocket connection periodically (interval configurable via CEI_WS_RETRY_INTERVAL_SECONDS).
6.3 Block Confirmation Depth
The CEI MUST NOT process a block until it has received at least CEI_CONFIRMATION_DEPTH additional blocks after it. This ensures that event records are only emitted for blocks that are unlikely to be reorganized out of the canonical chain.
The confirmation depth MUST be configurable and MUST default to a value appropriate for the target network. The RECOMMENDED default for EVM mainnets is 12 blocks.
The CEI MUST track the current chain tip (from newHeads or polling) and derive the highest eligible block as tip - CEI_CONFIRMATION_DEPTH.
6.4 Persistent Block Cursor
The CEI MUST persist the last successfully processed block number after completing ingestion and message queue delivery for that block. This value is the Block Cursor.
On startup, the CEI MUST read the persisted Block Cursor and resume ingestion from cursor + 1. If no cursor exists (first-time startup), the CEI MUST begin from the block number specified by CEI_START_BLOCK.
The Block Cursor MUST be written atomically (or with sufficient durability guarantees) to prevent data loss on unclean shutdown. Acceptable storage backends include PostgreSQL, Redis (with persistence enabled), or a local file with fsync.
If the persisted cursor represents a block that is no longer on the canonical chain (i.e., a reorg has occurred below the cursor), the CEI MUST detect this condition and MUST alert operators. Automatic reorg handling beyond the confirmation depth window is out of scope for this version.
7. Watched Events
The CEI watches two categories of on-chain events: events emitted by the Settlement Contract and standard Transfer events emitted by supported ERC-20 token contracts.
7.1 Settlement Contract Events
The CEI MUST watch and index all of the following events emitted by the Settlement Contract address obtained from the Basic Data Service:
| Event | Solidity Signature | Defined In |
|---|---|---|
PermittedTransfer | See FPSF-SS-001, Section 11 | FPSF-SS-001 |
AcquirerCreated | See FPSF-SS-001, Section 11 | FPSF-SS-001 |
CommissionGenerated | See FPSF-SS-001, Section 11 | FPSF-SS-001 |
AcquiringFeeUpdated | See FPSF-SS-001, Section 11 | FPSF-SS-001 |
Withdrawal | See FPSF-SS-001, Section 11 | FPSF-SS-001 |
7.2 ERC-20 Transfer Events
For each supported token, the CEI MUST watch and index the standard ERC-20 Transfer event:
event Transfer(address indexed from, address indexed to, uint256 value)
The token's domain separator MUST be included in every Erc20TransferRecord emitted (see Section 8.7), retrieved from the Domain Separator Cache.
The CEI MUST NOT watch Transfer events from token contracts that are not in the validated supported token list.
8. Event Record Schemas
This section is normative. The CEI is the authoritative source for all event record schemas. Downstream consumers MUST conform to these schemas. The CEI MUST NOT emit records that deviate from the schemas defined here.
All field values are JSON types. Numeric values that exceed safe JavaScript integer range (uint256, uint128) MUST be encoded as decimal strings.
8.1 Common Fields
Every event record, regardless of type, MUST include the following fields:
| Field | Type | Description |
|---|---|---|
recordType | string | Identifies the event type. One of the values defined in Sections 8.2–8.7. |
chainId | string | The decimal string representation of the network chain ID. |
blockNumber | number | The block number in which the event was emitted. |
blockTimestamp | number | Unix timestamp (seconds) of the block. |
transactionHash | string | 66-character hex string. The transaction that emitted this event. |
logIndex | number | The position of this log within the transaction. |
contractAddress | string | 42-character hex string. The contract that emitted the event. |
indexedAt | string | ISO 8601 UTC timestamp of when the CEI processed this record. |
8.2 PermittedTransferRecord
recordType: "PermittedTransfer"
Corresponds to the PermittedTransfer event defined in FPSF-SS-001, Section 11.
| Field | Type | Description |
|---|---|---|
| (common fields) | As defined in Section 8.1. | |
domainSeparator | string | 66-char hex. EIP-712 domain separator of the token involved. |
token | string | 42-char hex. Token contract address. |
payer | string | 42-char hex. Token owner whose permit authorised the transfer. |
recipient | string | 42-char hex. Beneficiary address. |
value | string | Decimal string. Principal amount credited to recipient, in token units. |
fee | string | Decimal string. Total fees deducted, in token units. |
acquirerId | string | 34-char hex (0x + 32 hex chars). The bytes16 Acquirer ID. |
orderReference | string | 66-char hex. 32-byte field: 16-byte Order Reference || 16-byte Acquirer ID. |
8.3 AcquirerCreatedRecord
recordType: "AcquirerCreated"
Corresponds to the AcquirerCreated event defined in FPSF-SS-001, Section 11.
| Field | Type | Description |
|---|---|---|
| (common fields) | As defined in Section 8.1. | |
acquirerId | string | 34-char hex (0x + 32 hex chars). The bytes16 Acquirer ID. |
wallet | string | 42-char hex. Registered Acquirer wallet address. |
feePercent | string | Decimal string. Configured acquiring fee percentage. |
8.4 CommissionGeneratedRecord
recordType: "CommissionGenerated"
Corresponds to the CommissionGenerated event defined in FPSF-SS-001, Section 11.
| Field | Type | Description |
|---|---|---|
| (common fields) | As defined in Section 8.1. | |
acquirerId | string | 34-char hex. The Acquirer ID that earned the commission. |
amount | string | Decimal string. Commission credited, in token units. |
8.5 AcquiringFeeUpdatedRecord
recordType: "AcquiringFeeUpdated"
Corresponds to the AcquiringFeeUpdated event defined in FPSF-SS-001, Section 11.
| Field | Type | Description |
|---|---|---|
| (common fields) | As defined in Section 8.1. | |
acquiring | string | 42-char hex. Acquirer wallet address whose fee was updated. |
feePercent | string | Decimal string. New acquiring fee percentage. |
8.6 WithdrawalRecord
recordType: "Withdrawal"
Corresponds to the Withdrawal event defined in FPSF-SS-001, Section 11.
| Field | Type | Description |
|---|---|---|
| (common fields) | As defined in Section 8.1. | |
owner | string | 42-char hex. Account whose internal balance was withdrawn. |
beneficiary | string | 42-char hex. Address that received the funds. |
amount | string | Decimal string. Amount withdrawn, in token units. |
8.7 Erc20TransferRecord
recordType: "Erc20Transfer"
Corresponds to the standard ERC-20 Transfer event.
| Field | Type | Description |
|---|---|---|
| (common fields) | As defined in Section 8.1. | |
domainSeparator | string | 66-char hex. EIP-712 domain separator of the token. From Domain Separator Cache. |
from | string | 42-char hex. Sender address. |
to | string | 42-char hex. Recipient address. |
value | string | Decimal string. Transfer amount in token units. |
9. Message Queue Distribution
9.1 Topic Layout
The CEI MUST publish event records to dedicated message queue topics. Each event type has its own topic to allow consumers to subscribe only to the events they require. The topic names MUST follow the naming convention defined below, where {prefix} is a configurable string (see CEI_TOPIC_PREFIX in Section 11).
| Event Type | Topic Name |
|---|---|
PermittedTransfer | {prefix}.permitted-transfer |
AcquirerCreated | {prefix}.acquirer-created |
CommissionGenerated | {prefix}.commission-generated |
AcquiringFeeUpdated | {prefix}.acquiring-fee-updated |
Withdrawal | {prefix}.withdrawal |
Erc20Transfer | {prefix}.erc20-transfer |
| Historical query responses | {prefix}.historical-query-response |
The CEI MUST create topics on startup if they do not already exist, subject to the message queue system's permissions model.
9.2 Message Envelope
Each message published to a topic MUST conform to the following envelope:
{
"envelopeVersion": "1",
"chainId": "<decimal string>",
"publishedAt": "<ISO 8601 UTC>",
"records": [ <EventRecord>, ... ]
}
| Field | Type | Description |
|---|---|---|
envelopeVersion | string | Always "1" for this specification version. |
chainId | string | Decimal string. Identifies the source network. |
publishedAt | string | ISO 8601 UTC. Time at which the CEI published this message. |
records | array | One or more Event Records (see Section 8). All records in a batch MUST be of the same recordType. |
9.3 Batch Packing
To reduce message queue overhead, the CEI MAY group multiple Event Records of the same type into a single message envelope. Records from different event types MUST NOT be mixed within a single message.
The maximum number of records per batch MUST be configurable via CEI_MAX_BATCH_SIZE. Records within a batch MUST be ordered by blockNumber ascending, then by logIndex ascending.
Batching is an optimization and MUST NOT delay publication beyond a configurable flush interval (CEI_BATCH_FLUSH_INTERVAL_MS). The CEI MUST publish any buffered records at the flush interval even if CEI_MAX_BATCH_SIZE has not been reached.
9.4 Delivery Guarantees
The CEI MUST guarantee at-least-once delivery for all event records. It MUST NOT advance the Block Cursor (Section 6.4) until all records for that block have been successfully acknowledged by the message queue broker.
The CEI MUST NOT guarantee exactly-once delivery. Downstream consumers MUST be designed to handle duplicate records, using transactionHash and logIndex as a composite idempotency key.
10. On-Demand Historical Query
The CEI MUST accept requests to retrieve and re-publish event records for a specific block range and token set. This is intended for backfill, recovery, and audit use cases.
10.1 Request Topic
The CEI MUST subscribe to the topic {prefix}.historical-query-request and process all messages received on it without disrupting the operations of the Live Indexing (section 2.4) .
10.2 Request Message Format
{
"requestId": "<UUIDv4>",
"fromBlock": 12345678,
"toBlock": 12345900,
"domainSeparators": ["<bytes32 hex>", ...],
"eventTypes": ["PermittedTransfer", "Erc20Transfer"],
"replyTo": "<optional override topic name>"
}
| Field | Type | Required | Description |
|---|---|---|---|
requestId | string (UUIDv4) | REQUIRED | Unique identifier for this query. Echoed in the response. |
fromBlock | number | REQUIRED | Start of the requested block range (inclusive). |
toBlock | number | REQUIRED | End of the requested block range (inclusive). |
domainSeparators | array of string | REQUIRED | One or more domain separators identifying the tokens of interest. MUST correspond to supported tokens. |
eventTypes | array of string | OPTIONAL | If provided, restricts results to the listed recordType values. If omitted, all event types are returned. |
replyTo | string | OPTIONAL | If provided, the CEI MUST publish the response to this topic instead of the default response topic. |
The CEI MUST reject requests where:
toBlock < fromBlock;- any
domainSeparatordoes not correspond to a currently supported token; or - any value in
eventTypesis not a validrecordType.
10.3 Response Topic
Unless overridden by the replyTo field, the CEI MUST publish the response to {prefix}.historical-query-response.
10.4 Response Message Format
The response is published as one or more messages, each conforming to the standard message envelope (Section 9.2), with the addition of the following fields in the envelope:
{
"envelopeVersion": "1",
"chainId": "<decimal string>",
"publishedAt": "<ISO 8601 UTC>",
"requestId": "<UUIDv4>",
"isLastBatch": true,
"records": [ ... ]
}
| Additional Field | Type | Description |
|---|---|---|
requestId | string | Echoes the requestId from the request. |
isLastBatch | boolean | true on the final message for this query. Allows consumers to know when all results have been received. |
The CEI MAY split large results across multiple messages. isLastBatch MUST be false on all messages except the last.
10.5 Error Responses
If the CEI cannot serve a historical query, it MUST publish a single error response to the response topic:
{
"envelopeVersion": "1",
"chainId": "<decimal string>",
"publishedAt": "<ISO 8601 UTC>",
"requestId": "<UUIDv4>",
"error": {
"code": "<error code>",
"message": "<human-readable description>"
}
}
| Error Code | Condition |
|---|---|
INVALID_RANGE | toBlock < fromBlock |
UNSUPPORTED_TOKEN | One or more domainSeparators do not match supported tokens |
UNKNOWN_EVENT_TYPE | One or more eventTypes are not valid recordType values |
RANGE_TOO_LARGE | The requested range exceeds CEI_HISTORICAL_QUERY_MAX_BLOCKS |
INTERNAL_ERROR | Unexpected failure during query execution |
11. Configuration Reference
All configuration MUST be supplied via environment variables. The CEI MUST fail to start if a REQUIRED variable is absent or invalid.
| Variable | Required | Default | Description |
|---|---|---|---|
CEI_CHAIN_ID | REQUIRED | — | Expected chain ID of the target network (decimal integer). Validated on startup against eth_chainId. |
CEI_RPC_WS_URL | OPTIONAL | — | WebSocket RPC endpoint (wss:// or ws://). Primary connectivity method. |
CEI_RPC_HTTP_URL | REQUIRED | — | HTTP RPC endpoint. Used as polling fallback when WebSocket is unavailable. |
CEI_BASIC_DATA_SERVICE_URL | REQUIRED | — | Base URL of the Basic Data Service instance. |
CEI_START_BLOCK | REQUIRED | — | Block number to start from when no persisted cursor exists. SHOULD be set to the Settlement Contract deployment block. |
CEI_CONFIRMATION_DEPTH | OPTIONAL | 12 | Number of blocks to wait after a block before processing it. |
CEI_POLL_INTERVAL_SECONDS | OPTIONAL | 5 | Block polling interval when operating in HTTP fallback mode. |
CEI_MAX_BLOCKS_PER_CALL | OPTIONAL | 500 | Maximum block range per eth_getLogs call. Adjust to respect RPC provider limits. |
CEI_WS_RETRY_INTERVAL_SECONDS | OPTIONAL | 30 | How often to reattempt WebSocket connection when in polling fallback mode. |
CEI_TOKEN_REFRESH_INTERVAL_SECONDS | OPTIONAL | 300 | How often to re-query the Basic Data Service for supported token changes. |
CEI_TOPIC_PREFIX | REQUIRED | — | Prefix prepended to all message queue topic names (e.g. stablecoin-stack.mainnet). |
CEI_MQ_BROKERS | REQUIRED | — | Comma-separated list of message queue broker addresses. |
CEI_MAX_BATCH_SIZE | OPTIONAL | 100 | Maximum number of records per message queue message. |
CEI_BATCH_FLUSH_INTERVAL_MS | OPTIONAL | 500 | Maximum time to buffer records before flushing a batch, in milliseconds. |
CEI_CURSOR_STORE_DSN | REQUIRED | — | Connection string for the Block Cursor persistent store (e.g. PostgreSQL DSN). |
CEI_HISTORICAL_QUERY_MAX_BLOCKS | OPTIONAL | 10000 | Maximum block range allowed in a single historical query request. |
CEI_LOG_LEVEL | OPTIONAL | info | Log verbosity. One of: debug, info, warn, error. |
12. Operational Requirements
12.1 Persistent State
The CEI MUST persist the Block Cursor to a durable store after each successfully processed and published block. The cursor write MUST be atomic with respect to the message queue acknowledgement: the CEI MUST NOT advance the cursor unless the message queue broker has acknowledged receipt of all records for that block.
The persistent store MUST be external to the CEI process (i.e. not in-process memory or local-only files without durability guarantees) to support restart and horizontal scaling of upstream dependencies. PostgreSQL is the RECOMMENDED store. Redis with AOF persistence is also acceptable.
12.2 Health and Observability
The CEI MUST expose an HTTP health check endpoint at GET /health. The endpoint MUST return HTTP 200 when the service is operating normally and HTTP 503 when it is in a degraded state (e.g. WebSocket disconnected and HTTP polling is failing, or message queue is unreachable).
The CEI SHOULD emit structured JSON logs for all significant events, including:
- startup and initialization completion;
- token validation results (per token);
- Block Cursor advance (at configurable verbosity);
- WebSocket connection events (connect, disconnect, reconnect);
- message queue publish errors and retries;
- historical query receipt and completion; and
- any warning or error condition.
The CEI SHOULD expose Prometheus-compatible metrics at GET /metrics, including at minimum:
| Metric | Type | Description |
|---|---|---|
cei_last_processed_block | Gauge | Block number of the most recently processed block. |
cei_chain_tip_block | Gauge | Most recently observed chain tip block number. |
cei_confirmation_lag_blocks | Gauge | Difference between chain tip and last processed block, minus confirmation depth. |
cei_records_published_total | Counter | Total records published, labelled by record_type. |
cei_mq_publish_errors_total | Counter | Total message queue publish errors. |
cei_rpc_errors_total | Counter | Total RPC call errors, labelled by transport (ws or http). |
cei_historical_queries_total | Counter | Total historical queries received. |
12.3 Graceful Shutdown
On receiving SIGTERM or SIGINT, the CEI MUST:
- stop accepting new historical query requests;
- complete in-flight block processing for the current block;
- flush any buffered batch records to the message queue;
- persist the Block Cursor; and
- close the message queue and RPC connections cleanly before exiting.
The CEI MUST complete graceful shutdown within a configurable timeout. If the timeout is exceeded, the process MAY exit uncleanly, relying on the persisted cursor for safe recovery on next start.
13. Security Considerations
Read-only access. The CEI only reads from the blockchain. It has no signing keys, no funded accounts, and cannot submit transactions. A CEI compromise does not directly result in loss of funds.
Basic Data Service integrity. The CEI trusts the Basic Data Service to supply accurate token and Settlement Contract addresses. Operators MUST ensure the Basic Data Service instance used by the CEI is the authoritative, operator-controlled instance and is served over TLS.
On-chain cross-check. The domain separator cross-check (Section 4.2) provides a cryptographic verification that the token metadata from the Basic Data Service corresponds to the actual deployed contract. This mitigates the risk of a misconfigured or tampered Basic Data Service directing the CEI to watch the wrong contracts.
RPC endpoint trust. The blockchain node RPC endpoint is a trusted data source. Operators MUST use a reliable, operator-controlled node or a reputable provider. A malicious or compromised RPC endpoint could return false event data. Operators SHOULD use at least two independent RPC sources for critical deployments (multi-provider verification is out of scope for this version, identified in Section 15).
Message queue access control. The CEI requires write access to the configured topics and read access to the historical query request topic. Operators MUST configure the message queue with appropriate access controls to ensure no unauthorized party can publish to CEI topics or inject historical query requests.
No sensitive data in records. Event records contain only data already publicly available on-chain. The CEI does not collect, store, or transmit any private keys, user credentials, or off-chain personal data.
14. Conformance
A conformant CEI implementation MUST:
- perform full token discovery and on-chain validation at startup (Section 4), and abort on any mismatch;
- maintain a Domain Separator Cache and include
domainSeparatorin all applicable event records; - watch exactly one blockchain network per instance (Section 5);
- prefer WebSocket connectivity and fall back to HTTP polling (Section 6);
- respect the configured confirmation depth and only process confirmed blocks (Section 6.3);
- persist the Block Cursor after each processed block and resume from it on restart (Section 6.4);
- collect and publish all event types defined in Section 7;
- emit event records that conform exactly to the schemas defined in Section 8;
- publish records to the topic layout defined in Section 9.1, using the message envelope defined in Section 9.2;
- support batch packing and respect the configured batch size and flush interval (Section 9.3);
- guarantee at-least-once delivery and not advance the cursor before broker acknowledgement (Section 9.4);
- implement the historical query interface defined in Section 10;
- support all configuration variables defined as REQUIRED in Section 11;
- expose the
/healthendpoint (Section 12.2); and - perform graceful shutdown as defined in Section 12.3.
15. Future Work
Multi-provider verification. Consuming and cross-checking logs from two or more independent RPC endpoints before publishing, to detect tampered or inconsistent node responses.
Chain reorganization handling. Automated detection and handling of reorgs deeper than the confirmation depth, including retraction of already-published records.
Per-token fee model support. When per-token fee configuration is introduced to the Settlement Contract (noted as future work in FPSF-SS-002, Section 8.2), the CEI SHOULD surface per-token fee parameters in applicable records.
Multi-network message aggregation. An optional aggregator layer that consolidates event streams from multiple CEI instances (one per network) into unified cross-network topics.
16. Change Log
| Version | Date | Author | Summary |
|---|---|---|---|
| 1.0.0 | 2026-03-30 | Adalton Reis | Initial release. Full specification of the Chain Event Indexer component, covering initialization, connectivity, event schemas, message queue distribution, historical query interface, configuration, and operational requirements. |
17. License
Copyright © 2026 Fabric Payment Standards Foundation. All rights reserved.
This specification is published under the Apache License, Version 2.0. Unless required by applicable law or agreed to in writing, material distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.