Waypoint Ledger

For developers, agencies and researchers

Price a diagnostic journey with one request

Send a sentence a person actually wrote. Get back every unit of care it names, priced and cited.

No key, no account, no sign-up. Every route below is open. GET routes send access-control-allow-origin: *, so a browser app can call them directly. The price table is version 2026-09-09.1: 274 rows, 251 of them priced and addable, each with the file it was read from.

On this page — 6 sections

Price a story

Every response carries the published federal figure that prices each unit of care, and the year, basis, population and source URL behind that figure — so whatever you build can show any number it prints. A deterministic matcher maps words to a unit of care; a published table prices it. No model produces a dollar figure.

1 · The call the site itself makes

One request, one journey, every line traceable.

BASE=https://waypoint-ledger.pages.dev

curl -s -X POST $BASE/api/price \
  -H 'content-type: application/json' \
  -d '{"story":"saw my regular doctor three times, then a cardiologist,
        an echocardiogram, blood work twice, and I missed work"}'

Real response. Long coverage statements and three further segments are elided, marked with an ellipsis key:

{
  "ok": true,
  "input": "story",
  "segments": [
    {
      "raw": "saw my regular doctor three times",
      "times": 3,
      "itemId": "cms-99213",
      "label": "Doctor's office visit, established patient, low complexity",
      "confidence": "DERIVED",
      "matchedOn": "saw my regular doctor",
      "matchScore": 100,
      "valueUsd": 95.19,
      "outOfPocketUsd": null,
      "lineTotalUsd": 285.57,
      "basis": "allowed",
      "attribution": "gross",
      "year": "2026",
      "geography": "United States, national (geographic practice cost indices set to 1.000)",
      "population": "Medicare Part B fee-for-service beneficiaries",
      "coverage": "A follow-up visit with a doctor you have seen before, for a straightforward problem. HOW THIS NUMBER WAS MADE: …",
      "sourceTitle": "CMS, CY2026 National Physician Fee Schedule Relative Value File (RVU26C, July release, published 2026-06-30)",
      "sourceUrl": "https://www.cms.gov/medicare/payment/fee-schedules/physician/pfs-relative-value-files/rvu26c",
      "code": "CPT 99213",
      "summable": true
    },
    {
      "…": "3 more segments, each with its own source"
    }
  ],
  "unpriced": [
    {
      "raw": "I missed work",
      "kind": "known-unpriceable",
      "unpriceableId": "lost-work",
      "reason": "Valuing a lost workday requires your actual earnings, not a national median. The Bureau of Labor Statistics pu …",
      "whatWouldFixIt": "Ask the person for their own hourly or weekly pay and multiply. That is arithmetic on a figure they supplied, …"
    }
  ],
  "totals": {
    "totalUsd": 675.2,
    "outOfPocketUsd": 0,
    "outOfPocketReported": false,
    "pricedCount": 4,
    "unpricedCount": 1,
    "basesUsed": [
      "allowed"
    ]
  },
  "basisWarning": null,
  "conflicts": [],
  "nonSummable": [],
  "excludedFromTotal": [],
  "bundlingNote": null,
  "tableVersion": "2026-09-08.1-verified",
  "method": "A deterministic matcher maps words to a unit of care; a published federal table prices it. No model produces a dollar figure."
}

What to notice. matchedOn shows the exact words in the table the phrase matched, so the mapping is never a black box. “I missed work” comes back in unpriced with the reason it will not be priced and what would fix it — valuing a lost day needs that person’s own pay, not a national median. And confidence: "DERIVED" says this figure was computed from two numbers read in the CMS file, not lifted from a dollar column that does not exist.

Nine more worked calls

2 · Read one unit of care, with its provenance

Every row carries who it describes, who it does not, and the rules about adding it to anything else.

curl -s $BASE/api/table/cms-99213
{
  "ok": true,
  "version": "2026-09-08.1-verified",
  "item": {
    "id": "cms-99213",
    "label": "Doctor's office visit, established patient, low complexity",
    "valueUsd": 95.19,
    "basis": "allowed",
    "year": "2026",
    "population": "Medicare Part B fee-for-service beneficiaries",
    "coverage": "A follow-up visit with a doctor you have seen before, for a straightforward problem. HOW THIS NUMBER WAS MADE: CMS does not publish a dollar column in …",
    "sourceTitle": "CMS, CY2026 National Physician Fee Schedule Relative Value File (RVU26C, July release, pub …",
    "sourceUrl": "https://www.cms.gov/medicare/payment/fee-schedules/physician/pfs-relative-value-files/rvu26c",
    "confidence": "DERIVED",
    "code": "CPT 99213",
    "rules": {
      "summable": true,
      "mutuallyExclusiveWith": [],
      "bundlesAncillaries": false,
      "alternates": {
        "…": "5 alternate measures of the same service (a CY2024 average allowed amount, an average submitted charge, the hospital outpatient facility fee), each with a note saying why it is an alternative and never an addition"
      }
    }
  }
}

What to notice. rules.summable and rules.mutuallyExclusiveWith are machine-readable, because a rule written only in prose is a wish. Ask /api/price for a whole-year figure alongside per-visit lines and it comes back priced and flagged in excludedFromTotal, with the conflict named: the year already contains the visits, so adding them counts the same care twice. The figure is never quietly dropped and never quietly summed.

3 · Save a journey and get a link back

Anonymous by default. A journey holds units of care, counts and the words the person typed. Nothing else.

curl -s -X POST $BASE/api/journeys \
  -H 'content-type: application/json' \
  -d '{"title":"Two years of looking",
       "entries":[{"raw":"saw my regular doctor about the fatigue","itemId":"cms-99214","times":6},
                  {"raw":"echocardiogram","itemId":"cms-img-echo","times":1}]}'

Real response — the url echoes the host you called; this run was against a local instance of this code:

{
  "ok": true,
  "id": "91dec6a1-5886-417d-9078-c842aaa1d93c",
  "slug": "7nvjqbdhmf",
  "url": "http://localhost:8792/ledger?s=7nvjqbdhmf",
  "savedTo": "link"
}

GET /api/journeys/{slug} reads it back with the table version it was priced against, so a link opened next year still says which edition of the table it came from.

4 · Price it for a person, not for the country

Add coverage and state (or a CMS locality key) and the response carries the same answer the site shows that person: whether the published figure describes them, which figure applies instead if it does not, and what that service costs from the cheapest CMS locality to the dearest.

curl -s -X POST $BASE/api/price \
  -H 'content-type: application/json' \
  -d '{"story":"saw my regular doctor three times, then an echocardiogram",
       "coverage":"uninsured", "state":"IA"}'

Real response. Long fields already shown above are elided with an ellipsis key:

{
  "ok": true,
  "input": "story",
  "context": {
    "coverage": "uninsured",
    "locality": {
      "key": "IA-00",
      "name": "Iowa",
      "state": "IA",
      "stateName": "Iowa",
      "mac": "05102",
      "workGpci": 1,
      "practiceExpenseGpci": 0.915,
      "malpracticeGpci": 0.397
    },
    "localityFrom": "state",
    "figureBasis": "Medicare allowed amounts for Iowa (CMS locality IA-00), CY2026 fee schedule formula"
  },
  "segments": [
    {
      "raw": "saw my regular doctor three times",
      "times": 3,
      "itemId": "cms-99213",
      "valueUsd": 95.19,
      "lineTotalUsd": 285.57,
      "…": "basis, year, population, coverage, sourceTitle and sourceUrl as before",
      "localityUsd": 89.23,
      "localityName": "Iowa",
      "nationalUsd": 95.19,
      "fit": {
        "verdict": "BILLED AGAINST THIS",
        "why": "With no insurance you are billed the provider’s charge, not an allowed amount. This is the average charge submitted for this same service in CY2024.",
        "figureUsd": 189.25,
        "figureNote": "Average submitted charge, CY2024 · billed charge, never added to an allowed amount",
        "which": "charge",
        "offerGap": false,
        "lineTotalUsd": 567.75
      },
      "localityRange": {
        "nationalUsd": 95.19,
        "lowUsd": 86.86,
        "lowLocalityKey": "AR-13",
        "lowLocalityName": "Arkansas",
        "highUsd": 120.13,
        "highLocalityKey": "CA-65",
        "highLocalityName": "San Jose-Sunnyvale-Santa Clara (San Benito County)",
        "localityCount": 109,
        "formula": "(work RVU x work GPCI + non-facility PE RVU x PE GPCI + MP RVU x MP GPCI) x 33.4009"
      }
    },
    { "…": "1 more segment, priced and fitted the same way" }
  ],
  "fitted": {
    "totalUsd": 1258.3,
    "suppressedReason": null,
    "describedCount": 2,
    "notDescribedCount": 0,
    "figureKindsUsed": ["charge"],
    "labels": {
      "primary": "What providers billed on average for these services",
      "secondary": "What the published Medicare figures add up to in Iowa"
    },
    "verdicts": [{ "verdict": "BILLED AGAINST THIS", "lines": 2 }],
    "basisWarning": null
  },
  "totals": { "totalUsd": 482.3, "pricedCount": 2, "basesUsed": ["allowed"], "…": "" },
  "tableVersion": "2026-09-08.1-verified"
}

What to notice. valueUsd is still the published national figure and never moves. localityUsd is what Medicare allows in Iowa for the same code, from CMS’s own formula and the three geographic indices echoed in context. And because this caller is uninsured, fit.figureUsd is neither of those — it is the average charge providers submitted, because a charge is what an uninsured person is billed against. Three different true numbers for one line, each labelled with what it is.

5 · When nothing published describes the person

Medicaid rates are set by each state and are in no national dataset. Roughly one in five Americans is on Medicaid, and they are over-represented in exactly the population this tool is built for. So the API does not return a zero.

curl -s -X POST $BASE/api/price \
  -H 'content-type: application/json' \
  -d '{"story":"saw my regular doctor three times, then an echocardiogram",
       "coverage":"medicaid", "locality":"TX-18"}'
"fitted": {
  "totalUsd": null,
  "suppressedReason": "No published federal figure describes this person on any line here, so there is no total to report. This is a gap in the published data, not a cost of zero. The lines carry what each figure is and who it does describe, and every one of them can be counted at POST /api/gap.",
  "describedCount": 0,
  "notDescribedCount": 2,
  "figureKindsUsed": [],
  "verdicts": [{ "verdict": "NOT DESCRIBED", "lines": 2 }]
}

totalUsd is null, never 0, because a zero would be read as a price. Each line still comes back with the Medicare figure, what it is, and who it does describe. The honest output here is a counted gap, and POST /api/gap is where it goes.

6 · Errors are sentences, and a value we cannot honour is never ignored

Before this round the API took a state, ignored it, and returned the national figure with no warning. It now refuses, and the refusal tells you what to send.

# a state with more than one CMS payment locality
curl -s -X POST $BASE/api/price -H 'content-type: application/json' \
  -d '{"story":"a doctor visit","state":"TX"}'

{"ok":false,"error":"Texas has more than one CMS payment locality, so a state is
not enough to price a line. Send one of: TX-31 (Austin), TX-20 (Beaumont),
TX-09 (Brazoria), TX-11 (Dallas), TX-28 (Fort Worth), TX-15 (Galveston),
TX-18 (Houston), TX-99 (Rest Of Texas)."}

# a coverage we do not have a rule for
{"ok":false,"error":"coverage must be one of: employer, marketplace, medicaid,
medicare, uninsured, unsure. Send no coverage at all and every line comes back
as a reference price."}

7 · Take all 5,668 locality figures, with the inputs that made them

52 CMS Physician Fee Schedule codes priced for each of the 109 Medicare payment localities. Every row carries the three RVU components, the three geographic indices, the conversion factor, the formula written out, and the SHA256 of both CMS files it came from — so a state health department can filter it to their own state and recompute every figure without us.

curl -s $BASE/data/locality-prices.csv -o locality-prices.csv

head -1 locality-prices.csv
price_id,code,label,state,locality,locality_key,locality_name,mac,work_rvu,
pe_nonfacility_rvu,mp_rvu,pw_gpci,pe_gpci,mp_gpci,conversion_factor,allowed_usd,
national_usd,pct_of_national,formula,basis,year,population,locality_audit_status,
national_audit_status,rvu_file,rvu_file_sha256,gpci_file,gpci_file_sha256,table_version

grep '^cms-img-echo,.*,TX-18,' locality-prices.csv | cut -d, -f1,6,7,16,17
cms-img-echo,TX-18,HOUSTON,197.09,196.73

Also as JSON with the formula, the sources and the audit; columns explained in locality-dictionary.csv. The generator is the audit: node scripts/gen-locality-table.mjs recomputes all 5,668 from the published RVUs and indices and exits non-zero on one cent of drift.

8 · Ask for one place, not for the whole country

A national Medicare figure is not what anyone is charged. Send ?locality= or ?state= to any read route and every row CMS prices geographically comes back as that place’s allowed amount, with the three relative value units, the three geographic indices and the formula that produced it — enough to re-derive the number without this API. valueUsd is never overwritten, so the national figure and the local one can never be confused for each other.

# one row, priced where the person actually lives
curl -s "$BASE/api/table/cms-99213?locality=IA-00"

# the whole table for one place — or a state, where CMS gives it a single locality
curl -s "$BASE/api/table?locality=IA-00"
curl -s "$BASE/api/table?state=IA"

# the index of every locality, and every figure published for one of them
curl -s $BASE/api/localities
curl -s $BASE/api/localities/IA-00

Real response, one field elided and marked:

{
  "ok": true,
  "locality": {
    "key": "IA-00", "name": "Iowa", "state": "IA", "stateName": "Iowa",
    "mac": "05102", "workGpci": 1, "practiceExpenseGpci": 0.915, "malpracticeGpci": 0.397
  },
  "item": {
    "id": "cms-99213",
    "label": "Doctor's office visit, established patient, low complexity",
    "code": "CPT 99213",
    "valueUsd": 95.19,
    "nationalUsd": 95.19,
    "localityUsd": 89.23,
    "localityGeography": "Iowa (CMS payment locality IA-00, Medicare Administrative Contractor 05102)",
    "localityFormula": {
      "code": "99213",
      "text": "(work_rvu*pw_gpci + pe_nonfacility_rvu*pe_gpci + mp_rvu*mp_gpci) * 33.4009",
      "conversionFactor": 33.4009,
      "parts": [
        { "name": "Work",             "rvu": 1.3,  "gpci": 1,     "product": 1.3 },
        { "name": "Practice expense", "rvu": 1.46, "gpci": 0.915, "product": 1.3359 },
        { "name": "Malpractice",      "rvu": 0.09, "gpci": 0.397, "product": 0.03573 }
      ],
      "rvuSum": 2.67163,
      "total": 89.23
    },
    "…": "every other field of the row, unchanged"
  },
  "localityRange": {
    "nationalUsd": 95.19,
    "lowUsd": 86.86,  "lowLocalityKey": "AR-13", "lowLocalityName": "Arkansas",
    "highUsd": 120.13, "highLocalityKey": "CA-65",
    "highLocalityName": "San Jose-Sunnyvale-Santa Clara (San Benito County)",
    "localityCount": 109,
    "formula": "(work RVU x work GPCI + non-facility PE RVU x PE GPCI + MP RVU x MP GPCI) x 33.4009"
  }
}

A place we cannot honour is a 400, never a silent national fallback. That was the defect worth fixing: an answer to a question nobody asked, returned with no warning, is wrong and looks right.

# a locality that does not exist — refused, never quietly national
{"ok":false,"error":"No CMS locality has the key \"IA-99\". A key is the two-letter
state and the two-digit CMS locality number, like \"IA-00\" or \"TX-31\". All 109 are
published at /data/locality-prices.json."}

# a state CMS splits into several — refused, and it names them
{"ok":false,"error":"Texas has more than one CMS payment locality, so a state is not
enough to price a line. Send one of: TX-31 (Austin), TX-20 (Beaumont), TX-09
(Brazoria), TX-11 (Dallas), TX-28 (Fort Worth), TX-15 (Galveston), TX-18 (Houston),
TX-99 (Rest Of Texas)."}

9 · Take the whole thing, and the catalog that describes it

/data.json is a DCAT-US v1.1 catalog — the metadata standard data.gov harvests — describing the price table, the locality table, the three register exports and the dictionary, each with its licence, its distributions, its data dictionary and the federal files it was built from. It validates against the government’s own published JSON Schema; the schema is vendored in this repository with its SHA-256 and the test re-hashes it before it validates anything.

The application itself is downloadable in one file. Not a description of a repository: the source, the data, the migrations, the tests and a README you can run from.

# the catalog, in the shape data.gov harvests
curl -s $BASE/data.json

# the whole application — source, data, migrations, tests, and a README to run from
curl -sL $BASE/waypoint-ledger-source.tar.gz | tar -xz && cd waypoint-public
npm install && npm test
npm run dev

The code is Apache-2.0; the data is CC0 1.0, public domain — NOTICE says which is which, line by line. The five DCAT-US fields that belong to federal agencies (bureauCode, programCode, dataQuality, primaryITInvestmentUII, systemOfRecords) are absent from our catalog, as the standard’s own guidance directs for a non-federal publisher. We are not an agency, and an OMB bureau code we do not have would be exactly the kind of invented federal number this project exists to refuse.

10 · A correction, addressed to the body that published the number

A thumb on a ledger line is bound to one published federal row. This is that row’s provenance and the public counts in one call — no bundle, no join, no account. Add ?format=text and there is nothing to parse.

curl -s $BASE/api/citation/cms-99213?format=text
PUBLIC CORRECTION REPORT — Waypoint Ledger

Published figure: Doctor's office visit, established patient, low complexity
Row identifier:   cms-99213  ·  code CPT 99213
Published value:  $95.19 (2026)
Basis:            allowed (the negotiated or fee-schedule amount)
Geography:        United States, national (geographic practice cost indices set to 1.000)
Population:       Medicare Part B fee-for-service beneficiaries
Published by:     Centers for Medicare & Medicaid Services
Source:           CMS, CY2026 National Physician Fee Schedule Relative Value File (RVU26C, July release, published 2026-06-30)
Source URL:       https://www.cms.gov/medicare/payment/fee-schedules/physician/pfs-relative-value-files/rvu26c

What the public said about this row: 0 responses — 0 say the figure describes them, 0 say it does not.
Median amount respondents said they actually paid: none reported.

Sample: self-selected members of the public using a free tool. Counts are reported exactly as entered — no weighting, no imputation, no extrapolation to a population.
Counted as of: 2026-09-09
Method and every source: https://waypoint-ledger.pages.dev/method

Without ?format=text the same call returns JSON with the figure, the publisher, the document, the source URL and the counts as separate fields, plus this block in text. Every row of the table answers, including the rows nobody has spoken about yet.

What you can take without asking

The register is public: what people said a federal figure got wrong, what care never entered any claims file, and how the people who carried the burden ranked it. Counts and a de-identified CSV, not free text.

The corrections export is a defect report, not a comment box. Every row carries the price row id, the CPT or HCPCS code (and the LOINC code where the row is a lab), the published figure with its basis, year, geography and population, the federal file by name with its URL, its SHA-256 and the day we read it, the exact line or arithmetic the figure was re-read from, the audit verdict, the direction of the correction, the counts and fit rate on that figure, the chained row hash, and a permalink that renders it as a paste-ready report for the agency that published the number. The first line of the CSV is a comment naming the price-table version and the audit; /api/export/corrections.json publishes the same rows with the column contract beside them. Free text is never exported.

DataJSONCSV
Everything, one call/api/register
The whole price table, with provenance/api/tableprice-table.csv
All 5,668 CMS locality figureslocality-prices.jsonlocality-prices.csv
One CMS payment locality, priced/api/localities/{key}
The open-data catalog (DCAT-US v1.1)/data.json
The application itself, to run yourselfwaypoint-ledger-source.tar.gz
The licence, as a file counsel can readLICENSENOTICE
One figure’s provenance and counts/api/citation/{id}as plain text
The hash-chain head of every count/api/integrity
Corrections to published figures/api/correctionscorrections.csv · corrections.json
Care that no claim recorded/api/gapgap.csv
How people ranked the burdens/api/surveysurvey.csv
Written interviews (count only, ever)/api/interview
What changed because someone said so/api/changes

Every route

MethodPathWhat it does
GET/api/healthService and table version.
POST/api/healthProve the database accepts a write, not just a read.
GET/api/tableThe whole price table with provenance.
GET/api/table/{id}One unit of care.
GET/api/localitiesThe 109 places Medicare prices separately.
GET/api/localities/{key}One locality, and every figure published for it.
POST/api/pricePrice a story, or a list of units of care.
POST/api/mapAI reads a story into units of care; the published table prices them.
POST/api/fhirA ledger as an HL7 FHIR R4 collection Bundle.
GET/api/fhir/exampleThe demo journey, already converted.
POST/api/journeysSave a journey and get a link back.
GET/api/journeys/{id}Read a shared journey.
PUT/api/journeys/{id}Replace the lines of a journey you own.
DELETE/api/journeys/{id}Delete a journey — with the session that owns it, or with the delete token it was saved with.
GET/api/registerEvery public count in one call.
GET/api/correctionsCounts of right and wrong per federal figure.
POST/api/correctionsSay a published figure is right or wrong for you.
GET/api/gapThe measured shape of what claims data cannot see.
POST/api/gapReport care you needed and did not get.
GET/api/surveyThe community’s ranking of which burden weighed most, with its N.
POST/api/surveyRank the five burdens.
GET/api/interviewHow many written interviews have been received. Nothing else, ever.
POST/api/interviewSend a written interview.
GET/api/changesThe published change log: what someone said, and what changed because of it.
GET/api/integrityThe hash-chain head of every public count.
GET/api/citation/{id}One published figure, its provenance and what the public said about it.
GET/data/locality-prices.csvEvery CMS payment locality figure behind this tool, as CSV.
GET/data/locality-prices.jsonThe same 5,123 figures as JSON, with the formula, the sources and the audit.
GET/data/price-table.csvThe whole national price table as CSV, with provenance and audit verdict.
GET/api/export/{kind}.csvThe de-identified register as CSV.
GET/api/export/corrections.csvCorrections to published federal figures, joined to the file each figure came from.
GET/api/export/corrections.jsonThe same corrections file as JSON, shaped as a DCAT distribution, with its column contract.
GET/api/openapi.jsonThis description.
POST/api/auth/register/optionsBegin creating a passkey.
POST/api/auth/register/verifyFinish creating a passkey and start a session.
POST/api/auth/login/optionsBegin signing in with a passkey.
POST/api/auth/login/verifyFinish signing in; sets the session cookie.
POST/api/auth/logoutEnd the session and clear the cookie.
GET/api/meWho the session belongs to, or null.
PUT/api/meSet a display name.
DELETE/api/meErase the account and everything it owns.
GET/api/me/correctionsThe corrections this account has sent.
GET/api/me/journeysThe journeys saved to this account.

The full description, with every request and response schema, is at /api/openapi.json. Administrative routes take a bearer token and are not published there.

The rules

  • No model, average or interpolation ever produces a dollar figure. A phrase that maps to nothing stays unpriced and is returned as unpriced, with the reason.
  • Figures that cannot be added are not added. Measures that answer different questions raise basisWarning; a whole-year figure is returned outside the total in excludedFromTotal.
  • Rate limits, per network per hour: 300 to /api/price, 30 to /api/journeys, 20 to each write route. Over the limit returns 429 with a sentence, not a code.
  • Privacy is the precondition. No account is required and none is offered by these routes. No IP address is stored; rate limiting hashes it with a daily salt and forgets it within the hour. Free text sent to the register is held privately and is never served back or exported.
  • Errors are sentences. Every failure is { ok: false, error } with the right status, written for a person reading a log.

Why reuse this

The hard part of a cost-of-illness number is not arithmetic. It is knowing which published figure applies, what it measures, who it leaves out, and what it must not be added to. That work is in the table and its rules, and this API hands you all of it with every response. A state health agency costing a care pathway, a patient organization building its own tool, a researcher who needs the same unit prices we used — none of you need our interface, and none of you should have to take our word for a number.

If you want to stand the whole thing up for your own condition, your own state or your own population, /adopt is the procedure: the files to edit, in order, and the two commands that refuse a figure which does not reproduce. The application is a single download and runs on your machine in three commands, so none of that is a promise you have to take on trust.

Questions, or a row you think is wrong: bo@precisionfederal.com.