meterline-gold
Part of meterline. meterline helps people who run or pay for AI tools in an organisation see and compare usage and cost across providers: it reads each provider's usage API and writes the data in one common record format.
Reads meterline silver (the v2 records), a set of organisation files, a reference price list and a policy file, and writes a gold folder. The folder holds datasets about people, groups, seats, costs and data quality, each already filtered for the audience that gets it (central, manager, person, exec). Nothing it writes enforces anything. Project terms such as silver, gold and ground truth are defined in the glossary.
It reads files and writes files. It calls no provider API, needs no credential other than the hashing key, and opens no network connection. Python 3.10 or later; runtime dependencies are meterline, duckdb, pydantic and PyYAML.
Status: built from the accepted spec docs/dev/history/meterline-gold-core-SPEC-v1.1-2026-10-14.md (phases 1 and 2 were built against v1.0, whose findings v1.1 resolves) in three phases; all three are built. The hidden marker and gold schema version 1.1 follow the accepted spec docs/dev/meterline-gold-core-SPEC-v1.2-2026-10-15.md. See Known limits.
| Phase | What it covers |
|---|---|
| 1 | Package, parameter files and schemas, the LiteLLM price importer, loading and hashing, the organisation datasets, person_day, person_day_activity, person_month, seats, pools, costs, reconciliation, data quality, the central/ and delivery/ folders, the manifest, assertions A01-A03, A08, A09 and the CLI. |
| 2 | The fixed grid, the privacy algorithm, comparison groups, the manager/, person/ and exec/ folders, assertions A04-A07 and the example dashboard. |
| 3 | Shadow scenarios, alert events, named exceptions and the rest of the layout; the manifest's unknown_envelope_keys, the month's unit tree, A05's parent-subset part, A06 step a and the escaping of cell ids. |
The findings the builds logged against the spec are in docs/dev/change-list-meterline-gold.md.
Read this before changing gold
In this order:
- This README: what a build reads and writes, what it refuses, and the privacy rules.
docs/dev/meterline-gold-core-SPEC-v1.2-2026-10-15.md, the contract. It is long: read section 3 (inputs), section 6 (the datasets), section 7 (privacy) and section 9 (assertions) first. An accepted spec is frozen: a finding goes to the change list.docs/dev/change-list-meterline-gold.md: what each build and review found against the spec, and what the code does meanwhile. Read the entries for the section you change.docs/gold-fixture/README.md: the hand-built fixture whose expected outputs were computed by hand.src/meterline_gold/sql/, in file-name order. The SQL is the engine;run.pyloads, executes and fetches, andload.pyandwrite.pydo the stage 0 and the writing. No dataset figure is computed in Python.tests/test_done_check_G*(the acceptance checks) and thetest_review_*files (the checks added after the review).
Gold computes the reference cost and the credit pool; meterline-sim recomputes both on its own as an independent oracle, so a change to a price or pool rule is made on both sides separately (see the simulator's README).
pip install meterline-gold
export METERLINE_GOLD_KEY="a-key-of-at-least-32-bytes-that-you-keep-out-of-git"
meterline-gold build --silver SILVER --org-files ORG --policy POLICY.yaml --prices PRICES.yaml --out gold
From a clone of the repository (for the tests and development):
pip install -e packages/meterline -e packages/meterline-sim -e "packages/meterline-gold[dev]" # the tests run the simulator
The command
meterline-gold build --silver DIR --org-files DIR --policy FILE --prices FILE --out DIR [--invoice FILE] [--parquet]
meterline-gold schemas --out DIR
meterline-gold import-litellm-prices --file FILE --keys FILE --as-of YYYY-MM-DD --out FILE
--silver points at the folder that holds records/ (the folder above it), not at records/ itself; a folder with no silver records is an input error.
schemas writes the policy and price schemas, the organisation, manifest and dataset schemas (one gold-dataset-<name>.schema.json per dataset file form, with column types, units, descriptions, keys and joins) and gold-audiences.json, which says which file may be served to which reader. From Python, meterline_gold.servable(path, audience) reads those rules. It is False for delivery/ whatever the audience. It is True for manifest.json when the audience is central and False for the other three. Any other file is True only for the audience whose folder holds it, and a path the rules do not list is False. The same facts as a page are in the dataset reference.
Exit codes: 0 done; 2 bad arguments, an input that fails validation, a missing or short key, or an output folder that is not empty; 1 an assertion failed during the build (nothing is written).
Public interface
What later releases keep working:
- the three commands above, with their options and exit codes;
- the policy and price file formats (
policy_format,price_list_format, and the JSON Schemasmeterline-gold schemaswrites), the dataset schemas andgold-audiences.jsonit writes beside them; - the organisation CSV files (JSON Schemas in
docs/schemas/); - the datasets and
manifest.jsona build writes, versioned byGOLD_SCHEMA_VERSION(every dataset row carries it inschema_version, the manifest ingold_schema_version); - from Python, the names in
meterline_gold.__all__:build,servable,GOLD_SCHEMA_VERSION,InputError,BuildFailureand__version__.
Everything else is internal and may change in any release: every module of the meterline_gold package, the SQL files and the example dashboard's code.
What the inputs must be
A build stops with exit 2, writes nothing and names the file when:
- a silver record breaks a rule of the record format: the
meterlinevalidator runs on every record read and refuses, for example, a negative amount, an unknownactor_kindorcost_basis, a token sum that does not add up or a number that is not finite; - silver costs are in more than one currency, or in a currency other than the policy's: gold never adds amounts of different currencies, so build once per currency from silver that holds one;
- a currency (policy, price list, silver) is not an ISO 4217 code (three upper-case letters from the list in
iso4217.py).
A build does not stop for a person that an identity row or a usage row names and people.csv does not employ that month, nor for a unit whose parent_id is no unit valid that day. The first drops that person's cost from every person_month, the second makes the unit a root. Both are counted: data_quality gets rows_person_missing and cost_ref_person_missing (per month, provider and org) and units_parent_unknown (per month), each written only where it is not zero; manifest.json gets input_flags; the build command prints a warning. Assertion A10 holds that, per month, the usage cost of costs_month equals the cost in person_month plus the cost that data_quality accounts for.
What a build writes
A build writes one folder per audience, a delivery folder and a manifest:
| Folder | Holds |
|---|---|
central/ |
Pseudonymous person-level data and every grid cell, suppressed ones with their reason; also scenario_persons, scenario_units, scenario_summary, alert_events and named_exceptions. |
manager/<manager_key>/ |
groups_month: the unit cells the manager heads, and every unit cell below them (never the org cell), with a last column hidden and no reason; named_exceptions: count-valued rules for the persons they manage. Each named_exceptions row carries the person_key of the person it names: a manager needs to know who. |
person/<person_key>/ |
person_day, person_month and comparison: only that person's rows, and a banded comparison with their group. A person folder carries no other person's key: its person_month has no manager_key column (central's has one). comparison ends with a hidden column (see Privacy). |
exec/ |
groups_month down to audiences.exec.max_depth, seats_month, pools_month and costs_month with the small-population rule applied, and scenario_summary with the stopped count shown only when both sides are at least k. All five end with a hidden column (see Privacy). |
delivery/ |
directory.csv: the only file with names and emails, for a sender that is out of scope. Never serve it. |
manifest.json |
Inputs, outputs with hashes, the suppression counts, input_flags (what the inputs got wrong without stopping the build) and the unit-envelope keys that matched no unit. It lists every person and manager key through its paths, so it belongs with central/: never deliver it to an audience. |
Every file of the gold folder is readable by its owner only (files 0600, folders 0700, the output folder and any parent the build creates included), whatever the umask of the shell; tests/test_review_f33_output_permissions.py checks it. To hand a folder to an audience, copy it and set the permissions the copy needs.
Reading the CSV files. They are data for programs (the dashboard, a notebook, a warehouse load), not spreadsheets. Text that comes from the organisation files, such as a unit name, a function or a person's name in delivery/directory.csv, is written as it is. A value that starts with =, +, - or @ is taken as a formula by a spreadsheet that opens the file directly. To look at a file in a spreadsheet, import it with every column as text instead of opening it. gold does not rewrite such values, because that would change the data every other reader gets (tests/test_review_f33_csv_formula_cells.py pins both).
Compatibility. A new dataset, a new column appended after the others and a new value in an open list are non-breaking; a column renamed, removed or retyped, a dataset removed or its grain changed needs a new major GOLD_SCHEMA_VERSION. A reader selects columns by name, ignores a column it does not know, fails on a missing one, and reads schema_version (the first column) to know which rule applies. The rule is in the dataset reference.
Who gets which folder
Each role reads the folder of its own audience, never a wider one. <key> is the manager_key or person_key of the reader. meterline_gold.servable(path, audience) says the same for a single file, and the table is a summary of it.
| Role | Folder it gets | Never receives |
|---|---|---|
| Central team, platform lead, FinOps | central/ (and manifest.json) |
delivery/ (names and emails; only the people who run the build see it) |
| Executive | exec/ |
central/, manager/, person/, delivery/ or manifest.json; no per-person row (the exec files hold no person key) |
| Manager | manager/<key>/ |
another manager's folder, central/, exec/, person/, delivery/ or manifest.json; never the organisation cell |
| Individual | person/<key>/ |
another person's folder or key, central/, manager/, exec/, delivery/ or manifest.json |
A folder is copied to its reader; nothing in gold sends it or checks who opened it. The privacy rules are in the section Privacy below.
The example policies
Two policies ship in src/meterline_gold/examples/, both with invented values and both buildable on the simulator's output with prices.example.yaml:
| File | Shows |
|---|---|
policy.example.yaml |
A company with a unit tree: tree.kind: unit, cost bands, exec depth 2, scenarios with caps by level and unit envelopes. |
policy.attributes.example.yaml |
A flat organisation grouped by attributes: tree.kind: attributes with three dimensions (population, function, attrs.location), bands.metric: tokens, the breaker_events_at_least rule and a cap by population. |
To write your own, follow docs/how-to/configure-policy.md: it builds a worked example on the simulator's output and shows what each part produces.
The attributes a tree, a comparison group or a cap can use are function, level, cost_center, population and attrs.<key> for each key in people.attrs_keep.
Fixed values
These are in the code and not in the policy file:
- Ratios are rounded half up to 4 decimals (
premium_share,cache_rate,unreconciled_share, the shares indata_quality). projected_cost_refis rounded half up to 2 decimals.- Token prices are written per million tokens with at most 6 decimals and read as exact per-token decimals (a shift of 6 places). Every other amount has at most 12 decimals.
- A unit tree is followed 64 levels up from a person's unit; a hierarchy deeper than that would lose the ancestors beyond it.
- Group percentiles are nearest-rank at 50, 75, 90 and 99, the top decile is the top tenth of the organisation, and a person's comparison bands cut at the 50th, 75th and 90th percentile of their comparison cell.
Scenarios and alerts
The policy file holds N named shadow scenarios (caps by attribute, a flat cap, unit envelopes, a default cap). For each month the build writes who or which unit would have been stopped, the amount beyond the cap, the top-ups needed and the alert crossings, plus daily-breaker and spike events and the named exceptions of the policy. Nothing is enforced and nothing notifies anyone.
Privacy
The rules that keep a gold folder from exposing a person:
- Persons are never named in a dataset: a keyed hash (HMAC, key from the environment) stands for each of them.
- A group figure is published only for a cell of at least
kpeople. The suppression loop of spec 7.4 suppresses a cell that differs from another published cell by fewer thankpeople. It also suppresses a sibling, or the cell itself, when what remains of a block is underk, with or without the people who know their own figures. - What an empty value means (schema version 1.1). In
exec/costs_month,exec/pools_month,exec/seats_month,exec/scenario_summaryandperson/<key>/comparison, the last columnhiddenistrueexactly when a privacy rule withholds a value of that row. There, an empty covered value on a row withhidden=trueis withheld; an empty value on a row withhidden=false, or in a column the marker does not cover, means none. The marker carries no reason. The exec and managergroups_monthcarry the samehiddencolumn in place ofsuppressedandsuppression_reason; onlycentral/groups_monthkeeps the reason.central/withholds nothing in the other datasets. A gold folder of version 1.0 is rebuilt, never upgraded in place. - A withheld count can sometimes be worked out.
exec/seats_monthwithholdsactive_seatsandidle_seatsseparately and showsseats_billedandseats_on_eval_day. Soseats_billedminus a shownidle_seatsis an estimate of a withheldactive_seats, exact only in simple cases. The rule tests each count againstkon its own and does not stop this. - What a cost figure is.
costs_month(central and exec) ends withcost_basis_label, the words forcost_source_basisnext to the figure:usage value, not the invoiceforconsumption,estimate, not the invoiceforestimate,billed amount, as invoicedforbilled. It also ends withinvoice_usage, the invoiced usage amount from--invoicefor the same provider, organisation and month (the valuereconciliation_monthshows; empty when no invoice row exists). A Copilot usage value of 662.49 and an invoiced 47.49 therefore read as two different things on one row. Inexec/costs_monthboth columns are withheld with the figure they describe. - No file is sorted by a measure, and a person sees their own figures and a band against their comparison group, never another person's figure. A person folder carries no other person's key either.
- Keys are pseudonyms, not anonymous.
person_keyincentral/and in a person folder,manager_keyincentral/, and theperson_keyandmanager_keyof a manager'snamed_exceptionsall stand for a person for as long as the hashing key exists. A manager's folder lists the persons a rule names because a manager needs to know who (names are joined at delivery, fromdelivery/directory.csv).manifest.jsonlists every key through its paths and is a central file. - Keys rotate with the key. A new
METERLINE_GOLD_KEYchanges everypk_,ak_andkc_value of a rebuilt gold folder, so old and new folders no longer join. It does not change the short unkeyed sha256 labels that the connectors write into their raw folder names and step names (the LiteLLM entity tag, the Copilot login tag): an identifier that can be guessed, such as an email address, can be confirmed against them. Keep raw folders as closed as silver, and delete them when the labels must go. - The key and the policy's thresholds stay out of git:
.envfiles are ignored, and a test scans every tracked file for a key-shaped value. tests/holds a property test that tries to recover suppressed cells with an exact linear attacker and must fail, and named tests that pin each boundary of the rule atk= 2 andk= 5.
What disappears under suppression
Take k = 5, an organisation of three units, and a head in each unit who knows their own figure:
| Unit sizes | What the files show |
|---|---|
| 4, 6, 8 | The unit of 4 is suppressed with reason k: its row stays, its figures are empty. The unit of 6 is also suppressed, reason secondary: the org cell minus the units of 6 and 8 would give the unit of 4 away. The unit of 8 and the org cell are published. |
| 5, 6, 8 | The unit of 5 is suppressed, reason k: without its head, 4 persons remain. The unit of 6 is suppressed, reason secondary. The unit of 8 is published. |
| 6, 6, 8 | Nothing is hidden: 6 persons leave 5 once the head is taken out. |
The same unit sizes without a head who knows their figure (5, 6, 8) hide nothing. Only central/groups_month shows suppressed and the reason. The exec and manager groups_month show the same cells with a last column hidden (true for a suppressed cell) and no reason. A reader there learns that a cell is hidden for privacy, not that it is below k.
Known limits
The limits of gold, grouped by kind. The privacy rules above hold inside these limits.
What the privacy rules do not cover. Suppression is exact against a reader who adds and subtracts published figures, within one build and one month. It does not cover:
- Two builds of one month. A late correction to silver or
people.csvcan make a published cell differ between an earlier and a later build by one person's amount, and a reader who holds both builds learns that amount. Deliver each complete month once, and do not hand an audience two builds of the same month (spec 7.4, limits). - Outside knowledge. A reader who knows something the files do not say, such as one colleague's usage, is not modelled.
- The hashing key. Anyone who holds
METERLINE_GOLD_KEYcan compute the key of any person or actor whose id they know (aperson_idofpeople.csv, or a provider's actor id), and so find their rows. Keep the key as closed asdelivery/. - One tree. Additive figures follow one tree only (units, or one set of attributes). A second cross-cutting hierarchy with costs could reveal a person through its margins, so v1 offers none (spec section 15, card 2).
How a build behaves. These follow from how the build writes its output:
- A leftover temporary folder stops the next build. A build writes into
<out>.tmpand renames it at the end. A build that was killed can leave<out>.tmpbehind; the next build to the same--outthen stops with exit 2 ("a leftover temporary folder is in the way"). Delete the.tmpfolder and build again. - No delivery. Gold writes
delivery/directory.csvfor a sender that is out of scope: nothing sends a folder to its audience or checks who opened it.
Not verified. Gold has run on the hand-built fixture and on simulated data only, never on a real organisation's silver.
The example dashboard
src/meterline_gold/dashboard/index.html is one static file with no external request and no authentication. Serve one audience folder as the web root, with the file copied into it:
cp src/meterline_gold/dashboard/index.html gold/exec/
cd gold/exec && python3 -m http.server 8000
It shows each file as a table in the file's own row order, offers no sort on a measure and computes no figure. In a file with a hidden column it shows an empty covered value of a hidden = true row as hidden, and leaves an empty value of a hidden = false row blank. Never serve the gold root or delivery/.
A file with a month column opens on its latest month; a selector next to the table shows another month or all of them (this filters the rows shown, nothing is recomputed). The chart under the table opens on a cost column (total_cost_ref, usage_cost_ref or cost_ref, whichever the file has), never on schema_version. When the table is wider than the screen, the two columns after schema_version stay in view as it scrolls sideways (not on a screen narrower than 700 pixels).
The page carries a content security policy (a <meta> tag): no request leaves its own folder, and the only script and style allowed are its own inline ones, named by their sha256 hash. An edited script or style is blocked by the browser until the hash in the tag is updated; tests/test_review_f33_dashboard_csp.py fails and prints the value it expects. A web server that sets its own Content-Security-Policy header should send the same policy.
The page aims at WCAG 2.2 AA. It has a header and a main landmark, a name on each control and scroll region, and a caption and column headers on each table. It announces the file shown after a tab change, and its borders and bars reach 3:1 contrast in both colour schemes. tests/test_review_f43_dashboard_a11y.py checks these in the page source. A browser run of axe-core on the fixture build found no violation on 2026-10-17; no browser check runs in CI yet.
Tests
python -m pytest -q packages/meterline-gold
The done-checks of spec section 14 are the tests named test_done_check_G<NN>_*. They use only the hand-built fixture of docs/gold-fixture/ and simulator output.
Metadata
Release files for meterline-gold 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| meterline_gold-0.1.0.tar.gz | 173.0 kB | With provenance | Details for meterline_gold-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| meterline_gold-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for meterline_gold-0.1.0-py3-none-any.whl |
Total release size: 275.0 kB
Release files / meterline_gold-0.1.0.tar.gz
| Download URL | meterline_gold-0.1.0.tar.gz |
|---|---|
| Size | 173.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bc6cacb2910945d6974215640fedf21bbd1e73d3458f6eb36cd1f4b9b371b900
|
|
BLAKE2b-256 checksum How to use checksums |
933c85e69e088f8771590eff340f577a6299b6b22a536b36e5c672f2e4f7be8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logRelease files / meterline_gold-0.1.0-py3-none-any.whl
| Download URL | meterline_gold-0.1.0-py3-none-any.whl |
|---|---|
| Size | 102.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f320845935892e434c90e964e79c0cd3b1a190636d81560a3181828a977f4514
|
|
BLAKE2b-256 checksum How to use checksums |
50f1022bf80a06685548429540cc5404b9690e29827c292df5c8cc8702806be5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logSecurity for meterline-gold 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for meterline_gold-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.