Base URL

https://api.mlip-bench.dev/v1

All responses are static JSON files served with Content-Type: application/json.


Authentication

The API is protected by Cloudflare Access using a service token. Include both headers in every request:

Header Value
Cf-Access-Client-Id Your service-token Client ID
Cf-Access-Client-Secret Your service-token Client Secret

Contact the maintainers to obtain a service token.


Index & discovery

Endpoint Description
/v1/index.json Catalog: API version, data version, endpoint listing
/v1/openapi.json OpenAPI 3.1 schema for all endpoints
/v1/llms.txt Plain-text summary for LLM consumption (llms.txt standard)

curl examples

1 — Fetch the catalog

curl -s \
  -H "Cf-Access-Client-Id: $CF_CLIENT_ID" \
  -H "Cf-Access-Client-Secret: $CF_CLIENT_SECRET" \
  https://api.mlip-bench.dev/v1/index.json | jq .

2 — Fetch a potential record

curl -s \
  -H "Cf-Access-Client-Id: $CF_CLIENT_ID" \
  -H "Cf-Access-Client-Secret: $CF_CLIENT_SECRET" \
  https://api.mlip-bench.dev/v1/potentials/gracefm_GRACE-2L-OAM.json | jq .

3 — Fetch a track leaderboard

curl -s \
  -H "Cf-Access-Client-Id: $CF_CLIENT_ID" \
  -H "Cf-Access-Client-Secret: $CF_CLIENT_SECRET" \
  https://api.mlip-bench.dev/v1/tracks/elastic/leaderboard.json | jq '.entries[:5]'

Python client (mlip_bench)

A typed Python client wraps every endpoint on this page — typed structures and workflow instances, ase.Atoms geometry, pandas leaderboard/parity DataFrames, bulk dataset download, and client-side point lookups into the hash-sorted geometry/corpus stores over plain HTTP Range reads. It lives at clients/python/ in this repo; the walkthrough below is the full tour, and clients/python/README.md + examples/quickstart.py mirror it.

Install

The base client is on PyPI as mlip-bench 0.1.0:

pip install 'mlip-bench[pandas,datasets]'

That release is the client only. The lake and mcp extras and the MCP server landed after it and are not published yet, so for those install from a checkout of this repo instead:

pip install -e clients/python[pandas,datasets]
Extra Adds Needed for
(base) httpx, pyarrow, ase Client, structures, geometry, workflows, gbseg/corpus, store lookups
pandas pandas>=2 Track.leaderboard() / Track.parity() (DataFrames)
datasets fsspec[http] Datasets.filesystem() (stream bulk parquet without downloading)
pymatgen pymatgen geometry.to_pymatgen()

Credentials

Client() takes no required arguments; it resolves the same Cloudflare Access service token documented above, from the first fully-specified source of:

  1. explicit argsClient(client_id="...", client_secret="...")
  2. env varsMLIP_BENCH_CLIENT_ID / MLIP_BENCH_CLIENT_SECRET
  3. ~/.config/mlip-bench.toml[auth] client_id / client_secret
  4. ~/.config/mlip-bench.env — shell-style CF_CLIENT_ID= / CF_CLIENT_SECRET=

With none found it proceeds unauthenticated (emitting a UserWarning; pass anonymous=True to silence). The secret is never logged, and is redacted from repr().

Pulling data — a full tour

from mlip_bench import Client

client = Client()                        # credentials resolved automatically (see above)

Structures — by atoms_hash, mp_id, or a {namespace}:{structure_id} alias (aliases 302-redirect transparently wherever a hex hash is accepted):

s = client.structure("mp-132")           # -> Structure
s.record.element                         # 'Fe'
s.record.formula                         # 'Fe'
s.record.workflow_ids                    # ['elastic:allegro_MP-L', 'relax-full:...', ...]
s.record.geometry_available              # True

Geometry — as an ase.Atoms (the input / DFT-reference cell):

atoms = s.geometry()                     # ase.Atoms
atoms.get_chemical_formula()             # 'Fe'
atoms.cell, atoms.positions, atoms.numbers

Workflow instances — one typed record per (workflow, potential):

inst = s.workflow("elastic:allegro_MP-L")    # -> WorkflowInstance
inst.status                                  # 'done'
unit = inst.units[0]
unit.outcomes["k_vrh"]                       # e.g. bulk modulus (GPa)
unit.geometry["mlip_relaxed"].availability   # 'served' | 'not_computed' | 'bulk_only' | 'sp_only' | 'not_persisted'

Each unit's geometry is a set of honestly labelled references. Follow a served one to its ase.Atoms; a non-served one raises rather than hand you a dangling link:

from mlip_bench import GeometryUnavailable

ref = unit.geometry["mlip_relaxed"]
try:
    relaxed = client.fetch_geometry(ref)     # ase.Atoms, only if availability == 'served'
except GeometryUnavailable:
    print(ref.availability, "—", ref.note)   # e.g. 'not_computed — no geometry persisted for this unit'

Most workflows expose a single relaxed cell (ref.href is one URL). surface-slab and gb-energetics expose several — the relaxed slab and its reference bulk, several GB cells — so ref.artifacts is a {name: url} dict and you pass artifact=:

ref = unit.geometry["mlip_relaxed"]          # surface-slab unit (one Miller/termination)
ref.artifacts                                # {'slab_relaxed': ..., 'bulk_relaxed': ...}
slab = client.fetch_geometry(ref, artifact="slab_relaxed")   # ase.Atoms — the relaxed slab
bulk = client.fetch_geometry(ref, artifact="bulk_relaxed")   # the relaxed reference bulk

The input / initial cell is always unit.geometry["input"] (equivalently s.geometry()); note that for surfaces this is the bulk seed cell — the unrelaxed slab is not persisted. Calling fetch_geometry without an artifact on a multi-artifact ref (or with one on a single-artifact ref) raises ValueError telling you the available names.

Leaderboards & parity — as pandas DataFrames (needs the pandas extra):

lb = client.track("elastic").leaderboard()   # DataFrame indexed by `potential`
lb.head()
client.track("ordering").leaderboard(scheme="sp")   # the ordering track is published per-scheme

ASSYST corpus (~7.29M single-point structures) — client.structure(hash) transparently falls back to the corpus for a corpus-only hash (s.corpus is then populated, and s.geometry() still works, served from the corpus-geometry store); or fetch the raw record directly:

rec = client.corpus_record("a3f…64-hex")     # dict: {structure, potentials{}, geometry}

Grain-boundary segregation (Worker-served from a hash-sorted store, ~10M instances):

gb = client.gbseg_instance("aa…hash", "GRACE-2L-OAM-L")   # -> WorkflowInstance

Bulk datasets — list the catalog, download a parquet (Range-resumable), or stream it without downloading:

cat  = client.datasets.list()          # {"bulk": [{name, url, size}, ...], "subsets": [...]}
name = cat["bulk"][0]["name"]
path = client.datasets.download(name, dest_dir="/tmp")    # Range-resumable
fs   = client.datasets.filesystem()                       # fsspec 'http' fs (needs [datasets])

Direct store point-lookups — resolve one row of any hash-sorted store client-side (manifest → prefix-shard index → one row group → filter by key), over plain HTTP Range reads, with no server-side query route:

atoms = client.query_geometry("geom-elemental-relax", "…::relax-full::…::-::relaxed")
row   = client.stores.lookup("wf-gbseg-instances", "…::gbseg-relax::…::bfgs")

Errors

Exception Raised when
NotFoundError 404 — unknown structure/workflow/alias id (subclass of ApiError)
ApiError any other non-2xx after retries (GET retries 429/500/502/503/504 with backoff)
GeometryUnavailable fetch_geometry(ref) on a non-served ref (not_computed, bulk_only, …) — inspect ref.availability / ref.note first
ParityNotPublished Track.parity() where per-structure parity shards aren't published as API resources here
DataVersionMismatch Client(expect_data_version=…) and the server's /v1/index.json reports a different data_version
from mlip_bench import (
    NotFoundError, ApiError, GeometryUnavailable,
    ParityNotPublished, DataVersionMismatch,
)

MCP server (mlip-bench-mcp)

An MCP server that lets an LLM agent (Claude, or any MCP client) explore the benchmark data lake directly — ask a question in English, get an answer computed over the real parquet.

It is local and zero-hosting: the agent runs it on its own machine and queries data.mlip-bench.dev with client-side DuckDB over HTTPS. There is no server here to host and no per-user compute cost. It reads the lake rather than this /v1 API, so it sees the current snapshot — including datasets /v1 has not been re-baked for yet.

A Cloudflare Access service token is required. The lake bucket is Access-gated: an unauthenticated request is redirected (302) to a login page, and the server exits at startup with could not reach the data lake … (Expecting value: line 1 column 1) — a JSON parser meeting an HTML login page.

Needs mlip-bench 0.2.0 or newer. 0.1.0 predated this server and shipped neither the mcp extra nor the mlip-bench-mcp entry point, so on it pip install 'mlip-bench[mcp]' warned does not provide the extra 'mcp' and left you with no server.

Install & register

pip install 'mlip-bench[mcp]'           # 0.2.0+
mlip-bench-mcp                          # stdio

Register it with an MCP client — for Claude Desktop, claude_desktop_config.json:

{ "mcpServers": { "mlip-bench": {
  "command": "mlip-bench-mcp",
  "env": {
    "MLIP_BENCH_CLIENT_ID": "<service-token-id>",
    "MLIP_BENCH_CLIENT_SECRET": "<service-token-secret>"
  } } } }

Credentials resolve from, first wins: MLIP_BENCH_CLIENT_ID/MLIP_BENCH_CLIENT_SECRET, ~/.config/mlip-bench.toml ([auth] client_id/client_secret), or ~/.config/mlip-bench.env (CF_CLIENT_ID/CF_CLIENT_SECRET) — so with the env file already present you can omit the env block. Pin an immutable snapshot with MLIP_BENCH_LAKE_URL=https://data.mlip-bench.dev/v=<date>.

The nine tools

Tool Purpose
list_elements / list_potentials / list_tracks enumerate the dimensions
describe_datasets(name?) data dictionary — every dataset, its view name, row count and column schema. Read this before query.
get_potential(id) one potential's metadata + leaderboard coverage
leaderboard(track) a track's leaderboard rows
search(dataset, filters, limit?) equality-filter any dataset (bounded)
query(sql, limit?) read-only DuckDB SELECT across dataset views (bounded)
get_structure(id, include_outcomes?) structure by hash / alias / structure_id; opt-in per-potential outcomes (slow)

describe_datasets is derived from the lake's own catalog rather than a hardcoded list, so the tool surface tracks the lake automatically — it currently reports 61 datasets, and a newly published one appears with no change here.

Notes for agents


Property reference — every field the site serves

Everything the website plots is reachable here. The data comes in four families, each with a fixed set of named properties:

  1. Structure → workflow outcomes — the physical result of running one potential on one structure (elastic Cᵢⱼ, vacancy formation energy, surface γ, …). Keyed (atoms_hash, workflow, potential).
  2. Track leaderboards — the aggregate score of one potential on one benchmark track, ranked. Keyed (track, potential).
  3. Potential records — metadata about one potential (architecture, training set, params, links).
  4. Element aggregates — coverage counts per chemical element.

Scalars only. Every property below is a scalar (or a small fixed vector like the 21 elastic constants). Curve/array data — phonon DOS spectra, band structures, QHA F(T) curves — is not served by this API; it lives as bulk assets behind the /data page. See Curve data below.


Family 1 — Structure → workflow outcomes

Record path (static, one file per structure×potential):

/v1/structures/{atoms_hash}/workflows/{workflow}:{potential}.json

Each record has status, a method block, provenance, and a units[] list. Every unit carries an outcomes{} dict (the properties) and a geometry{} block of honestly-labelled cell references. Multi-unit workflows (surfaces, vacancies, GB) split one record into several units, discriminated by a key{}.

The complete outcome catalog, workflow by workflow:

Stability ordering — ordering-sp · 1 unit · live

Property Type Unit / meaning
spearman num Spearman ρ of predicted vs DFT polymorph energy ranking (−1…1)
ground_state_correct bool did the potential put the true ground state first
dE_mae num mean abs. error of relative polymorph energies (eV/atom)
n int number of polymorphs ranked
status str done / error / …

Cell relaxation — relax-hydro (hydrostatic) & relax-full (full cell+ions) · 1 unit · live

Property Type Unit / meaning
final_energy_ev_atom num relaxed energy (eV/atom)
volume_ang3 num relaxed cell volume (ų)
pressure_gpa num residual pressure (GPa)
converged bool optimizer reached fmax
n_steps int optimizer steps
status str run status

(relax-fixedcell — legacy fixed-cell variant — carries the same fields minus volume_ang3/pressure_gpa; superseded, not currently emitted.)

Elastic tensor — elastic · 1 unit · live

Property Type Unit / meaning
c11c66 num the 21 independent stiffness constants (GPa), Voigt notation
k_vrh num Voigt–Reuss–Hill bulk modulus (GPa)
g_vrh num Voigt–Reuss–Hill shear modulus (GPa)
mechanically_stable bool Born stability criteria satisfied
k_pct_err / g_pct_err num % error of K/G vs DFT reference
status str run status

Vacancy formation — vacancy-site · 1 unit per inequivalent site (key.site_index) · live

Property Type Unit / meaning
formation_energy num vacancy formation energy (eV)
wyckoff_symbol str Wyckoff label of the removed site
site_element str element removed
deviation_from_median num Eᶠ − median over potentials (eV) — MLIP-spread reference
lit_dev_ev / dft_dev_ev num deviation from literature / DFT (eV)
converged bool ionic relaxation converged
status str run status

Surface energy — surface-slab · 1 unit per (Miller, termination) (key.miller_index, key.termination_index) · live

Property Type Unit / meaning
gamma_j_per_m2 num surface energy γ (J/m²)
slab_converged bool slab relaxation converged
slab_n_atoms int atoms in the slab
status str run status

(surface-bulk — the reference-bulk relaxation feeding γ — is its own workflow, emitted since P5 as one unit per (structure, potential) with bulk_energy_ev_atom, bulk_converged, bulk_n_atoms and no status (status is a per-facet property of surface-slab, not of the one bulk relaxation the facets share). Its mlip_relaxed geometry links straight into geom-surface, under the slab facet the bulk cell was computed in — 5,057 of 5,277 pairs. The other 220 persisted no bulk cell and get no link at all rather than one that 404s, and they are not one thing: 89 report not_persisted — the bulk relaxation ran and its energy is right there in the record's outcomes (74 of the 89 with bulk_converged: true), only the cell is gone — and the remaining 131 report not_computed, the run having errored without producing an energy.)

GB energetics — gb-energetics · 1 unit per GB (key.gb_id) · live

Property Type Unit / meaning
gb_energy num grain-boundary energy (J/m²)
excess_volume num GB excess volume (ų)
surface_energy num free-surface energy (J/m²)
wsep_rigid / wsep_relaxed num work of separation, rigid / relaxed (J/m²)
cleavage_profile str cleavage-curve descriptor
status str run status

GB segregation — gbseg-relax · 1 unit per optimizer (key.algo = BFGS/FIRE) · Worker-served

Reached via the Worker route (not a static file) — see the query recipes below.

Property Type Unit / meaning
n_steps int optimizer steps
fmax_target num force convergence target (eV/Å)
error_msg / error_class str failure detail, if any
status str run status

(The relaxation runtime — runtime_ms, gpu_mem_mb, device — rides in the record's separate runtime{} block. The segregation energy itself, E_seg, is a leaderboard-level scored quantity — see Family 2, track segregation.)

ASSYST corpus single-point — sp · ~7.29M structures · Worker-served

Reached via /v1/query/corpus/{atoms_hash}.

Property Type Unit / meaning
energy_err_per_atom num MLIP−DFT energy error (eV/atom)
energy_err_per_atom_corrected_global num after global linear-reference correction (eV/atom)
energy_err_per_atom_corrected_elemental num after per-element correction (eV/atom)
stress_rmse num stress RMSE (GPa)
max_force num max force component (eV/Å)
status str eval status

Not yet emitted (registered, gated until their instance tables land): phonon-fc2 (omega_max_thz, has_imaginary, frac_imaginary, most_negative_freq_thz), qha (alpha_300k_per_k, b_300k_gpa, cv_300k_j_per_mol_k, qha_converged), and per-structure wsep (wsep_rigid_j_per_m2, wsep_relaxed_j_per_m2) whose scalar rows have no store export today (the cleavage leaderboard below is served, the per-structure records are not). The scalar phonon/QHA summaries are downloadable in bulk from /data.

Geometry references. Each unit's geometry{} gives input, dft_relaxed, and mlip_relaxed cells, each tagged with an honest availability label:

Label Meaning
served fetch it now (href present)
bulk_only the coordinates exist — in a bulk parquet, a lake dataset or a geometry store — but are not addressable one at a time through this API
not_persisted computed, then discarded. There is nothing to fetch, anywhere
not_computed never produced by this workflow
sp_only this potential only ran single-point, never relaxed

These labels describe the data, not this API. The distinction is load-bearing and we got it wrong: bulk_only and not_persisted are not two ways of saying "you can't get it here". The first says the bytes are out there and points you at them; the second says they are gone. If a cell says bulk_only, look in /data or the lake — you can have it, just not one at a time.

Six labels were corrected on 2026-08-16 after an audit measured them against the published data; the largest said not_persisted on 45,553 records whose coordinates were sitting in the lake the whole time. Every bulk_only cell now has to name where the bytes are, and a test resolves that name against the store list and the lake catalog, so this class of error fails a build instead of being discovered by a reader.

A label can also be wrong for only some of a workflow's records. On 2026-09-07, 89 surface-bulk units that had no stored relaxed bulk cell moved not_computed -> not_persisted, because the record they ship in publishes the energy of the very run whose cell is missing — and 74 of them report it converged, which the old note flatly denied ("failed or unconverged run"). The split is a rule about the data, not a list: any unit whose record kept the run's outcome is not_persisted, and the 131 that kept nothing keep not_computed.

How to query one outcome

# curl — the elastic tensor of mp-20 under uma_M-1p1
curl -s -H "Cf-Access-Client-Id: $CF_CLIENT_ID" -H "Cf-Access-Client-Secret: $CF_CLIENT_SECRET" \
  "https://api.mlip-bench.dev/v1/structures/$(
    curl -s -H "Cf-Access-Client-Id: $CF_CLIENT_ID" -H "Cf-Access-Client-Secret: $CF_CLIENT_SECRET" \
    -o /dev/null -w '%{redirect_url}' https://api.mlip-bench.dev/v1/structures/mp-20 | sed -E 's#.*/structures/##')/workflows/elastic:uma_M-1p1.json" \
  | jq '.units[0].outcomes | {k_vrh, g_vrh, mechanically_stable}'
from mlip_bench import Client
c = Client()

# scalar outcomes, any workflow, by mp_id / atoms_hash / alias:
inst = c.structure("mp-20").workflow("elastic:uma_M-1p1")
o = inst.units[0].outcomes
o["k_vrh"], o["g_vrh"], o["mechanically_stable"]      # 122.4, 73.5, True

# per-site / per-facet workflows expose one unit each — read the key:
vac = c.structure("mp-20").workflow("vacancy-site:GRACE-2L-OAM-L")
for u in vac.units:
    print(u.key["site_index"], u.outcomes["site_element"], u.outcomes["formation_energy"])

# Worker-served corpus & gbseg:
c.corpus_record("a3f…64hex")                          # sp outcomes for a corpus structure
c.gbseg_instance("…hash", "GRACE-2L-OAM-L")           # gbseg-relax instance

Discover which (workflow:potential) pairs a structure has before fetching:

s = c.structure("mp-20")
s.record.tracks_present          # ['elastic', 'lattice', 'phonon', ...]
s.record.workflow_ids            # ['elastic:uma_M-1p1', 'relax-full:...', ...]

Family 2 — Track leaderboard metrics

/v1/tracks/{track}/leaderboard.json{track, scheme, rows[]}; each row is one potential. The metric columns differ per track — the full set:

Track Row metrics (besides potential, n)
pes E_rmse_corrected, E_mae_corrected, E_rmse_corrected_elemental, E_mae_corrected_elemental, F_rmse_comp, F_mae_comp, F_radial_rmse, F_radial_mae, F_ang_med_deg, S_rmse_voigt
ordering mean_spearman, gs_hit_rate, median_dE_mae — per scheme (sp/hydro/full)
elastic median_abs_k_pct, median_abs_g_pct, frac_mech_stable
lattice median_edge_mape, median_vol_pct, frac_edge_1pct, median_strain
surface surface_mae, surface_rmse, surface_mbe (J/m²)
vacancy vacancy_consensus_mae, vacancy_consensus_mbe, vacancy_dft_mae, vacancy_lit_mae (eV)
phonon phonon_omega_consensus_mae, phonon_imag_frac, phonon_qha_bulk_mae, phonon_qha_bulk_mbe, phonon_mp_omega_mae
segregation Eseg_rmse, Eseg_mae, Eseg_spearman, n_best_from_bfgs, n_best_from_fire, algo_used
cleavage Wsep_rmse, Wsep_mae, Wsep_spearman, n_best_from_bfgs, n_best_from_fire, algo_used

The ordering track publishes per-scheme sub-leaderboards instead of a single leaderboard.json: /v1/tracks/ordering/leaderboard-{sp,hydro,full}.json.

curl -s -H "Cf-Access-Client-Id: $CF_CLIENT_ID" -H "Cf-Access-Client-Secret: $CF_CLIENT_SECRET" \
  https://api.mlip-bench.dev/v1/tracks/phonon/leaderboard.json | jq '.rows[:3]'
lb = c.track("elastic").leaderboard()          # DataFrame indexed by `potential`
lb.sort_values("median_abs_k_pct").head()
c.track("ordering").leaderboard(scheme="sp")   # ordering is per-scheme

Family 3 — Potential records

/v1/potentials.json (all) and /v1/potentials/{id}.json (one). Fields:

Property Type Meaning
id str canonical potential id (used everywhere as :{potential})
display_name str human label
architecture str model family
checkpoint str exact weights / release used
version str version string
params str parameter count
training_set / dataset_group str training data + its group
repo_url / paper_url str source + reference
matbench_discovery str|null Matbench-Discovery entry name, if any
affiliation obj {slug, name, short, url, logo}
flag str|null caveat flag (e.g. known defect), if any
perf obj|null technical-performance metrics, if measured
c.potentials()                       # list of all records
c.potential("gracefm_GRACE-2L-OAM")  # one record

Family 4 — Element aggregates

/v1/elements.json{elements: [{element, n_potentials}, …]} — how many potentials have data for each chemical element.

curl -s -H "Cf-Access-Client-Id: $CF_CLIENT_ID" -H "Cf-Access-Client-Secret: $CF_CLIENT_SECRET" \
  https://api.mlip-bench.dev/v1/elements.json | jq '.elements[] | select(.element=="Fe")'

Curve data (not in the API)

Phonon DOS spectra, band structures, and QHA F(T)/α(T)/Cv(T) curves are the one thing the website renders that this API does not serve per-id — only the scalar summaries above (omega_max_thz, alpha_300k_per_k, …) are exposed. The full curves are downloadable in bulk from the /data page (phonon parquet bundles, per-structure band shards, the raw FC2 archive). A per-material curve endpoint (client.phonon_dos(id, potential)(freq, dos)) is designed but not yet built.


Endpoint reference

Terse index of every route (properties for each are catalogued above).

Potentials

Endpoint Description
/v1/potentials.json Full list of all potentials with metadata
/v1/potentials/{id}.json Single potential record (Family 3 fields)

Structures → workflows

Endpoint Description
/v1/structures/{atoms_hash}.json Structure summary (tracks_present, workflow_ids, geometry_available)
/v1/structures/{atoms_hash}/workflows.json All workflow instances for a structure
/v1/structures/{atoms_hash}/workflows/{wf}:{pot}.json One instance record (Family 1 outcomes + geometry)
/v1/structures/{atoms_hash}/geometry.{json,cif,poscar} Input/reference cell
/v1/query/corpus/{atoms_hash} Worker: one ASSYST-corpus sp record
/v1/query/instance/gbseg/{atoms_hash}/{potential} Worker: one gbseg-relax instance
/v1/query/geom/{store}/{key} Worker: one geometry row from any hash-sorted store

A non-hex id (mp-132, elemental:59, …) 302-redirects to its atoms_hash.

Tracks

Available track ids: pes, ordering, elastic, lattice, surface, vacancy, phonon, segregation, cleavage, technical-performance.

Endpoint Description
/v1/tracks/{track}/leaderboard.json Per-track ranked leaderboard (Family 2 metrics)
/v1/tracks/ordering/leaderboard-{mode}.json Ordering sub-leaderboards: sp, hydro, full

Elements

Endpoint Description
/v1/elements.json Per-element aggregate metrics across all potentials

Datasets

Endpoint Description
/v1/datasets.json Catalog of bulk DFT parquet files and any declared partitioned subsets

OPTIMADE

An additive OPTIMADE v1.2 Tier-1 facade lives alongside the native API above, for tooling that speaks the OPTIMADE standard (e.g. optimade-client, aggregators, the official validator). It is honest about scope: fully conformant where we can be, and a proper OPTIMADE 501 error object — never a silent guess — everywhere our serverless architecture can't yet do more. The native API above stays the primary product; OPTIMADE _mlipbench_-prefixed fields link back into it for anything OPTIMADE has no room to express (workflow outcomes — elastic/vacancy/phonon/etc. results).

Base URL

https://api.mlip-bench.dev/optimade/v1

(versions is the one endpoint the OPTIMADE spec pins at the unversioned base: https://api.mlip-bench.dev/optimade/versions.)

Endpoints

Endpoint Description
/optimade/v1/info Entry-point info: api_version, available_api_versions, available_endpoints, is_index: false
/optimade/v1/info/structures Property definitions for the structures entry type (incl. _mlipbench_* custom fields)
/optimade/v1/links Related-database links (empty — no external OPTIMADE providers registered; that's a Tier-2 decision)
/optimade/versions Supported major API versions, text/csv; header=present (version\n1\n) — unversioned base, per spec
/optimade/v1/structures Collection listing — page 1 unfiltered (full entries), or dynamic pages with ?filter=/?page_limit=/?page_offset=/?response_fields= (elemental set only, reduced 4-field subset, see below)
/optimade/v1/structures/{id} Single entry. {id} is atoms_hash (sha256 hex). Covers all 834 elemental structures and the full ~7.29M-structure ASSYST corpus (fetched on demand from the corpus geometry store)

Note on page 1 shape: The static unfiltered page at /optimade/v1/structures (no query string) returns full OPTIMADE entries. Dynamic pages (any query parameters: filter=, page_offset>0, page_limit, etc.) return the reduced 4-field index subset; clients walking links.next will observe this shape change. Full OPTIMADE attributes are always available via the per-id route /optimade/v1/structures/{id}.

Single-entry coverage is total: every corpus/elemental structure the native API knows by atoms_hash is reachable here too, one entry at a time. The collection route (structures?filter=...), by contrast, only ever indexes the 834-structure elemental set — filtering the full corpus is out of scope for Tier-1 (see Non-goals in the design doc).

A non-hex id (an mp_id like mp-132, or a namespaced alias) 302-redirects to the canonical atoms_hash path, same alias resolution the native API uses. An uppercase-hex id canonicalizes (302) to lowercase.

Supported filter subset (collection route only)

Tier-1 supports exactly these atoms, combined with AND only (no OR, NOT, parentheses, or range comparisons):

Filter Example
id="..." filter=id="a3f...64hexchars"
nelements=N filter=nelements=2
elements HAS "X" filter=elements HAS "Fe"
elements HAS ALL "X","Y" filter=elements HAS ALL "Fe","Ni"
elements HAS ANY "X","Y" filter=elements HAS ANY "Fe","Ni"
chemical_formula_reduced="..." filter=chemical_formula_reduced="Fe2O3"
chemical_formula_anonymous="..." filter=chemical_formula_anonymous="AB2"
AND combinations of the above filter=nelements=2 AND elements HAS "Fe"

response_fields= on the collection route is similarly limited to the four attributes the listing actually indexes (elements, nelements, chemical_formula_reduced, chemical_formula_anonymous) — fetch /structures/{id} for the full mandatory attribute set (positions, cell, species, ...). An empty response_fields= is treated as absent (default four-field set), not "zero fields."

The 501 policy (honest Tier-1)

Anything recognized as valid OPTIMADE filter grammar but outside the subset above — OR, NOT, comparison operators (!=, <, <=, >, >=), CONTAINS/STARTS/ENDS/LENGTH, IS KNOWN/IS UNKNOWN, HAS ONLY, parenthesized/grouped expressions, or a real OPTIMADE property we simply don't index for filtering (lattice_vectors, species, ...) — returns 501 with an OPTIMADE error object (title: "UnsupportedFilterFeature"), not a silent partial match or a fabricated result:

{
  "errors": [{ "status": "501", "title": "UnsupportedFilterFeature", "detail": "..." }],
  "meta": { "api_version": "1.2.0", "provider": { "prefix": "mlipbench", ... } }
}

A filter that doesn't parse as valid OPTIMADE grammar at all (unterminated quote, stray character, right property/wrong shape) returns 400 (title: "FilterSyntaxError") instead — 501 is reserved for constructs we recognize but don't (yet) implement, 400 for ones that are simply malformed. Filtering the ASSYST corpus collection (as opposed to a single-entry lookup by id) is always 501 — collection filtering is elemental-only in Tier-1. The references/files OPTIMADE entry types are not implemented at all — any request under /optimade/v1/references or /optimade/v1/files gets the same 404 OPTIMADE error object every unknown route gets.

Auth

Same gate as the native API: Cloudflare Access via a service token, identical Cf-Access-Client-Id/Cf-Access-Client-Secret headers (see Authentication above). There is no separate, more-open auth tier for OPTIMADE — public-read exposure and registration with providers.optimade.org are recorded Tier-2 decisions, not taken here.

Workflow outcomes: use the native API

OPTIMADE has no vocabulary for benchmark results (elastic constants, vacancy formation energies, phonon spectra, ...). Every entry carries _mlipbench_workflows_url, a link back into the native structure→workflow API (/v1/structures/{id}/workflows.json for the 834 elemental structures, /v1/query/corpus/{atoms_hash} for ASSYST-corpus structures) — follow it for anything beyond geometry and chemistry. The Python client above wraps that structure→workflow API (typed WorkflowInstances, honest geometry labels); see /v1/openapi.json for the full schema of every workflow/query route.


Regenerating the API

The static files under api_dist/api/v1/ are generated locally and pushed to R2:

# Regenerate all JSON endpoints (routes heavy Python builders via srun)
npm run build:api

# Push to R2 (requires rclone configured with r2:benchmark-assets)
PUSH_API=1 bash tools/push_to_r2.sh

The light-weight node-only step (skips Python builders) is also available:

npm run build:api:node