Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
95c394d
feat(sample-health): wire UI badge + API surface + insufficient silen…
Flotapponnier Jun 26, 2026
8e06f59
feat(seo): /benchmarks/category/[cat] hub pages + BenchmarkGrid locke…
Flotapponnier Jun 26, 2026
c4d818b
feat(perp-funding-stability): scale to 15 venues + add 8 logos (bench…
Flotapponnier Jun 26, 2026
6143a9c
fix(perp-funding-stability): trim methodology[3] under 500 chars
Flotapponnier Jun 26, 2026
1f7fc48
fix(types): re-add venue dimension to BenchmarkFilters + spec schema
Flotapponnier Jun 26, 2026
61f1fa9
fix(main): restore src/lib/categories.ts + binance.svg mapping
Flotapponnier Jun 26, 2026
0c263ca
Merge pull request #746 from ChainBench/chore/cherry-funding-15v-v2
Flotapponnier Jun 26, 2026
d326616
hotfix(main): restore categories.ts + add venue dimension type
Jun 26, 2026
798786f
Merge pull request #748 from ChainBench/hotfix/missing-categories-lib
Flotapponnier Jun 26, 2026
5fbabcb
feat(badges): public catalog page for embedding live rankings (#744)
Flotapponnier Jun 26, 2026
9255ee6
feat(packages): python client (openchainbench) for PyPI (#745)
Flotapponnier Jun 26, 2026
ed3daf9
fix(logos): coinbase + kucoin colored PNG (was monochrome SVG, invisi…
Flotapponnier Jun 26, 2026
2799b81
hotfix(seo): bundle 3 SEO 404 fixes to main (alternatives + composite…
Flotapponnier Jun 26, 2026
3d469f3
Merge pull request #754 from ChainBench/chore/cherry-colored-logos
Flotapponnier Jun 26, 2026
8837340
fix(seo): canonicalize chain URLs on bench routes (kill uppercase orp…
Flotapponnier Jun 26, 2026
b1dbd6d
fix(benches): drop ton fallback from PromQL — harness emits gram only…
Flotapponnier Jun 26, 2026
c41d6d6
fix(seo): use permanentRedirect (308) instead of redirect (307) for c…
Flotapponnier Jun 26, 2026
32eb018
fix(seo): canonical points to /chains/<slug> when provider IS a chain…
Flotapponnier Jun 26, 2026
5a2e82c
fix(seo): filter unresolvable compare pairs out of search index (#759)
Flotapponnier Jun 26, 2026
887a128
fix(products): use raw bench slug for badge URLs, not canonical produ…
Flotapponnier Jun 26, 2026
549de50
Merge remote-tracking branch 'origin/main' into chore/sync-main-into-dev
Jun 26, 2026
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
76 changes: 76 additions & 0 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: PyPI Publish

on:
push:
tags:
- "python-v*"
workflow_dispatch:
inputs:
ref:
description: "Git ref to build (branch, tag or sha)"
required: false
default: "main"

jobs:
build:
name: Build sdist + wheel
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install build tooling
run: python -m pip install --upgrade pip build
- name: Build distributions
working-directory: packages/python-client
run: python -m build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: python-dist
path: packages/python-client/dist/

smoke-test:
name: Install + import smoke test
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/download-artifact@v4
with:
name: python-dist
path: dist
- name: Install built wheel
run: |
python -m pip install --upgrade pip
python -m pip install dist/openchainbench-*.whl
- name: Import smoke test
run: |
python -c "import openchainbench; print(openchainbench.__version__)"

publish:
name: Publish to PyPI (Trusted Publisher)
needs: [build, smoke-test]
if: startsWith(github.ref, 'refs/tags/python-v')
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/openchainbench/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: python-dist
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
37 changes: 37 additions & 0 deletions .github/workflows/python-client-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Python Client CI

on:
pull_request:
paths:
- "packages/python-client/**"
- ".github/workflows/python-client-ci.yml"
push:
branches: [main, dev]
paths:
- "packages/python-client/**"
- ".github/workflows/python-client-ci.yml"

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
env:
OCB_SKIP_INTEGRATION: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install package + dev deps
working-directory: packages/python-client
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Run unit tests
working-directory: packages/python-client
run: python -m pytest tests -m "not integration"
- name: Build sdist + wheel
working-directory: packages/python-client
run: python -m build
18 changes: 7 additions & 11 deletions benchmarks/l1-finality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,18 +191,14 @@ providers:
name: Gram
tag: Masterchain commit, polled via tonapi.io
formula: "Median wall-clock milliseconds for a Gram (formerly TON) masterchain block to commit under BAG consensus, polled via tonapi.io with a 3-seqno lookback, p50 over 24h."
# Prom selectors straddle ton|gram during the harness relabel window.
# The harness on Railway still emits chain="ton"; once it redeploys
# with chain="gram", the regex keeps matching both. Drop the ton
# branch after ~7 days of clean gram-labelled data.
queries:
p50: quantile_over_time(0.50, l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}[24h])
p90: quantile_over_time(0.90, l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}[24h])
p99: quantile_over_time(0.99, l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}[24h])
mean: avg_over_time(l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}[24h])
success: avg_over_time(l1_finality_wallclock_health{chain=~"ton|gram"}[24h])
sample_size: increase(l1_finality_wallclock_samples_total{chain=~"ton|gram"}[24h])
series: l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}
p50: quantile_over_time(0.50, l1_finality_wallclock_lag_milliseconds{chain="gram"}[24h])
p90: quantile_over_time(0.90, l1_finality_wallclock_lag_milliseconds{chain="gram"}[24h])
p99: quantile_over_time(0.99, l1_finality_wallclock_lag_milliseconds{chain="gram"}[24h])
mean: avg_over_time(l1_finality_wallclock_lag_milliseconds{chain="gram"}[24h])
success: avg_over_time(l1_finality_wallclock_health{chain="gram"}[24h])
sample_size: increase(l1_finality_wallclock_samples_total{chain="gram"}[24h])
series: l1_finality_wallclock_lag_milliseconds{chain="gram"}

- slug: stellar
name: Stellar
Expand Down
15 changes: 6 additions & 9 deletions benchmarks/network-fees.yml
Original file line number Diff line number Diff line change
Expand Up @@ -270,16 +270,13 @@ providers:
layer: l1
tag: Hardcoded 0.005 GRAM typical wallet v4 transfer
formula: "Conservative typical observed cost of a Gram (formerly TON) wallet transfer × GRAM USD price. Gram has no fee estimate RPC; this is the observed median."
# Straddle the ton/gram chain label during the harness relabel
# window. The harness Railway service currently emits chain="ton";
# the regex keeps matching once it redeploys with chain="gram".
queries:
p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}[24h])
p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}[24h])
p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}[24h])
mean: avg_over_time(tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}[24h])
success: avg_over_time(tx_fee_health{chain=~"ton|gram"}[24h])
series: tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}
p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="gram",tier="single"}[24h])
p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="gram",tier="single"}[24h])
p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="gram",tier="single"}[24h])
mean: avg_over_time(tx_fee_native_transfer_usd{chain="gram",tier="single"}[24h])
success: avg_over_time(tx_fee_health{chain="gram"}[24h])
series: tx_fee_native_transfer_usd{chain="gram",tier="single"}

- slug: stellar
name: Stellar
Expand Down
19 changes: 17 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,24 @@ const nextConfig: NextConfig = {
destination: "/chains/gram",
permanent: true,
},
// Scoped to the 3 benches that ship a gram (formerly ton)
// per_chain_explainer. The previous catch-all `/benchmarks/:slug/ton`
// fired for every bench, redirecting non-gram benches to a /gram
// path that 404s (e.g. aggregator-head-lag/ton → aggregator-head-lag/gram → 404),
// which Ahrefs and Google flag as a soft-404 redirect chain.
{
source: "/benchmarks/:slug/ton",
destination: "/benchmarks/:slug/gram",
source: "/benchmarks/l1-finality/ton",
destination: "/benchmarks/l1-finality/gram",
permanent: true,
},
{
source: "/benchmarks/network-fees/ton",
destination: "/benchmarks/network-fees/gram",
permanent: true,
},
{
source: "/benchmarks/wallet-labels-coverage/ton",
destination: "/benchmarks/wallet-labels-coverage/gram",
permanent: true,
},
...chainRedirects,
Expand Down
9 changes: 9 additions & 0 deletions packages/python-client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.venv/
dist/
build/
*.egg-info/
__pycache__/
*.py[cod]
.pytest_cache/
.coverage
htmlcov/
21 changes: 21 additions & 0 deletions packages/python-client/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 OpenChainBench contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
57 changes: 57 additions & 0 deletions packages/python-client/PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Publishing `openchainbench` to PyPI

Releases are fully automated through GitHub Actions and PyPI Trusted
Publishers. No long-lived API token is stored anywhere.

## One-time setup (PyPI Trusted Publisher)

1. Create an account on https://pypi.org if you do not have one.
2. Reserve the project name by uploading the first release manually
**or** request the project via the PyPI account settings. We do the
pending publisher flow below so the very first release is automated.
3. Go to https://pypi.org/manage/account/publishing/ and click
**Add a new pending publisher**.
4. Fill in:
- PyPI project name: `openchainbench`
- Owner: `ChainBench`
- Repository name: `OpenChainBench`
- Workflow name: `pypi-publish.yml`
- Environment name: `pypi`
5. Save. PyPI will accept the first upload from the matching GitHub
workflow with no token.
6. In GitHub, go to **Settings > Environments** for the repo and create
the `pypi` environment. Add required reviewers if you want a manual
gate before publish.

## Cutting a release

1. Bump the version in two places:
- `packages/python-client/pyproject.toml` (`project.version`)
- `packages/python-client/src/openchainbench/__init__.py` (`__version__`)
- `packages/python-client/src/openchainbench/client.py` (`USER_AGENT`)
2. Commit the bump on `main`.
3. Tag and push:
```bash
git tag python-v0.1.0
git push origin python-v0.1.0
```
4. The `PyPI Publish` workflow runs: build, smoke test on 3.10-3.13,
then publish via the Trusted Publisher OIDC flow.
5. Verify the release at https://pypi.org/project/openchainbench/.

## Local smoke build

```bash
cd packages/python-client
python -m pip install --upgrade build
python -m build
ls dist/
```

This produces an `sdist` (`.tar.gz`) and a pure-Python `wheel` (`.whl`).
The Trusted Publisher workflow runs the exact same command in CI.

## Tag namespace

We deliberately use `python-v*` (not bare `v*`) so OCB site tags such as
`v1.0-dataset` never trigger a Python publish.
118 changes: 118 additions & 0 deletions packages/python-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# openchainbench

Official Python client for [OpenChainBench](https://openchainbench.com), the live, reproducible benchmark suite for crypto infrastructure (RPC latency, bridge fees, perp venues, oracle deviation, and more).

[![PyPI version](https://img.shields.io/pypi/v/openchainbench.svg)](https://pypi.org/project/openchainbench/)
[![Python versions](https://img.shields.io/pypi/pyversions/openchainbench.svg)](https://pypi.org/project/openchainbench/)
[![Downloads](https://img.shields.io/pypi/dm/openchainbench.svg)](https://pypi.org/project/openchainbench/)
[![License](https://img.shields.io/pypi/l/openchainbench.svg)](https://github.com/ChainBench/OpenChainBench/blob/main/LICENSE)

The data served by openchainbench.com is published under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/). Attribute with a link back to the benchmark page (`pageUrl` on every payload).

## Install

```bash
pip install openchainbench
```

Requires Python 3.10+.

## Quick start

```python
from openchainbench import OpenChainBench

with OpenChainBench() as ocb:
for bench in ocb.list_benchmarks():
if bench.leader:
print(f"{bench.title}: {bench.leader.name} -> {bench.value}")
```

### Fetch one benchmark

```python
from openchainbench import OpenChainBench

with OpenChainBench() as ocb:
bench = ocb.get_benchmark("bridge-fee")
print(bench.headline)
for row in bench.rankings[:3]:
print(row.slug, row.ms.p50, row.success_rate)
```

### Fetch a time series

```python
from openchainbench import OpenChainBench

with OpenChainBench() as ocb:
series = ocb.get_series("bridge-fee", range="24h")
for provider in series.providers:
print(provider.name, provider.values[-1])
```

### Filter by chain or region

```python
bench = ocb.get_benchmark("network-fees", chain="ethereum")
series = ocb.get_series("network-fees", range="7d", chain="ethereum", region="eu-west")
```

## Error handling

The client maps HTTP responses to a typed exception hierarchy so callers
can react to intent rather than status codes.

```python
from openchainbench import (
OpenChainBench,
NotFoundError,
RateLimitError,
APIUnavailableError,
)

with OpenChainBench() as ocb:
try:
ocb.get_benchmark("not-a-real-slug")
except NotFoundError:
...
except RateLimitError as exc:
print(f"retry after {exc.retry_after_sec}s")
except APIUnavailableError:
# cold cache or Prom blackout, retry later
...
```

## API reference

| Method | Endpoint | Returns |
|---|---|---|
| `list_benchmarks()` | `GET /api/citable` | `list[BenchmarkSummary]` |
| `fetch_citable_index()` | `GET /api/citable` | `CitableIndex` |
| `get_benchmark(slug, *, chain=None, region=None)` | `GET /api/stat/<slug>` | `Benchmark` |
| `get_series(slug, *, range="24h", chain=None, region=None, providers=None)` | `GET /api/series/<slug>` | `Series` |

All models are immutable dataclasses (`frozen=True`).

## Rate limits

The public API allows 60 requests per minute per IP. The client surfaces
HTTP 429 as a `RateLimitError` with a `retry_after_sec` attribute.

## Citation

If you use the data in a paper, post, or product, please link the
benchmark page. The license is CC-BY-4.0. Every payload includes a
ready-to-paste `quote` field that already contains the attribution.

## Links

- Site: <https://openchainbench.com>
- Docs: <https://openchainbench.com/docs>
- Repository: <https://github.com/ChainBench/OpenChainBench>
- Issues: <https://github.com/ChainBench/OpenChainBench/issues>

## License

MIT. The data fetched from the API stays under CC-BY-4.0; this client
license only covers the code.
Loading
Loading