Conversation
stonexer
force-pushed
the
fix/feedback-10-45-batch
branch
from
September 9, 2026 14:25
df7da0b to
1111db8
Compare
…roup Lusha removed POST /v3/contacts/decision-makers on 2026-08-12 (API changelog 2.9.0) in favour of POST /v3/contacts/buying-group, the only operation that accepts contactsLimit and personas. Commit 5c52ae2 documented those parameters on the legacy path, but its surviving handler rejects them with 400 'property should not exist', so the documented per-company spend cap never applied: companies-only bodies still answered, uncapped at Lusha's 60-contacts-per-company default. - mark lusha.x.decision-makers `status: retired` with the removal story and `superseded_by: lusha.x.buying-group`; a direct call now answers 410 naming the successor before any credential loads or a hold is placed - add lusha.x.buying-group per Lusha's V3BuyingGroupRequest (companies 1-25 of {domain|id, clientReferenceId}, personas, contactsLimit default 60 minimum 1, pagination), same per_result 1-credit price and the people.decision_makers capability; no live probe was run, so it ships `skipped` with no example fixture instead of an invented one - tests: the tombstone leaves discovery and the successor stays platform-eligible; a POST at the retired id is 410, names the successor and moves no ledger entry - docs: money.md's contactsLimit paragraph now says the cap only reserves what the provider honours, and names the tombstone and its successor
A task_post only enqueues work; the answer comes back through task_get,
which ingest drops, or through id-keyed readers that are already
platform_blocked. The 26 task_post rows were still platform-eligible, so
a shared-key caller paid to start crawls and SERP tasks whose results
treg could never serve (213 platform calls across 21 orgs since 09-04).
- stamp platform_blocked on every DataForSEO task_post row with a reason
that says results come back through task_get, to connect an own key,
and names the one-shot sibling treg does serve (dataforseo.web.page.audit
for on-page, brightdata.x.trustpilot-reviews for Trustpilot reviews)
- test: a task_post may be offered on treg's key only while a consumer of
its family is; on-page consumer notes may only name ids that exist
- fix the on-page consumer notes that pointed at the non-existent
dataforseo.x.on-page-instant-pages and declare the {id} pathParam on
dataforseo.x.on-page-summary-id
- catalog hints (search, endpoint detail, MCP catalog_get via the same
route) and `treg catalog get` no longer say "key injected server-side"
for a platform_blocked row; they say the team's own key is needed and
carry the reason
- aliases: `website` and `site` bridge to `on-page`, so "website audit"
and "site audit" find the one-shot instant_pages row first instead of
only the async task_post; drop the duplicated `ig` alias key
…lk jobs per row Icypeas' /email-search, /domain-search and /bulk-search answer 2xx with an acknowledgement and no result rows; the credit for a hit is only visible later on the free poll route. Settling at the estimate billed every submission the 20-row page default (380000 micro), hit or miss. An acknowledgement now settles at 0 and closes the hold; synchronous bodies with data rows and the per_call verify route keep the estimate. icypeas.bulk.search reserves one credit per row of the body's top-level data array, capped at the platform row maximum, without changing how other providers' bodies are read. Catalog notes and the money fragment state that submissions settle at 0 today and that treg absorbs found-email credits until terminal settlement exists.
CompanyEnrich bills its search pages per row returned, with a documented floor on an empty page (2 credits for people, 1 for companies, 5 for lookalikes). No settlement rule counted the rows, so every 2xx settled at the reserve, the whole requested page: live 2026-09-09 an empty pageSize 10 people search cost 2 credits upstream and treg charged 20. - settle: a companyenrich rule charges len(items) rows at the catalog per-row price, never below the cost block's `minimum_units` floor; a body without an `items` list keeps the estimate. - catalog: `minimum_units: 1` on people/companies search, their scroll routes and companies.similar, validated by catalog_validate.check_cost. - tests: direct platform pages (empty, partial, full, unreadable) and a routed treg.people.search whose companyenrich child misses. - docs: money.md provider rules and sources, MAP.md regenerated.
… on params.size Serpstat meters one API credit per returned row and answers a rejected request as HTTP 200 with an `error` object. Verified live 2026-09-09 against the account's own limits meter: an error envelope cost 0 lines while treg settled the 20-row estimate (10,000 micro), and a getKeywordTop that returned 12 rows cost exactly 12 lines while treg settled 20 credits. Two estimator defects fed that. `_body_limit` never looked inside a JSON-RPC `params` object, so `size` was ignored and every row-priced route reserved the page default. The routes priced per RETURNED row (ranked_keywords, keywords.ideas, linking_domains.list) carried `unit: keyword`/`domain`, which the per-input counter read as exactly one credit regardless of `size`. With no settle rule the estimate was the charge either way. - resolve: a JSON-RPC envelope (`method` + `params` object) is read at the top level and then inside `params`, generically, for the same limit keys and entity lists as any body. - catalog: those three routes are `unit: row`; keywords.volume and domain.overview stay per input. Notes say what is verified and that an error envelope is free; the unsourced "since 2025-06-15" minimum is restated as the documented, unverified floor. - settle: `application/call/serpstat.py` counts rows in `result.data[]`, `result.data.top[]` or the keyed entries, settles an `error` envelope at 0 and a served empty result at the 1-credit floor; an unrecognised shape keeps the estimate.
seranking.google.keywords.ideas bills 10 credits per keyword RETURNED, but the catalog priced it per input keyword, so every call reserved and settled one unit (1790 micro-USD) whatever limit asked or the answer carried. Verified live 2026-09-09 on treg's own meter: 5 keywords returned cost 50 credits while treg charged 10; an empty answer cost 0 while treg charged 10. - catalog: the route is now unit: row with page_default: 100 (the API's own default page), source observed, confidence verified; keywords.volume stays per input keyword. - estimate: a row-priced cost may carry page_default, the rows the provider answers when the caller names no limit; the reserve is limit x unit, capped at the platform max, and the validator checks the key. - settle: the returned keywords list is the bill (the influencersclub rule); an empty list settles at 0 and a non-JSON body settles at the estimate. - tests: limit 5 reserves 5 units, 5 rows charge 8950, empty charges 0, an unparseable body settles at the estimate, siblings are untouched.
…ataset array apify.linkedin.search.jobs documented the actor README's display labels (Past month, On-site, Full-time) where the actor's input schema takes month, office, full-time; the five enum fields now carry the schema's values as enum lists and the test_request sends postedLimit so a re-verify exercises one. The maxItems note called the query-string cap the only hard spend limit, which is true for pay-per-result actors but not for this pay-per-event one: maxTotalChargeUsd is the platform's abort and the body maxItems is the actor's own cap. Every platform call on the route settled at a flat 20,000 micro (3,019 calls since 2026-08-20) because maxItems was not a reserve signal and no settle rule counted the response. resolve reads maxItems and resultsLimit as limit params (positive integers only; 0 keeps the page default) and settle counts a bare top-level JSON array for apify per_result rows, falling back to the estimate on gzip, truncated or unexpected shapes, mirroring the Bright Data rule.
…l domain search needs a role
The adapter verifier compared only the keys an adapter maps against the
endpoint's test_request, so an adapter that could never produce a
required input still verified. findymail.search.domain accepted
{company_domain} alone while its body.roles is required: every routed
call to it was a vendor 4xx by construction, which the router read as
the caller's fault and ended treg.people.search with 422
route_caller_fault, keeping the earlier children's charges.
- contracts.py: missing_required_inputs runs each accepted variant of an
adapter through to_upstream on a placeholder identity (choosing the
variant the router would send) and verify() fails any adapter that
cannot fill an input the endpoint marks required: true. Bare-array
bodies are read through their first element; an input of type array
that labels the array itself is satisfied by a non-empty array.
- adapters.yaml: findymail.search.domain accepts only
{company_domain, title} and sends roles: [title];
moz.web.backlinks.summary now sends distributions: true, which the
endpoint is defined and priced by.
- lusha.yaml: the two search inputs name the pagination field the live
API takes (pagination, size >= 10) instead of the docs' pages, as
their notes, test_requests and adapters already did.
- tests: the verifier rejects an adapter missing a required body field;
a domain-only routed people.search never reaches findymail and misses
honestly instead of 422.
- docs: catalog.md adapter verification section.
…-ranked one
The routed pre-check compared the ceiling with plan.candidates[0] only and
refused the call, calling that candidate "the cheapest" even though the
plan is ranked by specificity before price. people.phone.find for
{email, full_name} leads with leadsforge (245000 micro) ahead of tomba
(44500 micro), so a $0.05 cap was refused while the embedded plan showed
an affordable row and the attempt loop would have skipped past it.
The loop already skips an over-cap candidate; the pre-check is gone and
the 402 route_max_cost is raised only after the loop, when every
candidate was skipped for cost and nothing was asked or reserved. It
names the true minimum (cheapest_micro, cheapest_endpoint_id). The
unused next() over a miss in tried, which raised StopIteration when
every attempt was skipped, is removed.
Docs: the catalog fragment now states the actual specificity rule (a
derived key covers the caller's key it was derived from) and the
per-candidate ceiling.
Connect provisions every account of a provider on the same host with the same provider-tagged secret, so a second connected account or two providers sharing a host (meta-ads beside instagram-page-tools on graph.facebook.com) tied in the URL-passthrough matcher and every catalog call for that provider failed with 409 target_ambiguous. - _resolve_marketplace_call now asks _provider_tool_grant for a tool bound to a connection of the endpoint's provider on the upstream host before matching by host; the method filter is bypassed for endpoints that declare none, the bare service name wins among several accounts, then the newest connection. - _resolve_call's tiebreak treats a tool as provider-owned only when its secret's provider owns the upstream host (_providers_owning) and raises AmbiguousTarget carrying the tie. - A catalog call that still ties restates the 409 in catalog terms: the endpoint id, the colliding tools and the /call/<tool>/<path> form of each. - Own-account tools remain unmetered and unrouted on every branch. Fragments updated: docs/context/architecture/proxy-model.md, docs/context/architecture/instagram-oauth.md, docs/context/interface/api.md
Restore the icypeas platform fixture body, drop the duplicated contactsLimit entry in _LIMIT_PARAMS, merge the routers/catalog hints expression and the money.md row-count paragraph that both-sided cherry-picks had concatenated, and regenerate MAP.md.
…unts Provider identity still resolves meta-ads beside instagram-page-tools, but two accounts of one provider are a genuine tie: a silent default to the bare-named tool would route one account's request through the other's credential. Answer the structured 409 with each account's named form instead.
…ich phone candidate
quickenrich.people.phone.find (main, 2026-09-09) also covers {first_name,
last_name, domain} and undercuts tomba, so the two max-cost tests exclude it
and compare tomba against leadsforge by the plan's usd field.
stonexer
force-pushed
the
fix/feedback-10-45-batch
branch
from
September 9, 2026 14:39
1111db8 to
caf758d
Compare
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batch of the verified billing, routing and catalog fixes from the 2026-09-08/09 feedback triage (reports #11 to #45). Every root cause was traced in code, then confirmed on 2026-09-09 against the production ledger and the providers' own credit meters with controlled test calls. One commit per fix, plus one reconciliation commit and one review fix. The overflow disclosure and aggregator-strike change is split out into its own PR because it changes capacity policy for every overflowed call.
Billing (settlement and reservation)
people.email.find,bulk.searchand the sibling bulk routes) settle at 0 instead of the 20-row estimate; bulk jobs reserve one row per submitted row. Live: a NOT_FOUND cost 0 credits upstream while treg charged $0.38; every platform success since 08-20 was charged exactly $0.38.people.searchand.scrollsettle on returneditems(2 credits each) with the catalog-declared 2-credit minimum on an empty page. Live: an empty pageSize 10 page cost 2 credits upstream while treg charged 20.params.sizeinside the JSON-RPC envelope; row-priced routes useunit: row; a top-levelerrorobject settles at 0 and a success counts the returned rows. Live: error envelopes cost 0 lines while treg charged 20 credits; a 12-row SERP cost 12 lines.keywords.ideasreserves bylimitand settles per returned keyword. Live: 5 keywords cost 50 credits while treg charged 1 unit; an empty result cost 0.linkedin.search.jobs:maxItems/resultsLimitsize the reserve, the actor's bare dataset array is counted at settle, and the input notes carry the actor's real enum values instead of README labels (3,019 platform calls settled at a flat $0.02).Routing
X-Treg-Route-Max-Costis applied per candidate; the 402 names the truly cheapest candidate only when every candidate is over the cap.roles(it deterministically 4xx'd and endedtreg.people.searchas 422 with earlier charges kept)./call/<tool>/<path>form: nothing in the request says which account is meant, and a silent default would route one account's request through the other's credential.Catalog
lusha.x.decision-makersretired (Lusha removed the path on 2026-08-12) and superseded bylusha.x.buying-group, the path that acceptscontactsLimit.task_postrows areplatform_blockedon the shared key because their consumers are blocked or unlisted; hints stop claiming the key is injected for blocked rows; "website audit" aliases to the one-shot page audit.Review notes (manual, after the agent review pass was interrupted)
Read every hunk of the twelve commits against AGENTS.md and money.md. One change made as a result: the multi-account default above. Findings left as they are, for the reviewer to weigh:
icypeas.bulk.searchaccepts up to 5,000 rows per job (up to about $95 absorbed per job). Before this PR big jobs were already under-billed at a flat $0.38, so this is not a new exposure class, but blocking bulk.search on the shared key until async settlement lands is a reasonable alternative.errorobject as free. Verified live for-32000only; the32017not-found envelope is assumed free too (safe for customers, at most 1 credit of treg revenue per call).keywords.ideasnow reserveslimitrows with the API's default of 100, so a call that names no limit reserves $0.179 instead of $0.0018. That is the vendor's real price.distributionsconst, lusha people searchpagination); all 298 adapters verify on this branch as on main.lusha.x.decision-makersanswers 410 for the 13 orgs that used it in the last 30 days, and 21 orgs' DataForSEO task_post calls answer credential_missing on the shared key. Both intended; both deserve a note to those teams.Verification
uv run --with pytest-xdist pytest -n auto: 3322 passed, 5 skipped. Every commit compiles on its own. One test adapted: the max-cost routing tests exclude the new quickenrich phone candidate so the leadsforge-over-tomba story they exercise stays intact. One different test failed on each of two runs and passed standalone:test_archive.py::test_admin_archive_keys_endpoint(3/3 standalone), andtest_alembic_baseline.py::test_alembic_head_has_no_model_drift, which fails on this machine for every checkout because another checkout's test run left acallreviewtable in the shared sqlite files under$TMPDIR/treg-tests; it passes against a privateTREG_TEST_DB_URL. Neither is touched by this branch.lint-imports: 14 contracts kept.drift.sh origin/main..HEADmaps every changed source to fragments updated in the same commits.Not included here: the overflow disclosure (separate PR), the Hunter
companies.emailsrepricing, the routed-row pricing notes, and the catalog docs hygiene item were interrupted before commit and will follow separately. The findymail/companyenrich changes do not issue refunds; historical corrections are a separate grant.