Stablecoin Whale Monitor
Repository
Documentation

Documentation

Architecture and security

How the system collects, stores, and serves transfers, and the security controls protecting it.

This page describes the v3 design implemented in the application and Terraform configuration. Earlier designs are in Version history.

Overview

An indexer reads Ethereum and writes qualifying transfers to Cloud Storage. A read-only API loads those transfers into memory and answers queries. A static website and the API share one HTTPS load balancer, so the browser talks to a single origin.

Compute and service boundaries

The indexer runs continuously in a Cloud Run worker pool because collecting transfers is background work. It polls Ethereum even when nobody has the website open. The pool is configured for one instance while monitoring is enabled, giving the checkpoint a single writer.

The API runs as a separate Cloud Run service with its own CPU, memory, request concurrency, timeout, and instance limits. Its CPU remains allocated between requests so it can refresh its index on a timer. Each instance builds its own index from storage. API capacity can therefore change independently of the indexer.

Both workloads use the same container image with different startup commands and service accounts. The website is a Next.js static export served from Cloud Storage through Cloud CDN. This keeps page delivery independent of transfer collection and API availability.

Data flow

Ingest
Ethereum JSON-RPCMainnet node provider. URL kept in Secret Manager
IndexerCloud Run worker pool, one continuous instance. Polls every 30 s, 6 blocks behind the tip
Events bucketPrivate Cloud Storage. One NDJSON object per block range with qualifying transfers
State bucketSeparate private bucket holding the checkpoint: last processed block and its hash

Serve
Events bucketRead only by the API
APICloud Run. In-memory index, deduplicated, newest first, pruned to 7 days
HTTPS load balancerCloud Armor rate limit on the API. TLS 1.2 minimum. Static site from a private bucket behind Cloud CDN
BrowserOne origin for the site and the API

Alerting · implemented, disabled in this deployment
Storage eventNew object in the events bucket
Pub/Sub topicDead-letter topic for failed deliveries
AlerterPrivate Cloud Run service with its own threshold
EmailOperator inbox
Solid boxes show the monitoring path. Dashed boxes mark external components or the disabled alerting path.

1. Reading the chain

The indexer runs as a single continuous instance on a Cloud Run worker pool. Every 30 seconds it asks the JSON-RPC node for the newest block and reads only up to 6 blocks behind it, about 72 seconds of confirmation. It requests Transfer logs from the USDC and USDT contracts in small block ranges, decodes them, and keeps transfers of 100,000 tokens or more. Each log's block hash is checked against the block it claims to come from.

2. Writing ranges, then the checkpoint

Each processed block range that contains qualifying transfers becomes one NDJSON object, one transfer per line, named from the range itself, for example ethereum/ranges/000026248680-000026248684.json. The same range always uses the same object name. Ranges with nothing to record write no object.

Range objects are written with a Cloud Storage precondition that only allows creation, so writing a range that already exists does not create a second copy. Only after the range is stored does the indexer advance the checkpoint in a separate state bucket, using a generation precondition so two writers cannot both move it. If the indexer stops between those two steps, it reprocesses the same range on restart and the write is a no-op.

For example, if blocks 100 through 104 were stored before the process stopped, but the checkpoint still says 99, the next cycle starts at 100 again. The existing range object is preserved, and the checkpoint can advance after the range is processed. If another writer changes the checkpoint first, the generation check rejects the conflicting update.

Transfer data, processing progress, and infrastructure state have separate storage responsibilities:

StoreContentsPurpose
Events bucketTransfer records grouped by block rangeDurable input for rebuilding the API index
Application-state bucketCheckpoint and token metadataTracks where indexing should resume
Terraform state bucketTerraform resource stateTracks infrastructure managed by the operator

The API applies the 7-day history window using each transfer's block time. A separate bucket lifecycle rule deletes event objects according to their object age. Terraform state is bootstrapped separately with private access and object versioning so earlier state generations can be recovered.

3. Serving from memory

The API keeps every retained transfer in memory. Every 30 seconds it lists new range objects, loads them, removes duplicates by chain, transaction hash and log index, sorts newest first and drops anything older than 7 days. Filtering and pagination operate on that in-memory index.

Requests also check index age and can trigger a refresh if the timer has fallen behind. Concurrent refresh attempts share the same refresh operation. Stored records are validated before they are accepted into the index, and a failed refresh leaves the previous index available within the configured staleness window.

4. One origin for site and API

The website is a static export stored in a private bucket and cached by Cloud CDN. A global HTTPS load balancer provides the public entry point. The URL map selects a backend according to the request path:

RequestDestinationCaching
/api and /api/*Cloud Run API through a serverless network endpoint groupAPI responses use Cache-Control: no-store
/_next/static/*Static assets in the website bucketA separate CDN backend uses the asset cache policy
Other pathsWebsite pages in the same bucketThe HTML backend uses a shorter cache lifetime

The deployment sets a one-year immutable cache header on versioned static assets and a 60-second header on HTML. Separate CDN policies let pages update more frequently while browsers reuse unchanged JavaScript and CSS. Relative API URLs keep browser requests on the website's origin.

Reorganizations

The 6-block buffer reduces exposure to short chain reorganizations. Before each cycle, the indexer checks that the block in its checkpoint still has the same hash on the chain. A mismatch rejects the cycle and stops checkpoint advancement until the conflict is resolved. The buffer is not a finality guarantee, and records already published are not rewritten after a deeper reorganization.

Security

The public API provides read-only access. Security controls cover network access, workload permissions, secrets, and deployment.

Network access and rate limiting

Certificate Manager provides the managed certificate for the public domain. The load balancer redirects HTTP requests to HTTPS and uses a modern TLS policy with a minimum version of TLS 1.2.

The API allows unauthenticated invocation so visitors can read transfers. Its Cloud Run ingress setting accepts internal and load-balancer traffic, keeping direct external requests to the service URL outside the public access path. Public API traffic passes through the load balancer's Cloud Armor policy, configured for 60 requests per minute per IP with HTTP 429 responses and a 60-second ban when the threshold is exceeded.

Identity and permissions

The indexer and API use separate service accounts because they perform different jobs. IAM grants are scoped to the buckets and secret each workload needs:

IdentityConfigured accessPurpose
IndexerObject Creator on events, Object User on application state, Secret Accessor on the RPC secretCreates transfer batches, updates progress, and connects to Ethereum
APIObject Viewer on events and application stateReads transfers and reports checkpoint progress
CDN cache-fill service agentObject Viewer on the website bucketReads private site objects to serve through the CDN

Create-only access lets the indexer add event objects while preserving existing batches. The API's read permissions let it rebuild its index and inspect progress without changing stored records. Google Cloud client libraries use the service account attached to each workload for authentication.

Secrets and private storage

The Ethereum RPC URL is stored in Secret Manager. Cloud Run supplies it to the indexer through a secret-backed environment variable, and the indexer identity has access to that specific secret. The API reads stored transfers, so its configured permissions require no RPC credential.

The website, events, application-state, and release-artifact buckets enforce public access prevention and uniform bucket-level access. IAM controls access at the bucket level. Visitors receive site files through the CDN service agent's access and transfer data through the API's access.

Application safeguards

The API validates token symbols, addresses, transaction hashes, decimal amounts, page sizes, and cursors before running a query. Pages are limited to 50 records, and cursors have a bounded length. Token amounts are compared in integer base units to preserve decimal precision.

Responses include X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and Cache-Control: no-store. Unexpected server errors return a generic message. The shared container image installs production dependencies and runs both workloads as the non-root node user.

Build and deployment

Cloud Build separates pull-request verification, artifact creation, and deployment into three triggers with distinct service accounts. This separates the access used to check proposed changes from the access used to publish artifacts and update services.

PipelineTrigger and workConfigured permissions
VerificationPull requests to main run tests, frontend checks, and Terraform validationWrites build logs
CIPushes to main build the container image, static website, and release manifestWrites logs, publishes images to Artifact Registry, and creates and reads release objects
CDRelease tags select a published release and wait for approval before deploymentReads images and releases, updates the API and worker pool, acts as their runtime accounts, and manages website objects

CI publishes each release under its commit and a unique build identifier. The manifest records the commit, container image digest, and website archive checksum. Before deployment, CD checks the commit and checksum against the downloaded release and confirms that the digest-qualified image exists. It deploys that image to both workloads and uploads the matching website archive.

Terraform manages IAM, storage, routing, service configuration, and monitoring. CD updates the deployed images, which Terraform explicitly leaves under delivery ownership. The pipeline identities are granted no Terraform state access by these modules; infrastructure changes follow a separate operator-reviewed Terraform workflow. After deployment, the pipeline checks image digests and runs the smoke-test script. The operations guide covers deployment and rollback procedures.

Monitoring and failure handling

API availability and indexing progress are tracked separately. The health response includes the last successful index refresh, the last processed block, checkpoint age, and refresh or checkpoint-read errors. This helps distinguish an API that can answer requests from an indexer that is advancing through Ethereum.

SignalWhat it checks
HTTPS uptime checkCalls the public health endpoint, validates TLS, and checks for an HTTP 200 response containing the expected chain ID
API errors and latencyUses load-balancer metrics for 5xx responses and p99 request latency on the API backend
Degraded API healthUses structured health logs to detect persistent refresh or checkpoint-read errors
Worker availabilityChecks the indexer worker pool's active instance count
Indexer progressUses committed-range and completed-cycle logs to detect a stalled processing loop, including cycles with no qualifying transfers

If a refresh fails, the API can keep serving its last successful index within the configured staleness window and reports degraded health. If the index has never loaded or its age reaches 10 minutes, transfer queries and the health endpoint return HTTP 503. Checkpoint age remains a separate signal because a successful storage refresh can still read data from an indexer that has stopped advancing.

Cloud Monitoring policies send notifications to the configured operator email after their failure windows are met. During planned maintenance, Terraform scales the worker pool to zero, lowers the API minimum instance count to zero, removes public API routes, and disables API and indexer alert policies. The static website shows maintenance information. On resumption, indexing starts from the saved checkpoint.

Alerting

Disabled in the current deployment. The alerting code is in the repository, but its infrastructure is switched off. Visitors cannot sign up for alerts.

When enabled, alerting works like this:

  1. A new range object in the events bucket triggers a storage notification to a Pub/Sub topic.
  2. Pub/Sub delivers the message with an authenticated push to a private alerter service on Cloud Run.
  3. The alerter reads the range and sends an email for each transfer at or above its own threshold, set in deployment configuration. The example configuration uses 1,000,000 tokens.
  4. Messages that keep failing move to a dead-letter topic for inspection instead of retrying forever.

The threshold and recipients come from deployment configuration. There is no user-facing setup.

Found a problem in this page? Open an issue in the repository.