Vendor listing — add a provider to the catalog
A listing has three parts, and all three ship in the same PR:
- Registry entry (
src/treg/oauth_providers.py) — how a team connects a credential for the
provider, and how treg verifies that credential is real.
- Core catalog file (
src/treg/catalog/<service>.yaml) — what an agent can do: 8–15
curated endpoints with capability mapping, inputs, cost + provenance, and verified examples.
- Platform-key slot (
config.py + render.yaml + fx.yaml) — so treg can serve the
endpoints on its own key (tier 4). Always aim for this. A listing that is BYOK-only is the
exception and needs a stated reason (no self-serve pricing, own_account data, sales-gated).
And every listing PR carries a verification evidence ledger (Step 7b). No ledger, no merge.
Deep references (read before non-trivial work; do not duplicate them here):
docs/context/guides/expanding-a-category.md — the add-a-provider playbook, verify toolbox, traps
docs/context/architecture/catalog.md — catalog schema, cost provenance, verify pipeline, PII rules
docs/VENDORS.md — what we told the vendor to prepare (their checklist)
src/treg/web/vendor-listing.md — the HOSTED instructions (served at /vendor-listing) that a
vendor's own coding agent follows to raise a listing PR; the dashboard's "List as vendor" modal
(connections view, vendorAsk in index.html) hands vendors a prompt pointing at it. Keep the
three vendor-facing surfaces (doc, hosted page, modal prompt) telling one story.
Step 0 — intake: collect the vendor facts
Before touching code, you need ALL of these. If the vendor's submission is missing any, ask —
do not guess ("unconfirmed" beats a wrong path shipped):
- A contact email for the vendor's team — required in the PR/issue description. It is how a
test credential gets arranged for live verification; without it the listing stalls at step 7.
service id (lowercase slug), display name, one-line summary (what an agent can DO)
base_url (exact API root)
- Auth: where the key rides (header name + format, or query param name). Key in the URL path is
not supported — decline or defer.
- A free or near-free probe endpoint where a valid key returns 2xx and an invalid key does NOT
— plus the exact bad-key behavior (status code, or the JSON field that signals invalid)
- Pricing page URL, per-endpoint prices, and the billing model (
per_call / per_success /
per_result / credits / quota). Machine-readable rate-card endpoint if they have one.
- Docs URL; OpenAPI spec URL if published
- The 8–15 endpoints they consider their core surface, with example parameter values
- A test credential (or credits grant) for verification — read it from env only, never write it
into any file
Step 1 — eligibility gate
Reject decisively, with a recorded reason, when:
- The key cannot be validated (API returns success for garbage keys) — e.g. ScrapeCreators
- Key rides in the URL path (
/v3/{key}/…) — injectors do header/query only
- Sales-gated signup (no self-serve key breaks the fast path)
- Legal/shutdown risk, or deprecated/absorbed products
Step 2 — registry entry
Add an OAuthProvider(auth_kind="key", …) in oauth_providers.py and append it to REGISTRY.
Model it on HUNTER (a clean key provider). Pick the verify fields from the toolbox table in
expanding-a-category.md (token_header/token_format, token_location="query"+token_param,
probe_url, probe_method+probe_json, token_verify_field, token_ok_field+token_ok_value,
token_reject_field, probe_reject_statuses, …). Prefer a header over a query key so the secret
never lands in a logged URL. Set category (add to CATEGORY_ORDER only if genuinely new),
summary, base_url, docs_url, probe_path, and setup_url/setup_steps so a user can find
their key.
Provider-required constant headers (Crustdata's x-api-version: 2025-11-01): declare them in
required_headers=(("name", "value"),) on the OAuthProvider and in the providers.py CATALOG
row — never in the proxy. They become constant-format bindings. Trap (PR #191): a binding whose
format has no {secret} must NOT be fed to _secret_renderings — otherwise the literal
value (a date!) joins the redaction set and gets masked out of error evidence. Check the
constant does not appear in _secret_renderings' output, and add a test.
Step 2b — platform-key slot (do this for every listing)
The tier-4 wiring is part of the listing, not a follow-up:
src/treg/config.py: platform_key_<service>: str = "" with a one-line comment (auth shape,
what a top-up buys). Pairs (key+secret) use platform_extra_setting; see Tomba.
render.yaml: - key: TREG_PLATFORM_KEY_<SERVICE> + sync: false + a comment. No value.
src/treg/catalog/fx.yaml credit_rates_usd: the USD-per-credit treg actually pays, with
basis naming the real top-up/receipt (not the pricing page's headline tier), source, checked.
- A test asserting
cat.platform_eligible(ep) for every endpoint in the file (see
test_crustdata_and_aviato_catalogs_are_platform_priced); every cost must therefore be
confidence: documented|verified with a computable USD figure.
- If the platform key also needs a constant header,
_platform_bindings must carry it — assert
the tier-4 binding list equals BYOK's (see test_crustdata_platform_key_keeps_the_required_version_header).
- Hand the env value to Jason out-of-band (a file, never the PR, never chat if avoidable) with
the
TREG_PLATFORM_PROVIDERS allow-list entry. Setting it in Render is his ops decision; the PR
just makes it possible.
Modal pricing (preview/rescrape/email riders, bulk-per-record) that one scalar can't express: keep
the numbers in the YAML cost block (note today; a modifiers block if you add one) and read
them generically — do not hardcode credit arithmetic in api.py per provider (#191 debt).
Step 3 — logo
src/treg/web/logos/<service>.svg — a neutral lettermark, not the real brand mark.
test_every_provider_has_a_logo fails without it.
Step 4 — tests
- Add the id to
test_every_provider_is_registered (test_oauth_providers_m3)
- Add it to the offerable loop in
test_key_providers
Step 5 — LIVE bogus-key test (load-bearing; never skip)
Start the server, POST /connections/token with a garbage key against the real API:
422 "rejected …" → correct. Ship it.
200 → the probe does not validate the key → fix the verify fields or drop the provider.
404/502 in the reason → wrong probe path/host → fix base_url/probe_path.
Never ship a key provider you haven't watched reject a bogus key. Use a throwaway org
(e2e-…@treg.local) and delete it after. Watch for the known traps: trailing-slash 307 (put the
slash in probe_path), 200-with-error-body (read a body field), CSV/text responses.
Step 6 — core catalog YAML
src/treg/catalog/<service>.yaml, following the schema in catalog.md. In order:
- Ingest from OpenAPI if published (never hand-transcribe paths); else from docs with
source.openapi: null.
- Select ~8–15 endpoints; ALWAYS include ones matching capabilities other providers already
implement (overlap enables comparison).
- Map each to a capability from
capabilities.yaml. Search the taxonomy AND the other
provider files for the job first; reuse an existing id, never a near-duplicate. Missing jobs go
under proposed_capabilities: in the provider file, not straight into the shared taxonomy —
and if another provider's endpoint already does the job unmapped, name it in the PR so the
reviewer attaches both to the new capability.
- Describe
input (param names, types, required, location; constraints into note).
- Cost with full provenance:
type/value/currency/per/unit + source/source_url/checked/ confidence. Unknown price → value: null + confidence: unknown + a note. Prefer a
rate-card endpoint (source: rate_card_api) over a pricing page.
- test_request per endpoint — CHEAP: smallest limit, one item, public well-known target.
⚠️ Never probe with empty params "expecting a validation error": a no-required-params endpoint
returns its full default result set and bills for it (the Moz quota trap).
Step 7 — verify, scrub, validate
TREG_CATALOG_CRED='<secret>' uv run --frozen python scripts/catalog_verify.py <service>.yaml
uv run --frozen python scripts/catalog_validate.py # must exit 0
uv run --frozen python -m pytest -q
- Stamp
verified: only on endpoints that PASSED with a real target that returned real data and
billed the documented amount. Docs lie; documented ≠ verified. Three stamp traps from #191:
- Placeholder path params (
urn:li:activity:0000…, id: 0): providers return an empty
2xx for 0 credits. That proves the route exists, nothing else. Use a real id harvested from a
sibling endpoint's response and observe the charge once.
- Preview / free modes (
preview=true) as the test_request: the free path is verified, the
paid price is not. Either observe the paid path once, or write "hit price unobserved" in the
cost note — never let the stamp imply the price was confirmed.
- A miss on a per_success route (
{"phones":[]}, 0 credits) proves miss=free, not the
hit price. Say so in the note.
- Settle ≤ reserve only on evidence. If the code settles a modal price below its reserve
(e.g. drops a documented per-result rider because one 1-row probe didn't charge it), that is an
unproven assumption that under-bills treg on every call (#141 inverse). Settle at the estimate
until a multi-row balance delta shows the rider is not billed.
- Scrub every captured example (this repo is public): no named private individuals
(contact-lookup routes get
untestable: + no test_request + no example), no third-party
emails/phones riding along, no first-party account identity.
- No credential value anywhere in the diff.
Step 7b — the verification evidence ledger (required in every listing PR)
"Live-called all N tools" in prose is not evidence. The PR description carries one table row per
endpoint, produced from your own run, so a reviewer can tell at a glance which stamps rest on an
observation and which don't:
| endpoint | http | test target | credits observed | catalog price | matches? | evidence |
|---|---|---|---|---|---|---|
| svc.companies.search | 200 | stripe.com, limit 1 | 0.03 | per_result 0.03 | ✅ | `x-credits-used: 0.03` header |
| svc.people.phone | 200 | real profile | 0 (miss) | per_success 8 | ⚠️ hit unobserved | balance 945.75→945.75 |
| svc.post.reactions | 200 | urn:…7496332962049933312 | 2 | per_success 2 | ✅ | balance 937.75→935.75 |
Rules for the ledger:
evidence names the meter: a charge header/field, a rate-card endpoint, or a balance delta
(before → after). "Docs say" is not a meter. Arithmetic across a batch is fine if the batch total
reconciles to the cent — state the reconciliation (87.25 documented − 25 preview − 6 placeholder − 8 miss = 48.25 observed ✓).
matches? is honest: ✅ observed = catalog; ⚠️ partial (miss/preview/placeholder only —
say what was not observed); ❌ mismatch (then the YAML must already be corrected to the
observed value with source: observed).
- Also record: the bogus-key probe (status + quoted body), balance before/after per provider,
catalog_validate.py and build_plugin.py --check output, the pytest count, and the date.
- Prices that were observed get
source: observed / confidence: verified; the rest stay
documented. The ledger and the YAML must agree — a ledger that contradicts its own YAML
bounces the PR.
Step 8 — optional extended tier
If the vendor publishes a stable OpenAPI spec with example parameter values, add an
ingest_<service>() to scripts/catalog_ingest.py, register it in INGESTERS, and generate
<service>.extended.yaml. Rules: never probe with a real call; platform = what the data is
ABOUT; normalise platform slugs across providers. Bulk-verify with
catalog_verify_extended.py --dry-run first, then with an explicit --budget.
Reviewing a vendor-RAISED PR (they wrote the files; you verify)
The same pipeline, entered from the other end. Every vendor claim is untrusted input — one
vendor PR was outright malicious (#92), and an honest one shipped a docs-transcribed price 5×
under the real charge (#141, GitHub→LinkedIn: claimed 1 credit, metered 5). The order:
- Gate on the required PR evidence (per
docs/VENDORS.md items 8–9): the Step 7b ledger
(per-endpoint status + claimed vs metered cost + meter evidence, dated) and the full-surface
map. Missing → ask for it before spending review time. A ledger that contradicts its own YAML
bounces the PR unreviewed. This applies to internal listing PRs too (#191 shipped with
prose claims and three stamps on a placeholder URN).
- Diff hygiene first: expected files only (registry entry, catalog YAML, logo, two test
lists, fx row), data-only changes, no credential values, no
verified: stamps or committed
examples (those are yours to add).
- Merge it onto current main locally before verifying — catalog PRs staleness-conflict in
the shared test lists and REGISTRY tuple within days.
- Independently verify with a key YOU control (steps 5–7 above): watch the bogus-key
rejection yourself and quote the real wire body, run
catalog_verify.py over every
test_request, and reconcile every cost block against the meter (charge field / rate-card
endpoint / balance delta) — the vendor's ledger is a cross-check, never the source of truth.
Where a price disagrees, fix it from the observed charge (source: observed,
confidence: verified) and tell the vendor their docs are stale. Post your own Step 7b table
as the review body and one inline comment per issue, anchored to the YAML/API line.
Read the keys from env only; if they arrived in chat, say they should be rotated.
- Audit the curation against their surface map: are the free count/pre-flight routes and
cheapest operation tiers in? Deliberate-miss test_requests labeled, with the hit price
observed once? per_success semantics actually observed (a miss settling at 0)?
- Finish the maintainer half they can't: front-door counts (llms.txt, skill.md, README) +
scripts/build_plugin.py, docs drift, and the tier-4 key slot (Step 2b — not optional; if
their prices can't support it, record why). Land your verified version (a maintainer branch
superseding their PR is fine); close their PR with credit and the findings.
Step 9 — done means
- Validator exits 0; suite green; bogus-key rejection observed live and quoted
- The PR description carries the Step 7b evidence ledger (one row per endpoint, meter named,
balances before/after, reconciliation), and the vendor's contact email — no credential value
anywhere
- Every endpoint carries
verified: + example backed by a real target and an observed charge, or
its cost note states exactly what was not observed (hit price / rider / placeholder)
- Platform-key slot shipped:
config.py setting, render.yaml key (no value), fx.yaml rate
from a real top-up, platform_eligible test for the whole file; env value handed to Jason
out-of-band with the TREG_PLATFORM_PROVIDERS entry. Enabling in Render is his call, not
automatic — but the PR must make it a one-line ops change.
- Any settle-below-reserve logic is backed by a multi-row balance delta, and no provider credit
arithmetic is hardcoded in
api.py that the YAML doesn't also state
- Docs synced: run
bash .claude/skills/tools-registry-context/scripts/drift.sh, update touched
fragments in the same commit
1---2name: vendor-listing3description: Onboard a vendor who wants their API listed in the treg catalog. Use whenever someone asks "how do we get listed on treg", a vendor sends their API details, or a listing PR/issue needs review. Walks the whole pipeline: eligibility gate → registry entry → platform-key slot → logo → tests → LIVE bogus-key test → core catalog YAML → verify → scrub → validate → evidence ledger in the PR. Every listing ships tier-4 wiring and a per-endpoint verification table. The vendor-facing doc this skill implements is docs/VENDORS.md.4---56# Vendor listing — add a provider to the catalog78A listing has **three parts**, and all three ship in the same PR:9101. **Registry entry** (`src/treg/oauth_providers.py`) — how a team connects a credential for the11 provider, and how treg verifies that credential is real.122. **Core catalog file** (`src/treg/catalog/<service>.yaml`) — what an agent can *do*: 8–1513 curated endpoints with capability mapping, inputs, cost + provenance, and verified examples.143. **Platform-key slot** (`config.py` + `render.yaml` + `fx.yaml`) — so treg can serve the15 endpoints on its own key (tier 4). **Always aim for this.** A listing that is BYOK-only is the16 exception and needs a stated reason (no self-serve pricing, `own_account` data, sales-gated).1718And every listing PR carries a **verification evidence ledger** (Step 7b). No ledger, no merge.1920Deep references (read before non-trivial work; do not duplicate them here):21- `docs/context/guides/expanding-a-category.md` — the add-a-provider playbook, verify toolbox, traps22- `docs/context/architecture/catalog.md` — catalog schema, cost provenance, verify pipeline, PII rules23- `docs/VENDORS.md` — what we told the vendor to prepare (their checklist)24- `src/treg/web/vendor-listing.md` — the HOSTED instructions (served at `/vendor-listing`) that a25 vendor's own coding agent follows to raise a listing PR; the dashboard's "List as vendor" modal26 (connections view, `vendorAsk` in `index.html`) hands vendors a prompt pointing at it. Keep the27 three vendor-facing surfaces (doc, hosted page, modal prompt) telling one story.2829## Step 0 — intake: collect the vendor facts3031Before touching code, you need ALL of these. If the vendor's submission is missing any, ask —32do not guess ("unconfirmed" beats a wrong path shipped):3334- **A contact email for the vendor's team** — required in the PR/issue description. It is how a35 test credential gets arranged for live verification; without it the listing stalls at step 7.36- `service` id (lowercase slug), display name, one-line summary (what an agent can DO)37- `base_url` (exact API root)38- Auth: where the key rides (header name + format, or query param name). Key **in the URL path is39 not supported** — decline or defer.40- A **free or near-free probe endpoint** where a valid key returns 2xx and an invalid key does NOT41 — plus the exact bad-key behavior (status code, or the JSON field that signals invalid)42- Pricing page URL, per-endpoint prices, and the billing model (`per_call` / `per_success` /43 `per_result` / credits / quota). Machine-readable rate-card endpoint if they have one.44- Docs URL; OpenAPI spec URL if published45- The 8–15 endpoints they consider their core surface, with example parameter *values*46- A test credential (or credits grant) for verification — read it from env only, never write it47 into any file4849## Step 1 — eligibility gate5051Reject decisively, with a recorded reason, when:52- The key **cannot be validated** (API returns success for garbage keys) — e.g. ScrapeCreators53- Key rides in the **URL path** (`/v3/{key}/…`) — injectors do header/query only54- **Sales-gated** signup (no self-serve key breaks the fast path)55- Legal/shutdown risk, or deprecated/absorbed products5657## Step 2 — registry entry5859Add an `OAuthProvider(auth_kind="key", …)` in `oauth_providers.py` and append it to `REGISTRY`.60Model it on `HUNTER` (a clean key provider). Pick the verify fields from the toolbox table in61`expanding-a-category.md` (`token_header`/`token_format`, `token_location="query"`+`token_param`,62`probe_url`, `probe_method`+`probe_json`, `token_verify_field`, `token_ok_field`+`token_ok_value`,63`token_reject_field`, `probe_reject_statuses`, …). Prefer a header over a query key so the secret64never lands in a logged URL. Set `category` (add to `CATEGORY_ORDER` only if genuinely new),65`summary`, `base_url`, `docs_url`, `probe_path`, and `setup_url`/`setup_steps` so a user can find66their key.6768**Provider-required constant headers** (Crustdata's `x-api-version: 2025-11-01`): declare them in69`required_headers=(("name", "value"),)` on the `OAuthProvider` and in the `providers.py` CATALOG70row — never in the proxy. They become constant-format bindings. Trap (PR #191): a binding whose71`format` has no `{secret}` must NOT be fed to `_secret_renderings` — otherwise the literal72value (a date!) joins the redaction set and gets masked out of error evidence. Check the73constant does not appear in `_secret_renderings`' output, and add a test.7475## Step 2b — platform-key slot (do this for every listing)7677The tier-4 wiring is part of the listing, not a follow-up:7879- `src/treg/config.py`: `platform_key_<service>: str = ""` with a one-line comment (auth shape,80 what a top-up buys). Pairs (key+secret) use `platform_extra_setting`; see Tomba.81- `render.yaml`: `- key: TREG_PLATFORM_KEY_<SERVICE>` + `sync: false` + a comment. **No value.**82- `src/treg/catalog/fx.yaml` `credit_rates_usd`: the USD-per-credit treg actually pays, with83 `basis` naming the real top-up/receipt (not the pricing page's headline tier), `source`, `checked`.84- A test asserting `cat.platform_eligible(ep)` for every endpoint in the file (see85 `test_crustdata_and_aviato_catalogs_are_platform_priced`); every cost must therefore be86 `confidence: documented|verified` with a computable USD figure.87- If the platform key also needs a constant header, `_platform_bindings` must carry it — assert88 the tier-4 binding list equals BYOK's (see `test_crustdata_platform_key_keeps_the_required_version_header`).89- Hand the **env value** to Jason out-of-band (a file, never the PR, never chat if avoidable) with90 the `TREG_PLATFORM_PROVIDERS` allow-list entry. Setting it in Render is his ops decision; the PR91 just makes it possible.9293Modal pricing (preview/rescrape/email riders, bulk-per-record) that one scalar can't express: keep94the numbers in the YAML cost block (`note` today; a `modifiers` block if you add one) and read95them generically — do not hardcode credit arithmetic in `api.py` per provider (#191 debt).9697## Step 3 — logo9899`src/treg/web/logos/<service>.svg` — a **neutral lettermark**, not the real brand mark.100`test_every_provider_has_a_logo` fails without it.101102## Step 4 — tests103104- Add the id to `test_every_provider_is_registered` (test_oauth_providers_m3)105- Add it to the offerable loop in `test_key_providers`106107## Step 5 — LIVE bogus-key test (load-bearing; never skip)108109Start the server, `POST /connections/token` with a **garbage key** against the real API:110- `422 "rejected …"` → correct. Ship it.111- `200` → the probe does not validate the key → fix the verify fields or drop the provider.112- `404`/`502` in the reason → wrong probe path/host → fix `base_url`/`probe_path`.113114**Never ship a key provider you haven't watched reject a bogus key.** Use a throwaway org115(`e2e-…@treg.local`) and delete it after. Watch for the known traps: trailing-slash 307 (put the116slash in `probe_path`), 200-with-error-body (read a body field), CSV/text responses.117118## Step 6 — core catalog YAML119120`src/treg/catalog/<service>.yaml`, following the schema in `catalog.md`. In order:1211221. **Ingest** from OpenAPI if published (never hand-transcribe paths); else from docs with123 `source.openapi: null`.1242. **Select** ~8–15 endpoints; ALWAYS include ones matching capabilities other providers already125 implement (overlap enables comparison).1263. **Map** each to a capability from `capabilities.yaml`. Search the taxonomy AND the other127 provider files for the job first; reuse an existing id, never a near-duplicate. Missing jobs go128 under `proposed_capabilities:` in the provider file, not straight into the shared taxonomy —129 and if another provider's endpoint already does the job unmapped, name it in the PR so the130 reviewer attaches both to the new capability.1314. **Describe** `input` (param names, types, required, location; constraints into `note`).1325. **Cost** with full provenance: `type/value/currency/per/unit` + `source/source_url/checked/133 confidence`. Unknown price → `value: null` + `confidence: unknown` + a note. Prefer a134 rate-card endpoint (`source: rate_card_api`) over a pricing page.1356. **test_request** per endpoint — CHEAP: smallest limit, one item, public well-known target.136 ⚠️ Never probe with empty params "expecting a validation error": a no-required-params endpoint137 returns its full default result set and bills for it (the Moz quota trap).138139## Step 7 — verify, scrub, validate140141```bash142TREG_CATALOG_CRED='<secret>' uv run --frozen python scripts/catalog_verify.py <service>.yaml143uv run --frozen python scripts/catalog_validate.py # must exit 0144uv run --frozen python -m pytest -q145```146147- Stamp `verified:` only on endpoints that PASSED **with a real target that returned real data and148 billed the documented amount**. Docs lie; documented ≠ verified. Three stamp traps from #191:149 - **Placeholder path params** (`urn:li:activity:0000…`, `id: 0`): providers return an empty150 2xx for 0 credits. That proves the route exists, nothing else. Use a real id harvested from a151 sibling endpoint's response and observe the charge once.152 - **Preview / free modes** (`preview=true`) as the test_request: the free path is verified, the153 paid price is not. Either observe the paid path once, or write "hit price unobserved" in the154 cost `note` — never let the stamp imply the price was confirmed.155 - **A miss on a per_success route** (`{"phones":[]}`, 0 credits) proves miss=free, not the156 hit price. Say so in the note.157- **Settle ≤ reserve only on evidence.** If the code settles a modal price below its reserve158 (e.g. drops a documented per-result rider because one 1-row probe didn't charge it), that is an159 unproven assumption that under-bills treg on every call (#141 inverse). Settle at the estimate160 until a multi-row balance delta shows the rider is not billed.161- **Scrub every captured example** (this repo is public): no named private individuals162 (contact-lookup routes get `untestable:` + no test_request + no example), no third-party163 emails/phones riding along, no first-party account identity.164- No credential value anywhere in the diff.165166## Step 7b — the verification evidence ledger (required in every listing PR)167168"Live-called all N tools" in prose is not evidence. The PR description carries **one table row per169endpoint**, produced from your own run, so a reviewer can tell at a glance which stamps rest on an170observation and which don't:171172```173| endpoint | http | test target | credits observed | catalog price | matches? | evidence |174|---|---|---|---|---|---|---|175| svc.companies.search | 200 | stripe.com, limit 1 | 0.03 | per_result 0.03 | ✅ | `x-credits-used: 0.03` header |176| svc.people.phone | 200 | real profile | 0 (miss) | per_success 8 | ⚠️ hit unobserved | balance 945.75→945.75 |177| svc.post.reactions | 200 | urn:…7496332962049933312 | 2 | per_success 2 | ✅ | balance 937.75→935.75 |178```179180Rules for the ledger:181- **`evidence` names the meter**: a charge header/field, a rate-card endpoint, or a balance delta182 (before → after). "Docs say" is not a meter. Arithmetic across a batch is fine if the batch total183 reconciles to the cent — state the reconciliation (`87.25 documented − 25 preview − 6 placeholder184 − 8 miss = 48.25 observed ✓`).185- **`matches?` is honest**: ✅ observed = catalog; ⚠️ partial (miss/preview/placeholder only —186 say what was not observed); ❌ mismatch (then the YAML must already be corrected to the187 observed value with `source: observed`).188- Also record: the **bogus-key probe** (status + quoted body), balance before/after per provider,189 `catalog_validate.py` and `build_plugin.py --check` output, the pytest count, and the date.190- Prices that were observed get `source: observed / confidence: verified`; the rest stay191 `documented`. The ledger and the YAML must agree — a ledger that contradicts its own YAML192 bounces the PR.193194## Step 8 — optional extended tier195196If the vendor publishes a stable OpenAPI spec with example parameter values, add an197`ingest_<service>()` to `scripts/catalog_ingest.py`, register it in `INGESTERS`, and generate198`<service>.extended.yaml`. Rules: never probe with a real call; platform = what the data is199ABOUT; normalise platform slugs across providers. Bulk-verify with200`catalog_verify_extended.py --dry-run` first, then with an explicit `--budget`.201202## Reviewing a vendor-RAISED PR (they wrote the files; you verify)203204The same pipeline, entered from the other end. Every vendor claim is **untrusted input** — one205vendor PR was outright malicious (#92), and an honest one shipped a docs-transcribed price 5×206under the real charge (#141, GitHub→LinkedIn: claimed 1 credit, metered 5). The order:2072081. **Gate on the required PR evidence** (per `docs/VENDORS.md` items 8–9): the Step 7b ledger209 (per-endpoint status + claimed vs metered cost + meter evidence, dated) and the full-surface210 map. Missing → ask for it before spending review time. A ledger that contradicts its own YAML211 bounces the PR unreviewed. This applies to **internal** listing PRs too (#191 shipped with212 prose claims and three stamps on a placeholder URN).2132. **Diff hygiene first**: expected files only (registry entry, catalog YAML, logo, two test214 lists, fx row), data-only changes, no credential values, no `verified:` stamps or committed215 examples (those are yours to add).2163. **Merge it onto current main locally** before verifying — catalog PRs staleness-conflict in217 the shared test lists and REGISTRY tuple within days.2184. **Independently verify with a key YOU control** (steps 5–7 above): watch the bogus-key219 rejection yourself and quote the real wire body, run `catalog_verify.py` over every220 test_request, and reconcile every cost block against the meter (charge field / rate-card221 endpoint / balance delta) — the vendor's ledger is a cross-check, never the source of truth.222 Where a price disagrees, fix it from the observed charge (`source: observed`,223 `confidence: verified`) and tell the vendor their docs are stale. Post your own Step 7b table224 as the review body and one inline comment per issue, anchored to the YAML/API line.225 Read the keys from env only; if they arrived in chat, say they should be rotated.2265. **Audit the curation against their surface map**: are the free count/pre-flight routes and227 cheapest operation tiers in? Deliberate-miss test_requests labeled, with the hit price228 observed once? per_success semantics actually observed (a miss settling at 0)?2296. **Finish the maintainer half they can't**: front-door counts (llms.txt, skill.md, README) +230 `scripts/build_plugin.py`, docs drift, and the tier-4 key slot (Step 2b — not optional; if231 their prices can't support it, record why). Land your verified version (a maintainer branch232 superseding their PR is fine); close their PR with credit and the findings.233234## Step 9 — done means235236- Validator exits 0; suite green; bogus-key rejection observed live and quoted237- The PR description carries the Step 7b evidence ledger (one row per endpoint, meter named,238 balances before/after, reconciliation), and the vendor's contact email — no credential value239 anywhere240- Every endpoint carries `verified:` + example backed by a real target and an observed charge, or241 its cost `note` states exactly what was not observed (hit price / rider / placeholder)242- Platform-key slot shipped: `config.py` setting, `render.yaml` key (no value), `fx.yaml` rate243 from a real top-up, `platform_eligible` test for the whole file; env value handed to Jason244 out-of-band with the `TREG_PLATFORM_PROVIDERS` entry. Enabling in Render is his call, not245 automatic — but the PR must make it a one-line ops change.246- Any settle-below-reserve logic is backed by a multi-row balance delta, and no provider credit247 arithmetic is hardcoded in `api.py` that the YAML doesn't also state248- Docs synced: run `bash .claude/skills/tools-registry-context/scripts/drift.sh`, update touched249 fragments in the same commit