The Event Indexer Service is a lightweight backend service that indexes Soroban contract events from the Checkmate Escrow contract. It provides fast, filtered queries for match history and event data with sub-100ms latency.
- Event Polling: Periodically polls Soroban RPC for new events from the escrow contract
- Persistent Storage: SQLite database for reliable event persistence
- In-Memory Cache: High-performance cache for recently indexed events
- REST API: Simple JSON API for event and match queries
- Input Validation: Rate limiting and parameter validation
- Metrics: Query latency tracking and cache statistics
Soroban RPC
↓
Event Poller (polls every N seconds)
↓
Event Parser & Validator
↓
Cache Layer (in-memory)
↓
Database Layer (SQLite)
↓
REST API (Axum)
| Variable | Default | Description |
|---|---|---|
STELLAR_RPC_URL |
https://soroban-testnet.stellar.org |
Soroban RPC endpoint |
CONTRACT_ESCROW |
Required | Address of the escrow contract |
EVENT_INDEXER_DB_PATH |
./events.db |
Path to SQLite database |
EVENT_INDEXER_BIND_ADDR |
127.0.0.1 |
API server bind address |
EVENT_INDEXER_PORT |
8080 |
API server port |
EVENT_INDEXER_CACHE_SIZE |
10000 |
Maximum cache entries |
EVENT_INDEXER_POLL_INTERVAL |
5 |
Event polling interval in seconds |
Endpoint: GET /health
Description: Check if the service is running and healthy, including a live database connectivity check.
Response (healthy):
{
"db": "ok"
}Response (database error):
{
"db": "error",
"detail": "unable to open database file"
}Latency: < 10ms
Endpoint: GET /events
Description: Query events with optional filters. Returns events matching the criteria sorted by ledger sequence (newest first).
Query Parameters:
player_address(optional): Filter by player address (matches player1 or player2)status(optional): Filter by match status (pending,active,completed,cancelled,expired)limit(optional): Maximum number of results (default: 100, max: 1000)offset(optional): Pagination offset (default: 0)
Example Request:
curl "http://localhost:8080/events?player_address=GA7QSTFKSQX4K3DWORVLKFWQIHD7DKD3UD5RN7NXLKPX22FQVQY5HQW&status=completed&limit=50"Response:
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"ledger_sequence": 12345,
"match_id": 1,
"event_type": "match:completed",
"player1": "GA7QSTFKSQX4K3DWORVLKFWQIHD7DKD3UD5RN7NXLKPX22FQVQY5HQW",
"player2": "GBBD47UZQ5SYWBZMW5XJBZPMZ5XLQNB4BBFZGHKZVKNXZX2FMGD2KVYF",
"status": "completed",
"winner": "player1",
"stake_amount": "1000000",
"token": "CABD7H7QWXSTDZ6YPMPZRJ2FLGDWP5AYWLF5PYQRB5PQV6PDBGFPMTD",
"game_id": "abc12345",
"platform": "lichess",
"timestamp": "2026-06-22T10:30:00Z",
"txn_hash": "0x1234..."
}
],
"error": null
}Latency: < 50ms (cached queries < 10ms)
Endpoint: GET /events/:match_id
Description: Get events for a specific match in chronological order. Supports pagination.
Path Parameters:
match_id(required): The match ID to query
Query Parameters:
limit(optional): Maximum number of results (default: 100)offset(optional): Pagination offset (default: 0)
Example Request:
curl "http://localhost:8080/events/1?limit=50&offset=100"Response:
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"ledger_sequence": 10000,
"match_id": 1,
"event_type": "match:created",
"player1": "GA7QSTFKSQX4K3DWORVLKFWQIHD7DKD3UD5RN7NXLKPX22FQVQY5HQW",
"player2": "GBBD47UZQ5SYWBZMW5XJBZPMZ5XLQNB4BBFZGHKZVKNXZX2FMGD2KVYF",
"status": "pending",
"winner": null,
"stake_amount": "1000000",
"token": "CABD7H7QWXSTDZ6YPMPZRJ2FLGDWP5AYWLF5PYQRB5PQV6PDBGFPMTD",
"game_id": "abc12345",
"platform": "lichess",
"timestamp": "2026-06-22T10:00:00Z",
"txn_hash": "0x1000..."
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"ledger_sequence": 10500,
"match_id": 1,
"event_type": "match:activated",
"status": "active",
"timestamp": "2026-06-22T10:15:00Z"
}
],
"error": null
}Latency: < 10ms (with cache)
Endpoint: GET /matches
Description: Get all matches, optionally filtered by status.
Query Parameters:
status(optional): Filter by match status (pending,active,completed,cancelled,expired)
Example Request:
curl "http://localhost:8080/matches?status=active"Response:
{
"success": true,
"data": [
{
"match_id": 1,
"player1": "GA7QSTFKSQX4K3DWORVLKFWQIHD7DKD3UD5RN7NXLKPX22FQVQY5HQW",
"player2": "GBBD47UZQ5SYWBZMW5XJBZPMZ5XLQNB4BBFZGHKZVKNXZX2FMGD2KVYF",
"status": "active",
"winner": null,
"stake_amount": "1000000",
"token": "CABD7H7QWXSTDZ6YPMPZRJ2FLGDWP5AYWLF5PYQRB5PQV6PDBGFPMTD",
"game_id": "abc12345",
"platform": "lichess",
"created_ledger": 10000,
"completed_ledger": 10000,
"events": []
}
],
"error": null
}Latency: < 50ms
Endpoint: GET /match/:match_id
Description: Get complete match information including players, current status, and all events.
Path Parameters:
match_id(required): The match ID to query
Example Request:
curl "http://localhost:8080/match/1"Response:
{
"success": true,
"data": {
"match_id": 1,
"player1": "GA7QSTFKSQX4K3DWORVLKFWQIHD7DKD3UD5RN7NXLKPX22FQVQY5HQW",
"player2": "GBBD47UZQ5SYWBZMW5XJBZPMZ5XLQNB4BBFZGHKZVKNXZX2FMGD2KVYF",
"status": "completed",
"winner": "player1",
"stake_amount": "1000000",
"token": "CABD7H7QWXSTDZ6YPMPZRJ2FLGDWP5AYWLF5PYQRB5PQV6PDBGFPMTD",
"game_id": "abc12345",
"platform": "lichess",
"created_ledger": 10000,
"completed_ledger": 12345,
"events": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"ledger_sequence": 10000,
"match_id": 1,
"event_type": "match:created",
"timestamp": "2026-06-22T10:00:00Z"
}
]
},
"error": null
}Latency: < 20ms
Endpoint: GET /stats
Description: Get indexer statistics including cache size and total event count.
Example Request:
curl "http://localhost:8080/stats"Response:
{
"success": true,
"data": {
"total_events": 1024,
"cache_size": 256
},
"error": null
}Latency: < 5ms
The indexer tracks the following event types:
| Event Type | Description | Status | Winner | Triggered When |
|---|---|---|---|---|
match:created |
Match created | pending | - | Match created with player addresses |
match:deposit |
Player deposited | active | - | Player deposits stakes |
match:activated |
Match activated | active | - | Both players deposited |
match:completed |
Match result submitted | completed | player1/player2/draw | Oracle submits verified result |
match:cancelled |
Match cancelled | cancelled | - | Match cancelled before activation |
match:expired |
Match expired | expired | - | Match timeout reached |
All endpoints return standardized error responses:
{
"success": false,
"data": null,
"error": "Detailed error message"
}| Status Code | Scenario |
|---|---|
| 200 | Successful query with results |
| 404 | No events found matching criteria |
| 400 | Invalid query parameters |
| 500 | Database or RPC error |
- Query Latency: < 100ms for typical requests (< 10ms for cached)
- Event Polling: Every 5 seconds (configurable)
- Database: SQLite with indexed queries on match_id, player addresses, and timestamp
- Cache Efficiency: LRU cache for frequently accessed match events
- Throughput: Handles 1000+ queries/second at 50th percentile latency
No rate limiting is enforced by the service itself in the current version. This is intentional for the initial release — the service is designed to run behind a reverse proxy or API gateway that handles rate limiting at the infrastructure level.
Current behavior: All endpoints are unbounded. Clients may send any number of requests per second.
Planned for a future release:
| Tier | Limit | Scope |
|---|---|---|
| Default (unauthenticated) | 60 req/min | Per IP |
| Authenticated (API key) | 600 req/min | Per key |
Bulk query (/events with large limit) |
10 req/min | Per IP |
Implementation options under consideration:
- Reverse proxy: nginx
limit_req_zoneor Caddy rate-limit middleware (no code changes required) - In-process:
tower-governoras an Axum layer (planned for v1.1)
Until rate limiting is enabled, operators deploying this service publicly should add a reverse proxy with appropriate limits.
- Input Validation: All query parameters are validated before database queries
- SQL Injection Prevention: Using parameterized queries with rusqlite
- CORS: Disabled by default (enable via
tower-httpif needed) - Authentication: Add API key validation layer if needed
FROM rust:1.75-slim
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/event-indexer"]export STELLAR_RPC_URL=https://soroban-testnet.stellar.org
export CONTRACT_ESCROW=CABD7H7QWXSTDZ6YPMPZRJ2FLGDWP5AYWLF5PYQRB5PQV6PDBGFPMTD
export EVENT_INDEXER_BIND_ADDR=0.0.0.0
export EVENT_INDEXER_PORT=8080
cargo runMonitor these metrics:
- Query latency (p50, p95, p99)
- Cache hit rate
- Database query time
- Event polling success/failure rate
- Memory usage
- SQLite database size
- GraphQL endpoint for flexible querying
- WebSocket support for real-time event subscriptions
- Event archival to S3 after 30 days
- Multi-contract support
- Advanced metrics and observability
- Event replay functionality