Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/bridge-fee.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ methodology:

findings: []

source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/bridge-fee
source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/bridge-monitor

prometheus:
url: https://prometheus-production-9ffe.up.railway.app
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/bridge-quote-latency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ methodology:

findings: []

source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/bridge-quote-latency
source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/bridge-monitor

prometheus:
url: https://prometheus-production-9ffe.up.railway.app
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/metadata-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ methodology:

findings: []

source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/metadata-coverage
source: https://github.com/OpenChainBench/OpenChainBench/tree/main/harnesses/aggregator-head-lag

prometheus:
url: https://prometheus-production-0859.up.railway.app
Expand Down
53 changes: 40 additions & 13 deletions harnesses/README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
# Harnesses

One harness folder per benchmark, one Go binary per folder, one Railway service per binary. The Mobula-hosted services emit metrics that the shared OpenChainBench Prometheus scrapes; the site queries that Prometheus.
Each subdirectory is a benchmark runner — a long-running process that calls providers, measures latency / cost / success, and exposes Prometheus metrics on `/metrics`. Anyone can clone, fork, host, or contribute one.

```
harnesses/
├── aggregator-head-lag/ Bench № 001 (Go, exposes :2112/metrics)
├── bridge-quote-latency/ Bench № 002 (Go, exposes :9090/metrics)
├── bridge-fee/ Bench № 003 (Go, exposes :9090/metrics)
└── metadata-coverage/ Bench № 004 (Go, exposes :2112/metrics)
├── aggregator-head-lag/ Bench № 001 — Go service, exposes :2112/metrics
└── bridge-monitor/ Bench № 002 + № 003 — Go service, exposes :9090/metrics
```

## Hosting model
A single harness can serve multiple benchmarks when the same set of measurements is consumed by more than one spec — `bridge-monitor` is the canonical example, producing both `bridge_quote_latency_ms` (read by `bridge-quote-latency.yml`) and `bridge_cost_percent` (read by `bridge-fee.yml`).

Federation, not centralization. Each harness is run by whoever owns it. The current four are hosted by Mobula on Railway as a sponsorship contribution; new contributors host their own and submit a scrape config so the central Prometheus picks them up.
## Hosting model — federation, not centralization

You never share API keys with the project. Your harness runs with your credentials, on your infra, on your dime; only the metric values it publishes are public.
OpenChainBench does **not** centralize harness execution. Each harness is hosted by whoever owns it:

- **Mobula** runs `aggregator-head-lag` and `bridge-monitor` on its own Railway infrastructure (sponsoring those two benchmarks).
- **Independent contributors** run their own harnesses on whatever infra they prefer (Railway, Fly, Cloud Run, a VPS, a home server with a static IP).
- **Providers that want to be benchmarked on their own service** can host the harness themselves and submit a scrape config — the data path is identical to externally-hosted benchmarks.

The only piece of infrastructure the project shares is a single Prometheus instance ([`/infrastructure/prometheus`](../infrastructure/prometheus)) that scrapes every harness's public `/metrics` endpoint and aggregates everything into one queryable URL. The site queries that URL.

This means you never share API keys with the project. You run your harness with your own credentials, on your own infra, on your own dime — the maintainers never see your secrets.

## Contract

Every harness, regardless of language, must satisfy this contract:
A harness is a **data producer**, nothing more. It must:

| Concern | Requirement |
| --- | --- |
| Inputs | Read API keys / wallet keys from environment variables, never commit them |
| Loop | Run continuously and update the same metric set every iteration |
| Metric names | Match the names referenced in the matching `benchmarks/<slug>.yml` exactly |
| Labels | Include `provider` (or equivalent) and `region` at minimum; chain/route labels encouraged |
| Endpoint | Expose `/metrics` over HTTPS on a publicly reachable URL |
| Timeouts | Documented; failures fail closed |
| Endpoint | Expose `/metrics` over HTTPS on a publicly reachable URL. The OpenChainBench Prometheus scrapes from the public internet — no VPN-only addresses |
| Timeouts | Documented; failures fail closed (counted toward success rate, excluded from latency aggregates) |
| Reproducibility | README explains how to run locally with one command |
| License | MIT, same as the rest of the repo |

A harness does not ship its own Prometheus, Grafana, or Alertmanager. The shared infrastructure handles that.
A harness does **not** ship its own Prometheus, Grafana, Alertmanager, or `docker-compose.yml`. Hosting choice is the contributor's, but the data plane is shared.

## Subdirectory layout

Expand All @@ -45,4 +51,25 @@ harnesses/<slug>/

## Submitting a new harness

See [`/CONTRIBUTING.md`](../CONTRIBUTING.md) for the full submission flow.
See [`/CONTRIBUTING.md`](../CONTRIBUTING.md) for the full submission flow. Short version:

1. Open an issue with the [📊 Propose a benchmark template](https://github.com/OpenChainBench/OpenChainBench/issues/new?template=new-benchmark.yml).
2. Build the harness here at `harnesses/<slug>/`.
3. Deploy it on whatever infra you prefer, expose `/metrics` over HTTPS at a stable URL.
4. Append a scrape job to [`infrastructure/prometheus/prometheus.yml`](../infrastructure/prometheus/prometheus.yml) pointing at your URL.
5. Write the spec at `benchmarks/<slug>.yml`.
6. Open a PR.

Once merged, a maintainer redeploys the central Prometheus to apply the new scrape job — your benchmark appears on the site within 60 seconds (next ISR cycle).

## Hosting tips for contributors

A few options for hosting a harness, ranked roughly from "trivial setup" to "more control":

- **Railway** (via this repo, root `harnesses/<slug>/`) — same workflow as how Mobula hosts its harnesses. Free tier covers most light harnesses.
- **Fly.io** — `fly deploy` in any folder with a Dockerfile. Generous free tier.
- **Google Cloud Run** — pay-per-use, scales to zero.
- **VPS (Hetzner, DigitalOcean, OVH)** — €5/mo, full control, run a `systemd` service exposing a port.
- **Your laptop with ngrok** — fine for short-lived experiments, not for live benchmarks.

Whatever you pick, the OpenChainBench Prometheus only needs `https://<your-host>/metrics` to be reachable.
68 changes: 50 additions & 18 deletions harnesses/aggregator-head-lag/README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
# Harness · aggregator-head-lag

> Real-time monitor that produces the `head_lag_seconds` metric consumed by [`benchmarks/aggregator-head-lag.yml`](../../benchmarks/aggregator-head-lag.yml). One Go binary, one Railway service, one benchmark.
> Real-time monitor that produces the `head_lag_seconds` metric consumed by [`benchmarks/aggregator-head-lag.yml`](../../benchmarks/aggregator-head-lag.yml).

**Bench**: [№ 001 · Aggregator Head Lag](../../benchmarks/aggregator-head-lag.yml)

## How it works

Connects to each aggregator's WebSocket / REST feed and measures latency by comparing:
The monitor connects to each aggregator's WebSocket / REST feed and measures latency by comparing:

- The on-chain timestamp of a trade event (from the event payload)
- The wall-clock time at which the aggregator emitted the event

The delta is exported as a Prometheus gauge (`head_lag_seconds`), labelled by aggregator, chain and region. Failures surface as `head_lag_errors_total` counters.
The delta is exported as a Prometheus gauge (`head_lag_seconds`), labelled by aggregator, chain and region. Failures surface as `head_lag_errors_total` counters. REST API latency, quote API latency and metadata coverage are recorded in parallel from the same process.

**Tracked aggregators**: GeckoTerminal · Mobula · Codex
**Supported chains**: Solana · Ethereum · BNB Chain · Base

## Where the data goes

This harness is a data producer only. It exposes `/metrics` on port `2112`. The shared OpenChainBench Prometheus (see [`/infrastructure/prometheus`](../../infrastructure/prometheus)) scrapes that endpoint:
This harness is a **data producer only** — it exposes `/metrics` on port `2112`. The shared OpenChainBench Prometheus (see [`/infrastructure/prometheus`](../../infrastructure/prometheus)) scrapes that endpoint over Railway's internal DNS:

```
aggregator-head-lag.railway.internal:2112 ──► prometheus.railway.internal ──► public site
```

A contributor running locally just needs `/metrics` reachable from their Prom scraper — nothing else.

## Metrics produced

```
Expand All @@ -35,6 +37,9 @@ mobula_processing_lag_seconds{...} gauge
mobula_network_lag_seconds{...} gauge
aggregator_head_block{aggregator, chain, region} gauge
blockchain_head_block{chain, region} gauge
rest_api_latency_milliseconds_{bucket,sum,count}{...} histogram
quote_api_latency_milliseconds_{bucket,sum,count}{...} histogram
metadata_coverage_total / metadata_coverage_success{...} counter
```

## Run locally
Expand All @@ -43,13 +48,13 @@ Prerequisites: Go 1.24+, API keys for the aggregators you want to track.

```bash
cp .env.example .env
# Fill in MOBULA_API_KEY, COINGECKO_API_KEY, DEFINED_SESSION_COOKIE
# Fill in the keys (any missing key disables that aggregator's monitor)
go run ./cmd/script/
```

`/metrics` will be exposed on `http://localhost:2112/metrics`.
`/metrics` will be exposed on `http://localhost:2112/metrics`. To exercise the OpenChainBench site against your local data, point the YAML spec's `prom_url` at a local Prometheus that scrapes this endpoint (the `infrastructure/prometheus` config has notes on how to do this).

Or via Docker:
Or run via Docker:

```bash
docker build -t aggregator-head-lag .
Expand All @@ -58,7 +63,7 @@ docker run --rm --env-file .env -p 2112:2112 aggregator-head-lag

## Run on Railway

This service is deployed from the OpenChainBench repo, root directory `harnesses/aggregator-head-lag/`. Set the env vars listed below and the shared Prometheus picks it up via DNS.
This service is deployed from the OpenChainBench repo, root directory `harnesses/aggregator-head-lag/`. Set the env vars listed below, and the shared Prometheus will pick it up via DNS automatically.

## Environment variables

Expand All @@ -70,21 +75,48 @@ This service is deployed from the OpenChainBench repo, root directory `harnesses
| `MOBULA_WS_URL` | Override the Mobula WS endpoint | optional |
| `MONITOR_REGION` | Label written on every metric (e.g. `us-east`) | recommended |

A monitor with no key for a given aggregator is skipped cleanly.
A monitor with no key for a given aggregator is skipped cleanly — the harness will run for whatever providers it can authenticate with.

## Project layout

```
cmd/script/ Single Go binary
├── main.go Boots the head-lag goroutine + metrics server
├── config.go Env-var loader
├── metrics.go Prometheus metric definitions
├── head_lag_monitor.go WebSocket monitor: on-chain ↔ feed timestamp delta
├── mobula_fast_trade_monitor.go Mobula-specific lag breakdown
├── proxy.go HTTP transport + retries
└── log_buffer.go In-memory ring buffer
cmd/script/ Single Go binary — all monitors + Prometheus exporter
├── main.go Entrypoint, supervises goroutines per aggregator
├── config.go Env-var loader
├── metrics.go Prometheus metric definitions + HTTP /metrics handler
├── head_lag_monitor.go WebSocket monitor: on-chain ↔ feed timestamp delta
├── mobula_*.go Mobula REST + WS monitors
├── codex_*.go Codex REST monitor
├── geckoterminal_monitor.go
├── quote_api_monitor.go Mobula swap quoting latency
├── metadata_coverage_monitor.go
├── proxy.go HTTP transport + retries
└── log_buffer.go In-memory ring buffer (last N log lines)

Dockerfile Multi-stage Go build
.env.example Documented env vars
```

## Adding an aggregator

1. Create `cmd/script/<aggregator>_monitor.go` mirroring an existing file (e.g. `geckoterminal_monitor.go`).
2. Implement the WebSocket / REST loop and call the appropriate `metrics.go` recorder.
3. Add the API key field to `Config` in `config.go` and to `.env.example`.
4. Start the monitor goroutine in `main.go`.
5. Update the YAML spec at `benchmarks/aggregator-head-lag.yml` to reference the new aggregator under `providers:` and add its query under `prometheus.providers.<slug>`.

## Troubleshooting

```bash
# Are metrics being produced?
curl http://localhost:2112/metrics | grep head_lag_seconds

# Tail logs (Docker)
docker logs -f <container>
```

If a specific aggregator silently emits nothing: most often the API key is missing or rate-limited. Check the logs for `[AGGREGATOR][skip]` lines.

## License

MIT, same as the rest of OpenChainBench.
MIT same as the rest of OpenChainBench.
43 changes: 38 additions & 5 deletions harnesses/aggregator-head-lag/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,50 @@ func main() {
}
}()

// Head lag monitor — the only data producer for this binary.
// Measures the gap between an on-chain Swap event and its appearance
// on each aggregator's WebSocket / REST feed.
// Mobula Pulse V2 feeder — only feeds the metadata coverage queue,
// no pulse-specific metrics emitted (see mobula_pulse_monitor.go).
wg.Add(1)
go func() {
defer wg.Done()
runMobulaPulseMonitor(config, stopChan)
}()

// Mobula REST API monitor
wg.Add(1)
go func() {
defer wg.Done()
runMobulaRESTMonitor(config, stopChan)
}()

// Codex REST API monitor
wg.Add(1)
go func() {
defer wg.Done()
runCodexRESTMonitor(config, stopChan)
}()

// Quote API latency monitor (Jupiter, Li.Fi, 1inch, KyberSwap)
wg.Add(1)
go func() {
defer wg.Done()
runQuoteAPIMonitor(config, stopChan)
}()

// Metadata coverage monitor (Mobula vs Codex)
wg.Add(1)
go func() {
defer wg.Done()
runMetadataCoverageMonitor(config, stopChan)
}()

// Head lag monitor (blockchain head vs aggregator indexed head)
wg.Add(1)
go func() {
defer wg.Done()
runHeadLagMonitor(config, stopChan)
}()

// Mobula Fast-Trade monitor — used for the detailed Mobula breakdown
// (processing vs network lag) emitted alongside head lag.
// Mobula Fast-Trade monitor (for comparison with Pulse V2)
wg.Add(1)
go func() {
defer wg.Done()
Expand Down
48 changes: 0 additions & 48 deletions harnesses/bridge-fee/README.md

This file was deleted.

File renamed without changes.
File renamed without changes.
Loading
Loading