Merge pull request #566 from superdesigndev/codex/provider-fixes

fix(catalog): remove unsafe batch tools and route ContactOut
This commit is contained in:
Taus
2026-09-18 06:59:17 +06:00
committed by GitHub
67 changed files with 382 additions and 1495 deletions
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -17,13 +17,13 @@ covers (frontmatter `sources:`). Regenerate this index with
| Fragment | Status | Covers |
|---|---|---|
| [Google Ads conversion tracking — capture, outbox, upload](architecture/ads-conversions.md) | shipped | adsconv.py, signup.py, adtrack.js, gtag.js |
| [AI Ark — bounded synchronous enrichment and BYOK async jobs](architecture/aiark.md) | shipped | aiark.yaml, aiark.companies.search.json, aiark.lists.upsert.json, aiark.people.email.find.bulk.submissions.json, … |
| [AI Ark — bounded synchronous enrichment](architecture/aiark.md) | shipped | aiark.yaml, aiark.companies.search.json, aiark.lists.upsert.json, aiark.people.email.find.json, … |
| [Archive - versioned history and cache admission](architecture/archive.md) | building | archive.py, hunter.yaml, results.py, 0031_archive_result_admission.py, … |
| [Auth & secrets — injectors, encryption, OAuth freshness, health](architecture/auth-secrets.md) | shipped | injectors.py, ssrf.py, crypto.py, oauth.py, … |
| [BounceBan — email verification, billing boundaries and capacity](architecture/bounceban.md) | shipped | bounceban.yaml, bounceban.people.email.verify.json, bounceban.people.email.verify.waterfall.json, bounceban.people.email.verify.status.json, … |
| [Endpoint catalog — what you can DO with a connected key, and which provider should do it](architecture/catalog.md) | shipped | financialdatasets.yaml, financialdatasets.company.facts.json, financialdatasets.company.facts.ciks.json, financialdatasets.company.facts.tickers.json, … |
| [Application composition and deployment roles](architecture/composition.md) | shipped | bootstrap.py, bootstrap_handlers.py, bootstrap_http.py, call_surface.py, … |
| [ContactOut — LinkedIn enrichment, Starter billing and independent credit pools](architecture/contactout.md) | implemented; live connect and core surface verified, informational capacity monitoring | contactout.yaml, adapters.yaml, contactout.people.email.verify.json, test_routing.py, … |
| [ContactOut — LinkedIn enrichment, Starter billing and independent credit pools](architecture/contactout.md) | implemented; live connect and core surface verified, informational capacity monitoring | contactout.yaml, adapters.yaml, contactout.people.contact.work.json, contactout.people.contact.phone.json, … |
| [Data model — the registry tables, async DB, audit writer](architecture/data-model.md) | shipped | alembic.ini, env.py, 0001_baseline_current_schema.py, 0002_archive_tables.py, … |
| [Dropleads — synchronous people and company enrichment](architecture/dropleads.md) | implemented; live upstream behavior verified | dropleads.yaml, dropleads.people.email.find.json, dropleads.people.phone.find.json, dropleads.people.email.verify.json, … |
| [Feedback - private intake for problems and suggestions](architecture/feedback.md) | shipped | feedback_contract.py, __init__.py, reports.py, reviews.py, … |
@@ -42,7 +42,7 @@ covers (frontmatter `sources:`). Regenerate this index with
| [Openmart — metered synchronous data and BYOK lifecycle boundaries](architecture/openmart.md) | shipped | openmart.yaml, openmart.businesses.search.json, openmart.businesses.search.ids.json, openmart.businesses.lookup.openmart.json, … |
| [Prospeo — people and company enrichment](architecture/prospeo.md) | shipped | prospeo.yaml, prospeo.people.email.find.json, prospeo.people.phone.find.json, prospeo.people.enrich.json, … |
| [The proxy — faithful credential-injecting relay + tool resolution](architecture/proxy-model.md) | shipped | relay.py, ssrf.py, api.py, authorize.py, … |
| [Scrubby — quick and deep email verification](architecture/scrubby.md) | implemented; live authentication, billing and asynchronous behavior verified | scrubby.yaml, scrubby.people.email.verify.json, scrubby.people.email.verify.bulk.json, scrubby.people.email.verify.bulk.results.json, … |
| [Scrubby — single email verification](architecture/scrubby.md) | shipped | scrubby.yaml, scrubby.people.email.verify.json, adapters.yaml, fx.yaml, … |
| [Sumble — account intelligence, subscription credits and BYOK](architecture/sumble.md) | shipped | sumble.yaml, sumble.extended.yaml, sumble.organizations.json, sumble.py, … |
| [Super-admin — cross-tenant read + control](architecture/super-admin.md) | shipped | api.py, admin.py, access.py, config.py |
| [Wiza — synchronous search and company enrichment, BYOK async jobs](architecture/wiza.md) | shipped | wiza.yaml, wiza.people.search.json, wiza.companies.search.json, wiza.companies.enrich.json, … |
+9 -12
View File
@@ -1,14 +1,12 @@
---
title: AI Ark — bounded synchronous enrichment and BYOK async jobs
title: AI Ark — bounded synchronous enrichment
status: shipped
sources:
- src/treg/catalog/aiark.yaml
- src/treg/catalog/examples/aiark.companies.search.json
- src/treg/catalog/examples/aiark.lists.upsert.json
- src/treg/catalog/examples/aiark.people.email.find.bulk.submissions.json
- src/treg/catalog/examples/aiark.people.email.find.json
- src/treg/catalog/examples/aiark.people.enrich.json
- src/treg/catalog/examples/aiark.people.export.submissions.json
- src/treg/catalog/examples/aiark.people.personality.analyze.json
- src/treg/catalog/examples/aiark.people.phone.find.json
- src/treg/catalog/examples/aiark.people.preview.json
@@ -49,7 +47,7 @@ first and treg never meters it.
## Catalog surface and boundary
`aiark.yaml` exposes 18 tools from 21 documented operations. It uses the V2 single-email and mobile
`aiark.yaml` exposes eight bounded tools from 21 documented operations. It uses the V2 single-email and mobile
routes and omits their duplicate V1 forms. The free credits route stays internal to connection
verification and `collectors._aiark`, so catalog callers cannot inspect either their own or treg's
account balance.
@@ -62,11 +60,10 @@ maximum hold. Successful platform responses settle from the provider's exact `X-
AI Ark reports debits as negative numbers, so `_CREDIT_HEADERS` declares an explicit -1 multiplier;
positive, missing, nonnumeric and non-finite values remain untrusted and fall back to other evidence.
List mutation and ten async submission, result, status, history and webhook-resend tools are
BYOK-only. AI Ark track ids are account-scoped, a search track id is single-use, and delivery failure
can trigger a refund up to ten hours after submission. The current shared async contract does not
persist caller ownership and terminal charge/refund evidence for that lifecycle. The integration
therefore adds no AI Ark branch to the call, money, async, routing or Arena runtime.
List mutation remains BYOK-only. Ten async submission, result, status, history and webhook-resend
operations are omitted from the catalog: AI Ark track ids are account-scoped, a search track id is
single-use, and delivery failure can trigger a refund up to ten hours after submission. They require
a purpose-built workflow with explicit cost and lifecycle ownership before agents can call them.
## Pricing and settlement
@@ -89,8 +86,8 @@ the existing `get` expression to normalize it. A
reverse-lookup miss returned HTTP 404 and used no credit even though the public reference describes
a per-request charge, so treg conservatively settles only a successful profile.
The two async submission tools retain documented `per_result` prices as BYOK information. They do
not advertise a platform estimate, reserve a team balance or imply that delayed refunds are handled.
The omitted async submissions do not advertise a platform estimate, reserve a team balance or imply
that delayed refunds are handled.
## Routing and Arena
@@ -101,7 +98,7 @@ provider's observed response shapes. Sanitized fixtures verify every adapter whe
The tools become route and Enrich Arena candidates through the generic capability machinery.
Preview and personality analysis stay direct tools because the existing routed contracts do not
describe their outputs. Stateful list and async operations never enter Arena. There is no
describe their outputs. The stateful list operation never enters Arena. There is no
AI Ark-specific provider choice, retry or response wrapping.
## Capacity
+8 -16
View File
@@ -6,11 +6,6 @@ sources:
- src/treg/catalog/examples/bounceban.people.email.verify.json
- src/treg/catalog/examples/bounceban.people.email.verify.waterfall.json
- src/treg/catalog/examples/bounceban.people.email.verify.status.json
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.json
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.status.json
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.emails.json
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.dump.json
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.export.json
- src/treg/catalog/examples/bounceban.account.usage.json
- src/treg/catalog/adapters.yaml
- src/treg/catalog/fx.yaml
@@ -44,11 +39,10 @@ BounceBan supplies email verification through two hosts. The standard API uses
## Catalog boundary
The catalog exposes nine safe operations: standard and waterfall single verification, single-task
status, JSON bulk creation, four bulk read/export operations, and account usage. The documented
multipart CSV upload is omitted because the catalog has no file-upload contract. Bulk destruction
is omitted because it is destructive. `/v1/check` is omitted because it uses a separate Check Plan
whose acquisition price and capacity were not supplied.
The catalog exposes four operations: standard and waterfall single verification, single-task status,
and account usage. JSON and multipart bulk creation, bulk reads/exports, bulk destruction, and
`/v1/check` are omitted. Bulk work needs an explicit cost-confirmation and owned-job workflow;
`/v1/check` uses a separate plan whose acquisition price and capacity were not supplied.
Only `bounceban.people.email.verify` is platform eligible. It uses the standard host and does not
expose `disable_catchall_verify` or webhook inputs. Every accepted request therefore has the proven
@@ -57,12 +51,10 @@ body says `status=verifying`; malformed input returned HTTP 400 with no reported
the hold through the shared settlement rule. One credit costs $0.004 from the supplied $40 / 10,000
purchase. No provider-specific money branch is added.
Waterfall and bulk operations are BYOK only. Waterfall same-address retries inside 30 minutes do
not deduct a new credit, but the repeated response reports the original credit count. A waterfall
catch-all skipped with `disable_catchall_verify=1` can also cost zero. Bulk submission reserves
credits first and refunds unknown results only when the task finishes. Those delayed and ambiguous
rules cannot be settled exactly on a shared key, and task identifiers belong to the connected
account. The related status, result, dump, and export operations stay with that account.
Waterfall remains BYOK-only. Same-address retries inside 30 minutes do not deduct a new credit, but
the repeated response reports the original credit count; a catch-all skipped with
`disable_catchall_verify=1` can also cost zero. Bulk operations are omitted because submission
reserves many credits, refunds arrive only after completion, and task identifiers are account-owned.
## Routing and Arena
+29 -19
View File
@@ -184,20 +184,21 @@ related:
# Endpoint catalog — platform-grouped operations per provider
LimaData adds all 24 Basic v2 operations. Fifteen fixed, synchronous operations can use the shared
key; variable, 404-billed, and account-scoped batch operations require a team's own key. Six
LimaData exposes 21 of 24 Basic v2 operations. Fourteen fixed, synchronous operations can use the
shared key; variable and 404-billed operations require a team's own key. Account-scoped batch
submission and result operations are omitted. Six
fixture-verified adapters join existing routing and Enrich Arena contracts. See
[LimaData](limadata.md) for the full boundary and live evidence.
## BounceBan email verification (2026-09-16)
BounceBan adds nine tools across standard single verification, BYOK waterfall verification, BYOK
single/bulk lifecycle reads, JSON bulk submission, and account usage. Only the standard single tool
BounceBan adds four tools across standard single verification, BYOK waterfall verification, BYOK
single-result polling, and account usage. Only the standard single tool
is platform eligible. It has a fixed observed cost of one credit, priced at the supplied acquisition
rate of $0.004, and uses `per_call` so an accepted `status=verifying` submission is charged while a
rejected HTTP 400 request releases its hold. Waterfall retries, conditional zero-credit catch-all
results, bulk refunds, and task ownership make the other lifecycle operations unsafe for a shared
key, so they remain BYOK only.
results make waterfall unsafe for a shared key. Bulk submission and lifecycle operations are
omitted until an explicit cost-confirmation and owned-job workflow exists.
The verified adapter adds only the standard endpoint to `treg.people.email.verify`; routing and
Arena discover it from that adapter. Multipart upload, destructive bulk deletion, and the separately
@@ -214,15 +215,18 @@ showed that it needs the key in its JSON body, which the faithful relay does not
state-changing, and ambiguous-price operations are also outside the safe first surface. See
[ZeroBounce](zerobounce.md) for the inventory and evidence.
MoltSets adds 17 verified data tools: nine single-result shared-plan offers and eight BYOK-only
variable, batch, or dual-meter tools. See [MoltSets](moltsets.md) for the boundary and evidence.
MoltSets adds 12 verified data tools: nine single-result shared-plan offers and three BYOK-only
variable-result searches. Five hybrid scalar/batch email and phone operations are omitted. See
[MoltSets](moltsets.md) for the boundary and evidence.
Sumble adds the full v9 surface with verified platform operations and explicit BYOK restrictions. See [Sumble](sumble.md) for schemas, pricing rules, routing and live evidence.
GetLeads.io adds 12 direct contact-data tools. Every tool accepts BYOK or a $0 platform trial with
GetLeads.io adds 11 direct contact-data tools. Every tool accepts BYOK or a $0 platform trial with
five successful credit-using calls per team per day; its two free discovery tools do not consume
that allowance. The caller controls provider-valid page limits and batch
sizes; the allowance counts calls rather than returned records or upstream credits. Internal
that allowance. Three enrichment tools keep the upstream `items` array but opt into `strict_body`
with exactly one item. `_enforce_catalog_body` rejects invalid cardinality on every credential tier
without rewriting an accepted request. The allowance counts calls rather than returned records or
upstream credits. Internal
account routes, stateful exports and monitoring are excluded. See [GetLeads.io](getleadsio.md) for
the boundary and evidence.
@@ -2275,8 +2279,11 @@ before any example is committed, all learned the hard way:
1. **No named private individuals.** Contact-lookup routes (LinkedIn contact info, people-enrichment
by email) return a real person's name, personal email and phone. Such an endpoint stays in the
catalog — the route is real and useful — but it is marked `untestable:` with the reason, carries
NO `test_request` (so a re-verify cannot silently re-capture it), and no example is stored.
catalog — the route is real and useful — but it is marked `untestable:` with the reason and
carries NO `test_request` (so a re-verify cannot silently re-capture it). No captured person
response is stored. A routing adapter may use a hand-sanitized structural fixture only when its
contact values use reserved fake domains/numbers, it cannot be refreshed by the verifier, and
separate live evidence establishes the mapped response fields.
2. **No third-party PII riding along.** Emails and phones turn up inside unrelated payloads — a
YouTube description, a review body. Sweep every captured example for address-shaped strings and
mask anything that isn't a business contact.
@@ -2338,8 +2345,11 @@ and signature test files. The reusable setup is in `tests/conftest.py`.
`contactout.yaml` adds the core LinkedIn/contact surface with explicit work/personal selectors,
on-hit Starter rates supplied by the account owner, free verification, and deferred batches.
People lookup/search entries are `untestable:` without test requests or stored examples under the
PII rule. Their routing adapters are omitted; company search/enrichment and email verification
Contact reveals and availability checks live on the People shelf; profile identity tools remain
on LinkedIn. People entries stay `untestable:` without test requests under the PII rule. Work-email
and phone lookup use reserved-value structural fixtures and verified adapters for the existing
People routes. Personal email remains direct-only because the shared email contract is work-only;
people search/profile adapters remain omitted. Company search/enrichment and email verification
retain verified adapters. Profile-only LinkedIn enrichment costs $0.02 when found.
See [ContactOut](contactout.md) for request limitations, derived settlement and live evidence.
@@ -2380,19 +2390,19 @@ The single verified adapter is usable by Arena; the two-provider public routing
## Dropleads integration
`dropleads.yaml` adds twelve synchronous people and company tools. The balance check and export-cost
`dropleads.yaml` adds ten synchronous people and company tools. The balance check and export-cost
route stay outside the public catalog. Seven verified adapters add email finding, phone finding,
email verification, people search and enrichment, and company search and enrichment to the existing
routed tools and Enrich Arena. The count and synchronous bulk tools stay direct. The provider uses
routed tools and Enrich Arena. Count tools stay direct; two ten-person bulk tools are omitted. The provider uses
the existing `CatalogTarget` allow-list for its second API host; catalog data cannot send a
credential to another host. See [Dropleads](dropleads.md) for the surface, prices and live evidence.
## Prospeo integration
`prospeo.yaml` adds nine people and company tools on both own and platform keys. Six verified
`prospeo.yaml` adds seven people and company tools on both own and platform keys. Six verified
adapters add email finding, phone finding, person/company enrichment and person/company search to
the routed tools and Enrich Arena; bulk enrichment and search suggestions stay direct-only. Search
the routed tools and Enrich Arena; search suggestions stay direct-only and bulk enrichment is omitted. Search
pages are fixed at 25 upstream, so adapters cannot forward the contract `limit`; they expose
Prospeo's `pagination.total_count` while relaying the native result page. The account-information
route remains internal for key verification and capacity. See [Prospeo](prospeo.md) for pricing,
+29 -17
View File
@@ -4,6 +4,8 @@ status: implemented; live connect and core surface verified, informational capac
sources:
- src/treg/catalog/contactout.yaml
- src/treg/catalog/adapters.yaml
- src/treg/catalog/examples/contactout.people.contact.work.json
- src/treg/catalog/examples/contactout.people.contact.phone.json
- src/treg/catalog/examples/contactout.people.email.verify.json
- tests/test_routing.py
- src/treg/catalog/examples/contactout.companies.search.json
@@ -37,13 +39,16 @@ serving. No credential is committed or copied into a platform Secret row.
## Surface and selectors
Each tool has one catalog home: eight LinkedIn-specific lookup/contact tools on `linkedin`,
ten general people tools on `people`, and two company tools on `companies`. LinkedIn placement covers the three contact splits, three availability checkers,
LinkedIn profile enrichment and email-to-LinkedIn lookup. Their existing `contactout.people.*`
IDs remain stable for saved CLI/API calls; platform and capability metadata control browsing.
Capability labels distinguish work/personal email lookup from availability checks. Global catalog
search remains cross-platform. Email verification, company search and company enrichment
participate in their existing routed contracts. People search and profile enrichment remain direct-only: the PII rule excludes their
Each tool has one catalog home: two LinkedIn profile-identity tools on `linkedin`, sixteen general
people/contact tools on `people`, and two company tools on `companies`. The three contact reveals
and three availability checks live on the People shelf even though they accept a LinkedIn URL;
LinkedIn profile enrichment and email-to-LinkedIn lookup remain on the LinkedIn shelf. Existing
`contactout.people.*` IDs remain stable for saved CLI/API calls; platform and capability metadata
control browsing. Capability labels distinguish work/personal email lookup from availability
checks. Global catalog search remains cross-platform. Work-email lookup, phone lookup, email
verification, company search and company enrichment participate in their existing routed
contracts. Personal-email lookup remains direct-only because the shared email route promises work
email. People search and profile enrichment also remain direct-only: the PII rule excludes their
verification requests/examples, so their adapter registrations are omitted.
The catalog covers count, personal/work email and phone availability, single email verification,
@@ -205,8 +210,9 @@ https://api.contactout.com/#errors (checked 2026-09-08).
### Renewal and rollout
People routes carry `untestable:` with no catalog test request or stored example under the PII
rule. These entries cannot participate in automated catalog re-verification.
People routes carry `untestable:` with no catalog test request under the PII rule, so they cannot
participate in automated catalog re-verification. The two routed contact tools have only fixed,
reserved-value structural fixtures; no captured person response is retained.
`scripts/contactout_overflow_verify.py --budget-usd 10 --apply` discovers one profile
at runtime, builds requests using that ephemeral URL and required catalog selectors, compares
direct and aggregator shapes through the existing verifier, then syncs stamps.
@@ -238,22 +244,27 @@ recorded in `examples/contactout.people.email.verify.json`; no credential is inc
routing tests cover all five documented verdicts, query preservation, zero cost, provenance,
and fallback on missing verdicts or embedded errors.
## Shared discovery and profile routing
## Shared contact, discovery and profile routing
Company adapters join the existing contracts without changing capability labels. The three people
adapter registrations are omitted after removal of PII-bearing catalog fixtures; direct calls
remain available. Tests assert their absence from shared routing; direct platform tests cover
profile-only billing and own-key exclusion. No verification gate is bypassed:
Work-email and phone adapters join the existing People contracts, and company adapters join the
existing company contracts. The contact endpoints remain `untestable:` with no `test_request`, so
catalog re-verification cannot call them and recapture PII. Their adapter output maps are checked
against hand-sanitized structural fixtures containing only reserved fake contact values; the live
positive checks described above establish the real response fields. Personal-email, people-search
and profile adapters remain omitted. Direct platform tests cover profile-only billing and own-key
exclusion. No verification gate is bypassed:
| Routed tool | ContactOut child | Selected behavior |
|---|---|---|
| `treg.people.email.find` | `contactout.people.contact.work` | LinkedIn URL; `email_type=work`, `include_phone=false` |
| `treg.people.phone.find` | `contactout.people.contact.phone` | LinkedIn URL; `email_type=none`, `include_phone=true` |
| Ineligible: `treg.people.search` | `contactout.people.search` | `reveal_info=false`; domain/title/name/keyword identities; page size and location filters |
| `treg.companies.search` | `contactout.companies.search` | Domain/name/industry/technology identities; vendor page size retained |
| `treg.companies.enrich` | `contactout.companies.enrich` | One domain, sent as a one-element `domains` array |
| Ineligible: `treg.people.enrich` | `contactout.people.enrich` | LinkedIn URL or email; `include=[]` prevents contact reveal |
| Ineligible: `treg.linkedin.user.profile` | `contactout.people.linkedin.enrich` | LinkedIn URL or handle; `profile_only=true` |
These mappings do not route decision-maker search, personal-email splits, or combined-reveal
These mappings do not route decision-maker search, personal-email lookup, or combined-reveal
variants. The direct provider tools retain those capabilities. Company search does not document
a page-size control: the adapter does not forward the contract's `limit` as an invented parameter.
Its returned companies are still metered at $0.02 each. People search and domain enrichment use
@@ -271,5 +282,6 @@ company search two companies, domain enrichment one company, and person/LinkedIn
one profile each. The first sample LinkedIn URL missed, so the two successful profile captures
used a profile from that search. All captured contact arrays were empty with the above selectors.
The people captures and their catalog requests were removed after maintainer review under the
catalog PII rule; only company and email-verification examples remain. People tools are marked
`untestable:` to prevent re-capture. Other accepted company input variants were not separately live-verified.
catalog PII rule. The work-email and phone adapters instead use fixed structural fixtures with
reserved fake values; all People tools remain `untestable:` to prevent re-capture. Other accepted
company input variants were not separately live-verified.
+9 -12
View File
@@ -9,9 +9,7 @@ sources:
- src/treg/catalog/examples/dropleads.people.search.json
- src/treg/catalog/examples/dropleads.people.search.count.json
- src/treg/catalog/examples/dropleads.people.enrich.verified.json
- src/treg/catalog/examples/dropleads.people.enrich.verified.bulk.json
- src/treg/catalog/examples/dropleads.people.enrich.json
- src/treg/catalog/examples/dropleads.people.enrich.bulk.json
- src/treg/catalog/examples/dropleads.companies.search.json
- src/treg/catalog/examples/dropleads.companies.search.count.json
- src/treg/catalog/examples/dropleads.companies.enrich.json
@@ -48,11 +46,10 @@ because `CatalogTarget` explicitly allow-lists it. The platform credential is
## Public surface
The catalog exposes twelve synchronous tools: email finding, mobile finding, email verification,
people search and count, verified person enrichment and its 10-item bulk form, simple person
enrichment and its 10-item bulk form, and company search, count and enrichment. Company enrichment
accepts at most 50 combined domains and company names. These are synchronous bulk requests, so they
fit the ordinary relay; no asynchronous task model was added.
The catalog exposes ten synchronous tools: email finding, mobile finding, email verification,
people search and count, verified and simple single-person enrichment, and company search, count and
enrichment. Company enrichment accepts at most 50 combined domains and company names. The two
10-person bulk enrichment operations are omitted from the catalog.
The two single-person Try requests use a neutral name, organization and domain combination. They
show a documented identity shape without storing a user's email or LinkedIn profile in the catalog.
@@ -63,7 +60,7 @@ searchable or callable through the public catalog on BYOK or platform credential
Seven adapters join existing routed tools and therefore the corresponding Enrich Arena tasks:
work-email finding, phone finding, email verification, people search, simple person enrichment,
company search and company enrichment. Count and bulk endpoints remain direct tools. Verified
company search and company enrichment. Count endpoints remain direct tools. Verified
person enrichment has a distinct catalog capability rather than silently changing the ordinary
person-enrichment contract. Company count reuses the existing `companies.search.count` capability,
so it compares with other providers that count the same search result set.
@@ -81,8 +78,8 @@ Dropleads settlement is provider-specific. `credits_charged` is read for finder/
finite, nonnegative reported value—including zero—replaces the reservation estimate. Email Finder's
documented `status: not_found` response omits its numeric field and settles at zero explicitly.
Missing or malformed charge evidence falls back to the catalog estimate rather than inventing a
free call. Request-time reservation counts `details` for 10-item people batches, combines
`domains + companyNames` up to 50 for company enrichment, and caps company-search pagination at 50.
free call. Request-time reservation combines `domains + companyNames` up to 50 for company
enrichment and caps company-search pagination at 50.
Numeric string limits use the same bound, so a request for `"50"` cannot reserve only the default
20 rows. People search sends `filters.countries` as a list; company search sends the provider's
different `{include: [...]}` country object. Live three-way count checks proved both shapes.
@@ -104,8 +101,8 @@ The other paid rates remain documented until equivalent endpoint-specific billin
retained; their response shapes still support exact settlement when they report a charge. Tested
misses were free. People search and count agreed on totals. People search accepted a requested
limit above 50 but returned at most 50.
Both people bulk routes rejected 0 and 11 inputs; company enrichment rejected more than 50 combined
identities. Prime balance and API-host consumption can reconcile with delay, so per-call settlement
Historical discovery confirmed both omitted people-bulk routes rejected 0 and 11 inputs;
company enrichment rejected more than 50 combined identities. Prime balance and API-host consumption can reconcile with delay, so per-call settlement
uses response evidence rather than before/after wallet reads.
The same live checks confirmed that both count routes return root-level `{success, count}`. Their
+8 -9
View File
@@ -7,7 +7,6 @@ sources:
- src/treg/catalog/examples/getleadsio.people.enrich.from_linkedin.json
- src/treg/catalog/examples/getleadsio.people.enrich.from_person.json
- src/treg/catalog/examples/getleadsio.people.phone.lookup.json
- src/treg/catalog/examples/getleadsio.people.phone.lookup_batch.json
- src/treg/catalog/examples/getleadsio.people.colleagues.json
- src/treg/catalog/examples/getleadsio.people.decision_makers.json
- src/treg/catalog/examples/getleadsio.people.search.json
@@ -44,16 +43,16 @@ a positive credit balance. A team's own key wins over treg's key and is never me
## Public surface
The catalog publishes 12 direct tools: email, LinkedIn, and person enrichment; single and batch
phone lookup; colleague and decision-maker lookup; contact search and count; filter discovery; and
funding and acquisition feeds. The fair-use and contacts-health routes remain internal. CSV upload,
The catalog publishes 11 direct tools: three single-person enrichment operations, single phone
lookup, colleague and decision-maker lookup, contact search and count, filter discovery, and funding
and acquisition feeds. The phone batch operation is omitted. The fair-use and contacts-health routes remain internal. CSV upload,
asynchronous search export, and profile-monitoring state are excluded because they introduce files,
jobs, or account-owned resources that this integration does not yet model.
All 12 tools can use either a team's own key or treg's key. The catalog exposes only the original
provider operations and does not add restricted `.trial` copies. Callers choose any value that the
provider accepts for page limits, batch sizes, and other request fields. treg does not insert,
reduce, or otherwise rewrite those values. No provider-specific relay branch is added.
All 11 tools can use either a team's own key or treg's key. The catalog exposes only the original
provider operations and does not add restricted `.trial` copies. The three enrichment tools preserve the upstream `items` array but require exactly one entry on every
credential tier; invalid cardinality is rejected before relay and accepted requests are not rewritten.
Other provider-native request fields remain a faithful relay.
No GetLeads.io tool joins routed capabilities or Enrich Arena. Live synthetic misses were not a
reliable success signal: reserved invalid inputs on email, LinkedIn, and person enrichment returned
@@ -91,7 +90,7 @@ contact values, or absolute balances.
| Search count and filter values | 200 | Reported zero usage; balance unchanged |
| Contact search, two one-row pages | 200 | Distinct rows; one reported and consumed credit per page |
| Contact search, zero-row miss | 200 | Reported zero usage; balance unchanged |
| Phone lookup and phone batch, synthetic miss | 200 | No match; balance unchanged |
| Phone lookup, synthetic miss | 200 | No match; balance unchanged |
| Colleagues and decision makers, synthetic miss | 200 | Zero rows and zero reported usage; balance unchanged |
| Email, LinkedIn, and person enrichment, reserved invalid inputs | 200 | Each returned success and consumed one credit; motivates no adapter |
| Funding feed, limit one | 200 | One row and one reported/consumed credit |
+4 -10
View File
@@ -24,9 +24,6 @@ sources:
- src/treg/catalog/examples/limadata.web.extract.json
- src/treg/catalog/examples/limadata.web.research.json
- src/treg/catalog/examples/limadata.google.serp.organic.json
- src/treg/catalog/examples/limadata.people.audience.batch.start.json
- src/treg/catalog/examples/limadata.people.email.verify.batch.start.json
- src/treg/catalog/examples/limadata.people.batch.results.json
- src/treg/catalog/adapters.yaml
- src/treg/catalog/fx.yaml
- src/treg/config.py
@@ -64,13 +61,13 @@ allows `limadata`. A team's own key remains first and treg never meters it.
## Surface and shared-key boundary
`limadata.yaml` catalogs all 24 operations in the official Basic v2 OpenAPI document. Connected
teams can call every operation. Fourteen synchronous operations can use the shared key because their
`limadata.yaml` exposes 21 of the 24 operations in the official Basic v2 OpenAPI document. The two
batch submissions and their shared result reader are omitted from the catalog. Fourteen synchronous operations can use the shared key because their
successful charge is fixed and bounded: company enrichment, database autocomplete, company count,
five contact and identity lookups, email verification, company LinkedIn lookup, phone lookup, AI
research, and web search.
Ten operations stay BYOK-only:
Seven exposed operations stay BYOK-only:
- Person enrichment varies from one to 15 credits based on the identifier and optional results.
- People count requires People Database API access, which is not enabled on treg's shared account.
@@ -80,10 +77,7 @@ Ten operations stay BYOK-only:
- Identity resolution costs two credits even on HTTP 404. Generic settlement releases a hold on an
upstream error, so shared service would undercharge.
- URL extraction varies by URL count and JavaScript-rendering mode.
- Both batch submissions and batch result reads use account-scoped job identifiers. Advertising
batches refund misses only after completion.
The batch-results route is free. A live completed-job response carried `x-credits-cost: 1`, but the
The omitted batch-results route is free. A live completed-job response carried `x-credits-cost: 1`, but the
dashboard showed no polling activity or debit. That header describes task evidence and must not be
used as the current request price. No LimaData branch is added to the shared settlement runtime.
+5 -17
View File
@@ -8,11 +8,6 @@ sources:
- src/treg/catalog/examples/moltsets.linkedin.profile.search.json
- src/treg/catalog/examples/moltsets.people.email.find.name.json
- src/treg/catalog/examples/moltsets.people.enrich.name.json
- src/treg/catalog/examples/moltsets.people.email.find.json
- src/treg/catalog/examples/moltsets.people.email.find.business.json
- src/treg/catalog/examples/moltsets.people.email.find.personal.json
- src/treg/catalog/examples/moltsets.people.email.find.personal-best.json
- src/treg/catalog/examples/moltsets.people.phone.find.json
- src/treg/catalog/examples/moltsets.people.enrich.email.json
- src/treg/catalog/examples/moltsets.people.enrich.linkedin.json
- src/treg/catalog/examples/moltsets.people.audiences.maid.json
@@ -55,7 +50,7 @@ caller value without adding provider logic to the relay. A team's own key wins a
by treg. `platform_key_moltsets` supplies the optional shared credential, and the existing provider
allow-list remains the production switch. This integration does not change that switch.
The catalog exposes all 17 documented data operations: people and company search, name/company
The catalog exposes 12 of 17 documented data operations: people and company search, name/company
lookups, four profile-to-email variants, phone lookup, two reverse enrichments, three audience/hash
lookups, email-to-profile, and IP-to-company. Every operation was exercised with synthetic inputs.
MoltSets returns HTTP 200 for both hits and misses; `status: ok` is a hit and
@@ -76,20 +71,13 @@ Nine single-result tools are safe shared-key offers: business email and profile
email and profile lookup, three audience/hash conversions, email-to-profile, and IP-to-company.
They reserve one ordinary record and settle only when `status` is `ok`.
Eight tools are BYOK-only:
Three exposed tools are BYOK-only:
- People search, company search, and profile search can return variable record counts; profile
search also has a free `count_only` mode.
- Four profile-to-email tools accept either one URL or batches of up to 100, whose entries can have
mixed hit/miss outcomes.
- Phone lookup accepts the same batch shape and has two meters on a hit: one ordinary record plus
one phone token.
The current generic response contract cannot count these variable or per-item outcomes, and the
request guard cannot safely promise single-only input for a body that documents both fields. They
remain faithful, unmetered BYOK relays rather than gaining MoltSets-specific call or money logic.
Phone pricing is displayed as the documented upper-bound replacement cost: $0.50 phone token plus
the $0.01 ordinary record, or $0.51 per hit. Misses cost neither meter.
The four hybrid profile-to-email operations and hybrid phone lookup are omitted because each accepts
one URL or a batch of up to 100 with mixed hit/miss outcomes. They need a separately designed scalar
or confirmed-batch contract before agents can call them.
## Routing and Arena
+7 -15
View File
@@ -9,13 +9,6 @@ sources:
- src/treg/catalog/examples/openmart.businesses.lookup.google-place.json
- src/treg/catalog/examples/openmart.companies.enrich.json
- src/treg/catalog/examples/openmart.companies.search.json
- src/treg/catalog/examples/openmart.people.find.batch.json
- src/treg/catalog/examples/openmart.technologies.find.batch.json
- src/treg/catalog/examples/openmart.companies.email.find.batch.json
- src/treg/catalog/examples/openmart.people.enrich.batch.json
- src/treg/catalog/examples/openmart.tasks.batch.status.json
- src/treg/catalog/examples/openmart.tasks.batch.ids.json
- src/treg/catalog/examples/openmart.tasks.get.json
- src/treg/catalog/examples/openmart.deny-rules.create.json
- src/treg/catalog/examples/openmart.deny-rules.check.json
- src/treg/catalog/examples/openmart.deny-rules.delete.json
@@ -53,8 +46,8 @@ related:
Openmart is a pasted Bearer-key enrichment provider at `https://api.openmart.ai`. The free
`GET /api/v2/credit-balance` operation verifies a connection and supplies the capacity collector.
The catalog exposes 16 caller-facing operations: business and brand search, two ID lookups, company
enrichment, four batch submission types, three task reads, and three deny-rule operations. The
The catalog exposes nine caller-facing operations: business and brand search, two ID lookups,
company enrichment, and three deny-rule operations. The
balance route is deliberately not a tool: it is internal connection/capacity infrastructure.
## Shared-key boundary
@@ -73,10 +66,10 @@ records hold eight credits. Own-key calls bypass the guard and retain the upstre
catalog access check prices each endpoint's runnable example with this same Openmart-specific
formula instead of the generic 20-row estimate.
The other 11 operations remain BYOK-only. Fast ID search has no proven fractional price. The four
batch submissions and three task reads are one delayed, account-owned lifecycle whose charges
cannot be assigned safely by a synchronous response. The three deny-rule operations read or mutate
private account state and forbid caching. The shared balance route is absent from the catalog so no
The other four exposed operations remain BYOK-only. Fast ID search has no proven fractional price,
and the three deny-rule operations read or mutate private account state and forbid caching. Four
batch submissions and three task reads are omitted because their delayed, account-owned lifecycle
needs explicit cost confirmation and durable ownership. The shared balance route is absent from the catalog so no
caller can inspect operational inventory.
The catalog records the active subscription conversion of $149 for 5,000 credits, or $0.0298 per
@@ -93,8 +86,7 @@ Openmart tools are direct-call only: none has a routing adapter and none appears
The direct platform-eligible tools remain callable with treg's key. A live nonsense business query
returned an unrelated fallback row with `match_score: 0`, so direct callers must treat that score as
a miss; the result is not safe for automatic selection. Company enrichment can return several
location matches, so choosing the first would also change semantics. People operations are
asynchronous and do not join the synchronous people routes or Arena tasks.
location matches, so choosing the first would also change semantics. Asynchronous people operations are omitted and do not join the synchronous people routes or Arena tasks.
## Capacity and evidence
+8 -18
View File
@@ -6,10 +6,8 @@ sources:
- src/treg/catalog/examples/prospeo.people.email.find.json
- src/treg/catalog/examples/prospeo.people.phone.find.json
- src/treg/catalog/examples/prospeo.people.enrich.json
- src/treg/catalog/examples/prospeo.people.enrich.bulk.json
- src/treg/catalog/examples/prospeo.people.search.json
- src/treg/catalog/examples/prospeo.companies.enrich.json
- src/treg/catalog/examples/prospeo.companies.enrich.bulk.json
- src/treg/catalog/examples/prospeo.companies.search.json
- src/treg/catalog/examples/prospeo.search.suggestions.json
- src/treg/catalog/adapters.yaml
@@ -46,15 +44,14 @@ metered by treg.
## Public surface and routing
The public catalog contains nine tools: email finding, phone finding, person enrichment, 50-person
bulk enrichment, person search, company enrichment, 50-company bulk enrichment, company search and
search suggestions. `/account-information` remains internal to connection verification and capacity
The public catalog contains seven tools: email finding, phone finding, person enrichment, person
search, company enrichment, company search and search suggestions. `/account-information` remains internal to connection verification and capacity
collection; callers cannot use the catalog to inspect either their own or treg's account balance.
Six fixture-verified adapters join routed capabilities: `people.email.find`, `people.phone.find`,
`people.enrich`, `people.search`, `companies.enrich` and `companies.search`. The corresponding
single-record tasks participate in Enrich Arena where that capability has a task. Bulk endpoints and
suggestions remain direct tools because their contracts are not scalar enrichment results.
`people.enrich`, `people.search`, `companies.enrich` and `companies.search`. The corresponding single-record tasks participate in Enrich Arena where that capability has a task.
Suggestions remain a direct tool because its contract is not a scalar enrichment result. The two
50-record bulk enrichment operations are omitted from the catalog.
The three single-person tools deliberately share `/enrich-person` with different fixed selectors.
Email finding requires a verified email and disables mobile enrichment. Phone finding requires a
@@ -66,24 +63,19 @@ still relays the caller's request without treg metering.
The platform conversion in `fx.yaml` is the public Starter list price: $49 / 2,000 credits, or
$0.0245 per credit before the configured platform margin. Single email, person and company hits cost
one credit. A non-free phone hit has a fixed ten-credit platform price. Search charges one credit for
a non-empty page; suggestions are free. Bulk responses report the authoritative `total_cost`.
one credit. A non-free phone hit has a fixed ten-credit platform price. Search charges one credit for a non-empty page; suggestions are free.
The phone response does not expose whether Prospeo internally charged nine incremental credits after
a prior email reveal or ten credits for a fresh combined reveal. treg therefore advertises, reserves
and settles a predictable ten-credit price for every non-free phone hit. `free_enrichment=true`
still settles zero. Bulk mobile reservation reads the nine-credit rider from
`cost.modifiers.enrich_mobile.add_credits_per_result`; `resolve._credit_modifiers` owns the generic
arithmetic, so Python contains no Prospeo credit multiplier.
`settle._prospeo_cost_micro` requires endpoint-specific success evidence. Email finding requires a
still settles zero. `settle._prospeo_cost_micro` requires endpoint-specific success evidence. Email finding requires a
non-empty `person.email.email`; person enrichment charges one credit for a non-empty person object
when `free_enrichment=false`, because email is optional in profile mode; phone finding requires a
non-empty `person.mobile.mobile_international`; company enrichment requires a company object. A
recognizable email- or phone-finder field-level miss settles zero even when an envelope contains
`free_enrichment=false`. A malformed or wrongly typed single-enrichment object returns no
observation and retains the frozen estimate for reconciliation. Explicit provider errors settle
zero, searches use `free` plus the result list, and bulk calls use finite, nonnegative `total_cost`.
zero, and searches use `free` plus the result list.
The non-free person-with-null-email shape was not observed in the live evidence pass. Treating
`free_enrichment=false` plus a non-empty person as one charged credit is the conservative billing
@@ -129,8 +121,6 @@ operator record.
| `prospeo.people.enrich` | Public profile with unavailable email | 200 | `B → B` | person object and `free_enrichment=true` |
| `prospeo.people.enrich` | Three fresh public profiles with unavailable email | 200 each | `B → B` each | non-empty person, null/absent email and `free_enrichment=true` in all three |
| `prospeo.people.phone.find` | Prior email-reveal profile | 200 | `B → B-9` | mobile present, no numeric charge field; motivates fixed ten-credit platform price |
| `prospeo.people.enrich.bulk` | One-record repeat | 200 | `B → B` | exact `total_cost=0` |
| `prospeo.companies.enrich.bulk` | One-record repeat | 200 | `B → B` | exact `total_cost=0` |
A second end-to-end pass went through local treg rather than directly to Prospeo. Suggestions returned
`X-Treg-Cost-Micro: 0`; a company enrichment reserved and settled 24,500 micro-USD; a deduplicated
+7 -14
View File
@@ -1,13 +1,9 @@
---
title: Scrubby — quick and deep email verification
status: implemented; live authentication, billing and asynchronous behavior verified
title: Scrubby — single email verification
status: shipped
sources:
- src/treg/catalog/scrubby.yaml
- src/treg/catalog/examples/scrubby.people.email.verify.json
- src/treg/catalog/examples/scrubby.people.email.verify.bulk.json
- src/treg/catalog/examples/scrubby.people.email.verify.bulk.results.json
- src/treg/catalog/examples/scrubby.people.email.verify.deep.json
- src/treg/catalog/examples/scrubby.people.email.verify.deep.results.json
- src/treg/catalog/adapters.yaml
- src/treg/catalog/fx.yaml
- src/treg/config.py
@@ -41,17 +37,14 @@ Python urllib client profile. `TREG_PLATFORM_KEY_SCRUBBY` enables shared-key acc
## Surface and routing
The catalog exposes the complete five-operation OpenAPI surface: synchronous single verification,
quick-bulk submit and fetch, and deep-bulk submit and fetch. The single tool adapts to the existing
The catalog exposes only synchronous single verification. It adapts to the existing
`people.email.verify` contract and therefore participates in normal routing and Enrich Arena.
`Valid` maps to `valid=true`; `Invalid`, `Risky`, and `Unknown` are useful negative or uncertain
answers rather than misses.
Only the single tool can use treg's shared key. The four bulk lifecycle tools are BYOK-only. Scrubby
publishes no batch-size maximum, so treg cannot bound a shared-key reservation. Its poll identifier
is also carried in a JSON body, while the existing shared-key ownership guard authorizes declared
request parameters. The catalog does not invent a limit or weaken task ownership to add bulk
platform access.
The four bulk lifecycle operations are omitted from the catalog. Scrubby publishes no batch-size
maximum, so one agent call can spend an unbounded number of credits; the delayed result lifecycle
also needs explicit ownership and completion handling.
## Pricing and settlement
@@ -69,7 +62,7 @@ with `credits_used=0`. treg's real upstream timeout is 180 seconds: a fresh rese
completed successfully after 108.5 seconds and reported one credit. The public OpenAPI statement
that the server returns a refunded 408 after 15 seconds does not match this observed behavior.
## Async behavior and capacity
## Excluded async behavior and capacity
Quick bulk returned 202, charged exactly the number of fresh inputs, and exposed an identifier plus
a 30-second retry interval. Immediate polling returned processing; a later cached batch returned
+22
View File
@@ -216,6 +216,27 @@ def check_strict_query(ep: dict, where: str, errors: list[str]) -> None:
fail(errors, where, f"strict query field {name} enum must contain strings")
def check_strict_body(ep: dict, where: str, errors: list[str]) -> None:
if "strict_body" not in ep:
return
if ep["strict_body"] is not True:
fail(errors, where, "strict_body must be true when present")
return
fields = (ep.get("input") or {}).get("body")
arrays = [
spec for spec in (fields or {}).values()
if isinstance(spec, dict) and str(spec.get("type") or "").startswith("array")
]
if ep.get("method") not in {"POST", "PUT", "PATCH"} or not arrays:
fail(errors, where, "strict_body requires a body method with a declared array field")
return
for spec in arrays:
minimum = spec.get("minItems", spec.get("min"))
maximum = spec.get("maxItems", spec.get("max"))
if not isinstance(minimum, int) or not isinstance(maximum, int) or minimum > maximum:
fail(errors, where, "strict_body array fields require valid integer min/max bounds")
def check_platform_request(rule: object, input_schema: object, where: str,
errors: list[str]) -> None:
"""Platform-only fixed body values; BYOK input remains an upstream contract."""
@@ -921,6 +942,7 @@ def main(argv: list[str]) -> int:
check_status_marker(ep, where, endpoint_status, errors)
inp = ep.get("input") or {}
check_strict_query(ep, where, errors)
check_strict_body(ep, where, errors)
check_platform_auth(ep, where, errors)
if "platform_request" in ep:
check_platform_request(ep["platform_request"], inp, where, errors)
+40 -22
View File
@@ -658,14 +658,6 @@ def _marketplace_pricing(
doc = _json_object(body)
rate = float(cost.get("usd") or 0)
unit = _usd_to_micro(rate)
if endpoint_id in (
"dropleads.people.enrich.verified.bulk",
"dropleads.people.enrich.bulk",
):
details = doc.get("details")
count = len(details) if isinstance(details, list) else 1
# More than 10 is rejected before charging; reserve the maximum valid request.
return _usd_to_micro(rate * max(1, min(count, 10))), unit
if endpoint_id == "dropleads.companies.enrich":
domains = doc.get("domains") if isinstance(doc.get("domains"), list) else []
names = doc.get("companyNames") if isinstance(doc.get("companyNames"), list) else []
@@ -727,14 +719,7 @@ def _marketplace_pricing(
credits = -(-asked // 10) # ceil division: whole credits, minimum 1
return _usd_to_micro(credits * rate), _usd_to_micro(rate)
return estimate, unit
record_count = None
if provider == "prospeo" and endpoint_id in (
"prospeo.people.enrich.bulk",
"prospeo.companies.enrich.bulk",
):
records = _json_object(body).get("data")
record_count = max(1, min(len(records) if isinstance(records, list) else 1, 50))
if provider != "aviato" and not cost.get("modifiers") and record_count is None:
if provider != "aviato" and not cost.get("modifiers"):
return estimate, unit
# Credit-priced providers with a `cost.modifiers` block (Aviato, cloro): the request decides
@@ -751,12 +736,6 @@ def _marketplace_pricing(
return 0, 0
credits = float(cost.get("value") or 0) + added
settled_credits = float(cost.get("value") or 0) + settled_added
if record_count is not None:
# Prospeo's bulk routes price the base and optional mobile rider per submitted record.
# The request shape selects the count; every credit number remains catalog-declared.
return credit_micro(credits + per_result) * record_count, credit_micro(
float(cost.get("value") or 0)
)
if endpoint_id in ("aviato.companies.enrich.bulk", "aviato.people.enrich.bulk"):
lookups = doc.get("lookups") if isinstance(doc.get("lookups"), list) else []
per_record = credit_micro(credits)
@@ -1163,6 +1142,44 @@ def _enforce_catalog_query(ep: dict, query: QueryValues, has_body: bool) -> None
)
def _enforce_catalog_body(ep: dict, body: bytes) -> None:
"""Enforce opt-in array cardinality without rewriting a catalog request.
Most catalog schemas describe the upstream API and deliberately leave BYOK requests as a
faithful relay. ``strict_body`` is the narrow exception for a catalog tool whose advertised
contract is intentionally smaller than the upstream surface. Array limits are read from the
existing input declaration and applied on every credential tier.
"""
if not ep.get("strict_body"):
return
document = _strict_json_object(body, ep["id"])
fields = (ep.get("input") or {}).get("body") or {}
for name, spec in fields.items():
if not isinstance(spec, dict) or not str(spec.get("type") or "").startswith("array"):
continue
value = document.get(name)
minimum = spec.get("minItems", spec.get("min"))
maximum = spec.get("maxItems", spec.get("max"))
valid = isinstance(value, list)
if valid and isinstance(minimum, int):
valid = len(value) >= minimum
if valid and isinstance(maximum, int):
valid = len(value) <= maximum
if not valid:
expected = (
f"between {minimum} and {maximum}" if minimum != maximum
else f"exactly {minimum}"
)
raise ResolutionFailed(
"catalog_parameter_invalid", status_code=400, detail={
"error": "catalog_parameter_invalid",
"endpoint_id": ep["id"],
"parameter": f"body.{name}",
"message": f"{ep['id']} requires {expected} item in body.{name}",
},
)
def _enforce_platform_request(ep: dict, body: bytes) -> None:
"""Check explicit platform constraints and fixed pricing selectors before reserve/relay.
@@ -1543,6 +1560,7 @@ async def _resolve_marketplace_call(
upstream, consumed = _marketplace_upstream(ep, provider, query, chosen_method)
body = await read_body() if has_body else b""
_enforce_catalog_body(ep, body)
phash = _params_hash(ep["id"], query.multi_items(), body)
# The catalog's estimate travels on EVERY tier - informational on tiers 1/2 (the provider bills
# the org's own account; Activity shows "estimated") and the reserve amount on tier 4 only
+1 -10
View File
@@ -174,20 +174,11 @@ def _quickenrich_cost_micro(mk: MarketplaceCall, doc: dict) -> int | None:
def _prospeo_cost_micro(mk: MarketplaceCall, doc: dict) -> int | None:
"""Settle from Prospeo's dedupe flags, endpoint success field, and exact bulk charge."""
"""Settle from Prospeo's dedupe flags and endpoint-specific success fields."""
if doc.get("error") is True:
return 0
if doc.get("error") is not False:
return None
if mk.endpoint_id in (
"prospeo.people.enrich.bulk",
"prospeo.companies.enrich.bulk",
):
credits = doc.get("total_cost")
if (isinstance(credits, (int, float)) and not isinstance(credits, bool)
and math.isfinite(credits) and credits >= 0):
return int(credits * mk.unit_micro + 0.5)
return None
if mk.endpoint_id in ("prospeo.people.search", "prospeo.companies.search"):
if doc.get("free") is True:
return 0
+18 -2
View File
@@ -15,6 +15,13 @@
adapters:
# ---- people.email.find --------------------------------------------------------------------
contactout.people.contact.work:
accepts: [[linkedin_url]]
in: {linkedin_url: queryParams.profile}
const: {queryParams.email_type: work, queryParams.include_phone: false}
test_identity: {linkedin_url: "https://www.linkedin.com/in/routing-fixture"}
out: {email: "profile.work_email[0]"}
miss: "at_least(status_code != 200, coalesce(profile.work_email) == null)"
dropleads.people.email.find:
accepts: [[first_name, last_name, domain]]
in: {first_name: body.first_name, last_name: body.last_name, domain: body.company_domain}
@@ -258,6 +265,13 @@ adapters:
miss: "data.creator_info.unique_id == null"
# ---- people.phone.find --------------------------------------------------------------------
contactout.people.contact.phone:
accepts: [[linkedin_url]]
in: {linkedin_url: queryParams.profile}
const: {queryParams.email_type: none, queryParams.include_phone: true}
test_identity: {linkedin_url: "https://www.linkedin.com/in/routing-fixture"}
out: {phone: "profile.phone[0]", line_type: "'mobile'"}
miss: "at_least(status_code != 200, coalesce(profile.phone) == null)"
dropleads.people.phone.find:
accepts: [[linkedin_url]]
in: {linkedin_url: body.linkedin_url}
@@ -2352,8 +2366,10 @@ adapters:
# ContactOut people search/profile adapters are omitted: the PII rule excludes
# their verification fixtures. Direct tools remain available. Company discovery:
# ContactOut work-email and phone lookups use sanitized structural fixtures above. People
# search/profile adapters remain omitted because their PII-bearing responses have no safe
# routing fixture. Personal email stays direct-only because the shared email contract is work-only.
# Company discovery:
contactout.companies.search:
accepts: [[domain], [name], [industry], [technology]]
in: {domain: body.domain, name: body.name, industry: body.industry, technology: body.technologies}
+1 -215
View File
@@ -1,6 +1,6 @@
# AI Ark documents 21 callable operations. treg exposes the V2 single-email and mobile routes and
# omits their duplicate V1 forms. The free credit route is internal to connection verification and
# capacity collection. Stateful lists and both asynchronous job families remain BYOK-only.
# capacity collection. Stateful lists remain BYOK-only; both asynchronous job families are omitted.
provider: aiark
source:
docs: https://docs.ai-ark.com/
@@ -12,8 +12,6 @@ proposed_capabilities:
people.preview: Preview masked people-search results for a flat page price
people.personality.analyze: Generate DISC, OCEAN and outreach guidance for a profile
people.lists.upsert: Create or update a temporary exclusion list
people.export: Start and inspect an asynchronous people-and-email export
people.email.find.bulk: Find emails for a prior people-search result set
endpoints:
- id: aiark.people.search
capability: people.search
@@ -258,215 +256,3 @@ endpoints:
verified: '2026-09-17'
example_response: examples/aiark.lists.upsert.json
cache: forbidden
- id: aiark.people.export.start
capability: people.export
platform: people
scope: own_account
domain: contacts
method: POST
path: /v1/people/export
name: Start a people export with emails
summary: Queue up to 10,000 filtered people with real-time verified emails.
platform_blocked: Async ownership, delayed refunds and terminal settlement are not safe on a shared AI Ark account. Connect your own key.
input:
<<: *people_search_input
body:
account: {type: object, required: false}
contact: {type: object, required: false}
lists: {type: object, required: false}
page: {type: integer, required: true, min: 0}
size: {type: integer, required: true, min: 1, max: 10000}
webhook: {type: string, required: true, format: uri}
note: Poll aiark.people.export.statistics, then fetch aiark.people.export.results. Undelivered jobs can auto-refund after up to ten hours.
cost:
type: per_result
value: 1
currency: credit
per: 1
unit: record
source: docs
source_url: https://docs.ai-ark.com/reference/people-export-with-email
checked: '2026-09-17'
confidence: documented
note: "BYOK only. Maximum one credit per exported person: 0.5 for the profile plus 0.5 when a valid email is found."
docs_url: https://docs.ai-ark.com/reference/people-export-with-email
untestable: A non-routable webhook was rejected before billing; successful async settlement and delayed refunds remain unproved.
cache: forbidden
- id: aiark.people.export.results
capability: people.export
platform: people
scope: own_account
domain: contacts
method: GET
path: /v1/people/export/{trackId}/inquiries
name: Fetch people export results
summary: Fetch a page of completed people and email export results.
platform_blocked: Track ids are account-scoped and lack shared-key caller ownership. Connect your own AI Ark key.
input: &result_page_input
pathParams: {trackId: {type: string, required: true}}
query:
page: {type: integer, required: false, default: 0, min: 0}
size: {type: integer, required: false, default: 10, min: 1, max: 100}
cost: *free_cost
docs_url: https://docs.ai-ark.com/reference/export-people-results-by-track-id
untestable: Requires a track id created by the caller's own AI Ark account.
cache: forbidden
- id: aiark.people.export.statistics
capability: people.export
platform: people
scope: own_account
domain: contacts
method: GET
path: /v1/people/export/{trackId}/statistics
name: Get people export statistics
summary: Poll export state and totals until the job is done or refunded.
platform_blocked: Track ids are account-scoped and lack shared-key caller ownership. Connect your own AI Ark key.
input: {pathParams: {trackId: {type: string, required: true}}}
cost: *free_cost
docs_url: https://docs.ai-ark.com/reference/get-export-people-statistics-by-track-id
untestable: Requires a track id created by the caller's own AI Ark account.
cache: forbidden
- id: aiark.people.export.submissions
kind: utility
capability: people.export
platform: people
scope: own_account
method: GET
path: /v1/people/export/submissions
name: List people export submissions
summary: List the connected account's export jobs and refund states.
platform_blocked: Submission history belongs to the connected AI Ark account. Connect your own key.
input: &submissions_input
query:
state: {type: string, required: false}
fullyRefunded: {type: boolean, required: false}
page: {type: integer, required: false, default: 0, min: 0}
size: {type: integer, required: false, default: 10, min: 1, max: 100}
sort: {type: string, required: false}
cost: *free_cost
docs_url: https://docs.ai-ark.com/reference/get-export-people-submissions
test_request: {query: {page: 0, size: 1}}
verified: '2026-09-17'
example_response: examples/aiark.people.export.submissions.json
cache: forbidden
- id: aiark.people.export.webhook.resend
kind: utility
capability: people.export
platform: people
scope: own_account
method: PATCH
path: /v1/people/export/{trackId}/notify
name: Resend a people export webhook
summary: Ask AI Ark to send an export completion webhook again.
platform_blocked: Track ids and webhook delivery belong to the connected AI Ark account. Connect your own key.
input: &resend_input
pathParams: {trackId: {type: string, required: true}}
bodyType: json
body: {webhook: {type: string, required: true, format: uri}}
note: HTTP 200 can still contain delivered false. Inspect the response body.
cost: *free_cost
docs_url: https://docs.ai-ark.com/reference/resend-export-people-webhook-1
untestable: Requires an export track id and sends data to a caller-selected external URL.
cache: forbidden
- id: aiark.people.email.find.bulk
capability: people.email.find.bulk
platform: people
scope: own_account
domain: contacts
method: POST
path: /v1/people/email-finder
name: Start email finding for search results
summary: Find verified emails for one prior people-search result set.
platform_blocked: A track id is single-use, account-scoped and can refund later. Connect your own AI Ark key.
input:
bodyType: json
body:
trackId: {type: string, required: true}
webhook: {type: string, required: true, format: uri}
note: Use a trackId once within six hours of People Search. Poll the statistics tool and then fetch results.
cost:
type: per_result
value: 1
currency: credit
per: 1
unit: record
source: docs
source_url: https://docs.ai-ark.com/reference/people-email-finder-by-track-id
checked: '2026-09-17'
confidence: documented
note: BYOK only. One credit per found valid email and zero for each miss. Undelivered jobs can auto-refund after up to ten hours.
docs_url: https://docs.ai-ark.com/reference/people-email-finder-by-track-id
untestable: A non-routable webhook was rejected before billing; successful async settlement and delayed refunds remain unproved.
cache: forbidden
- id: aiark.people.email.find.bulk.results
capability: people.email.find.bulk
platform: people
scope: own_account
domain: contacts
method: GET
path: /v1/people/email-finder/{trackId}/inquiries
name: Fetch email finder results
summary: Fetch a page of email-finder inputs, states and verified outputs.
platform_blocked: Track ids are account-scoped and lack shared-key caller ownership. Connect your own AI Ark key.
input: *result_page_input
cost: *free_cost
docs_url: https://docs.ai-ark.com/reference/get-email-finder-results-by-track-id
untestable: Requires a track id created by the caller's own AI Ark account.
cache: forbidden
- id: aiark.people.email.find.bulk.statistics
capability: people.email.find.bulk
platform: people
scope: own_account
domain: contacts
method: GET
path: /v1/people/email-finder/{trackId}/statistics
name: Get email finder statistics
summary: Poll email-finder state and totals until the job is done or refunded.
platform_blocked: Track ids are account-scoped and lack shared-key caller ownership. Connect your own AI Ark key.
input: {pathParams: {trackId: {type: string, required: true}}}
cost: *free_cost
docs_url: https://docs.ai-ark.com/reference/get-email-finder-statistics-by-track-id
untestable: Requires a track id created by the caller's own AI Ark account.
cache: forbidden
- id: aiark.people.email.find.bulk.submissions
kind: utility
capability: people.email.find.bulk
platform: people
scope: own_account
method: GET
path: /v1/people/email-finder/submissions
name: List email finder submissions
summary: List the connected account's email-finder jobs and refund states.
platform_blocked: Submission history belongs to the connected AI Ark account. Connect your own key.
input: *submissions_input
cost: *free_cost
docs_url: https://docs.ai-ark.com/reference/get-email-finder-submissions
test_request: {query: {page: 0, size: 1}}
verified: '2026-09-17'
example_response: examples/aiark.people.email.find.bulk.submissions.json
cache: forbidden
- id: aiark.people.email.find.bulk.webhook.resend
kind: utility
capability: people.email.find.bulk
platform: people
scope: own_account
method: PATCH
path: /v1/people/email-finder/{trackId}/notify
name: Resend an email finder webhook
summary: Ask AI Ark to send an email-finder completion webhook again.
platform_blocked: Track ids and webhook delivery belong to the connected AI Ark account. Connect your own key.
input: *resend_input
cost: *free_cost
docs_url: https://docs.ai-ark.com/reference/resend-email-finder-webhook-1
untestable: Requires an email-finder track id and sends data to a caller-selected external URL.
cache: forbidden
+3 -140
View File
@@ -1,10 +1,6 @@
# Complete documented surface: standard/waterfall single verification, single status, JSON and
# CSV bulk submission, bulk status/emails/dump/export/destroy, Check API, and Account API.
# Excluded from direct tools: multipart CSV upload (no catalog file-upload contract), destructive
# bulk destroy, and /v1/check (a separate unfunded Check Plan with no supplied acquisition price).
# Webhook parameters remain available on BYOK-only bulk and waterfall submissions. The platform
# single tool omits them and omits disable_catchall_verify so every 2xx submission has the proven
# fixed one-credit price.
# Exposes standard/waterfall single verification, single status, and account usage. JSON and CSV
# bulk lifecycles are omitted until they have an explicit cost-confirmation and owned-job workflow.
# /v1/check is also omitted because it uses a separate unfunded Check Plan.
provider: bounceban
source:
docs: https://bounceban.com/public/doc/api.html
@@ -14,11 +10,6 @@ pricing_url: https://bounceban.com/pricing
limits: "Live account limits on 2026-09-16: single and single-status 100/second, bulk create 5/second, bulk reads 25/second, account 5/second. Results remain available for 90 days."
proposed_capabilities:
people.email.verify.status: Poll a single email-verification task
people.email.verify.bulk: Submit a list for asynchronous email verification
people.email.verify.bulk.status: Read bulk email-verification progress and totals
people.email.verify.bulk.emails: Read selected results from a bulk email-verification task
people.email.verify.bulk.dump: List cursor-paginated bulk email-verification results
people.email.verify.bulk.export: Create a CSV download link for bulk email-verification results
endpoints:
- id: bounceban.people.email.verify
@@ -106,134 +97,6 @@ endpoints:
verified: '2026-09-16'
example_response: examples/bounceban.people.email.verify.status.json
docs_url: https://bounceban.com/public/doc/api.html#tag/Single-Verification
- id: bounceban.people.email.verify.bulk
domain: contacts
capability: people.email.verify.bulk
platform: people
scope: any_account
method: POST
path: /v1/verify/bulk
name: Start bulk email verification
summary: Submit a JSON list of emails for asynchronous verification
input:
bodyType: json
body:
emails: {type: "array[string]", required: true, min: 1, max: 500000, example: ["dev@bounceban.com", "support@bounceban.com"]}
name: {type: string, required: false}
mode: {type: string, required: false, enum: [regular, deepverify], default: regular}
greylisting_bypass: {type: string, required: false, enum: [auto, speed, robust], default: auto}
disable_catchall_verify: {type: string, required: false, enum: ['0', '1'], default: '0'}
url: {type: string, required: false, note: "BYOK webhook URL for each verified email"}
url_finished: {type: string, required: false, note: "BYOK webhook URL for task completion"}
note: "BYOK only. Unknown results are refunded only when the task finishes. Recommended maximum is 500,000 emails."
test_request: {body: {emails: ["dev@bounceban.com"], name: "catalog-check"}}
cost:
type: per_result
value: 1
currency: credit
per: 1
unit: result
source: docs
source_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
checked: '2026-09-16'
confidence: documented
note: "One credit per final non-unknown result. Unknown results are refunded when the task finishes. BYOK is not metered by treg."
platform_blocked: "Final-result refunds and shared-key task ownership require durable task settlement; connect your own BounceBan key."
verified: '2026-09-16'
example_response: examples/bounceban.people.email.verify.bulk.json
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
- id: bounceban.people.email.verify.bulk.status
kind: utility
domain: contacts
capability: people.email.verify.bulk.status
platform: people
scope: any_account
method: GET
path: /v1/verify/bulk/status
name: Get bulk verification status
summary: Read progress, result counts, and final credit usage for a bulk task
input:
queryParams:
id: {type: string, required: true, example: "sanitized-bulk-id"}
test_request: {queryParams: {id: "sanitized-bulk-id"}}
cost: &free_bulk_read {type: free, value: 0, currency: USD, unit: call, confidence: verified, note: "Live bulk task reads used no credits"}
platform_blocked: &bulk_ownership "Shared-key task ownership is not available; connect your own BounceBan key."
verified: '2026-09-16'
example_response: examples/bounceban.people.email.verify.bulk.status.json
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
- id: bounceban.people.email.verify.bulk.emails
kind: utility
domain: contacts
capability: people.email.verify.bulk.emails
platform: people
scope: any_account
method: POST
path: /v1/verify/bulk/emails
name: Get selected bulk verification results
summary: Read results for up to 100 emails in a completed bulk task
input:
bodyType: json
body:
id: {type: string, required: true, example: "sanitized-bulk-id"}
emails: {type: "array[string]", required: true, min: 1, max: 100, example: ["dev@bounceban.com"]}
test_request: {body: {id: "sanitized-bulk-id", emails: ["dev@bounceban.com"]}}
cost: *free_bulk_read
platform_blocked: *bulk_ownership
verified: '2026-09-16'
example_response: examples/bounceban.people.email.verify.bulk.emails.json
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
- id: bounceban.people.email.verify.bulk.dump
kind: utility
domain: contacts
capability: people.email.verify.bulk.dump
platform: people
scope: any_account
method: GET
path: /v1/verify/bulk/dump
name: List bulk verification results
summary: Read a cursor-paginated page of verified emails from a bulk task
input:
queryParams:
id: {type: string, required: true, example: "sanitized-bulk-id"}
state: {type: string, required: false, enum: [deliverable, risky, undeliverable, unknown]}
cursor: {type: string, required: false, note: "opaque cursor from the previous response"}
page_size: {type: integer, required: false, min: 100, max: 3000, default: 100}
retrieve_all: {type: string, required: false, enum: ['0', '1'], default: '0', note: "1 returns all rows only for tasks with at most 20,000 emails"}
note: "Request without cursor, then repeat with the returned cursor until it is null."
test_request: {queryParams: {id: "sanitized-bulk-id", page_size: 100}}
cost: *free_bulk_read
platform_blocked: *bulk_ownership
verified: '2026-09-16'
example_response: examples/bounceban.people.email.verify.bulk.dump.json
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
- id: bounceban.people.email.verify.bulk.export
kind: utility
domain: contacts
capability: people.email.verify.bulk.export
platform: people
scope: any_account
method: POST
path: /v1/verify/bulk/export
name: Create a bulk-results download link
summary: Create a four-hour CSV download link for a completed bulk task
input:
bodyType: json
body:
id: {type: string, required: true, example: "sanitized-bulk-id"}
keep_all_rows: {type: boolean, required: false, default: false}
criteria: {type: object, required: false, note: "optional deliverable, risky, risky_score, undeliverable, unknown, free, role, accept_all, and disposable filters"}
note: "Link creation can take time. Retry no more than 15 times per export in 15 minutes. The returned public link expires after four hours."
test_request: {body: {id: "sanitized-bulk-id"}}
cost: {type: free, value: 0, currency: USD, unit: call, confidence: documented, note: "The documented export-link operation does not consume verification credits"}
platform_blocked: *bulk_ownership
example_response: examples/bounceban.people.email.verify.bulk.export.json
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
- id: bounceban.account.usage
kind: account
domain: account
+22 -21
View File
@@ -109,8 +109,8 @@ endpoints:
docs_url: https://api.contactout.com/
- id: contactout.people.personal_email.available
domain: contacts
capability: linkedin.email.personal.availability
platform: linkedin
capability: people.email.personal.availability
platform: people
scope: any_account
method: GET
path: /v1/people/linkedin/personal_email_status
@@ -137,8 +137,8 @@ endpoints:
verification request or stored example under the catalog PII rule.
- id: contactout.people.work_email.available
domain: contacts
capability: linkedin.email.work.availability
platform: linkedin
capability: people.email.work.availability
platform: people
scope: any_account
method: GET
path: /v1/people/linkedin/work_email_status
@@ -160,8 +160,8 @@ endpoints:
verification request or stored example under the catalog PII rule.
- id: contactout.people.phone.available
domain: contacts
capability: linkedin.phone.availability
platform: linkedin
capability: people.phone.availability
platform: people
scope: any_account
method: GET
path: /v1/people/linkedin/phone_status
@@ -645,8 +645,8 @@ endpoints:
verification request or stored example under the catalog PII rule.
- id: contactout.people.contact.work
domain: contacts
capability: linkedin.email.work.find
platform: linkedin
capability: people.email.find
platform: people
scope: any_account
method: GET
path: /v1/people/linkedin
@@ -686,11 +686,13 @@ endpoints:
rates_micro: *id033
docs_url: https://api.contactout.com/
untestable: Requires a person lookup or returns named individuals; no automated
verification request or stored example under the catalog PII rule.
verification request is stored under the catalog PII rule. The example is a
hand-sanitized structural routing fixture with reserved fake contact data.
example_response: examples/contactout.people.contact.work.json
- id: contactout.people.contact.personal
domain: contacts
capability: linkedin.email.personal.find
platform: linkedin
capability: people.email.personal.find
platform: people
scope: any_account
method: GET
path: /v1/people/linkedin
@@ -733,8 +735,8 @@ endpoints:
verification request or stored example under the catalog PII rule.
- id: contactout.people.contact.phone
domain: contacts
capability: linkedin.phone.find
platform: linkedin
capability: people.phone.find
platform: people
scope: any_account
method: GET
path: /v1/people/linkedin
@@ -776,7 +778,9 @@ endpoints:
rates_micro: *id033
docs_url: https://api.contactout.com/
untestable: Requires a person lookup or returns named individuals; no automated
verification request or stored example under the catalog PII rule.
verification request is stored under the catalog PII rule. The example is a
hand-sanitized structural routing fixture with reserved fake contact data.
example_response: examples/contactout.people.contact.phone.json
- id: contactout.people.linkedin.enrich
domain: contacts
capability: linkedin.user.profile
@@ -1033,13 +1037,10 @@ endpoints:
verification request or stored example under the catalog PII rule.
proposed_capabilities:
people.count: Count profiles matching filters without returning profiles
linkedin.email.personal.availability: Check personal email availability for a LinkedIn
people.email.personal.availability: Check personal email availability for a LinkedIn
profile
linkedin.email.work.availability: Check work email availability for a LinkedIn profile
linkedin.phone.availability: Check phone availability for a LinkedIn profile
people.email.work.availability: Check work email availability for a LinkedIn profile
people.phone.availability: Check phone availability for a LinkedIn profile
linkedin.profile.from_email: Find LinkedIn profile URL from an email address
linkedin.email.work.find: Find work email from a LinkedIn profile, optionally with
phone
linkedin.email.personal.find: Find personal email from a LinkedIn profile, optionally
people.email.personal.find: Find personal email from a LinkedIn profile, optionally
with phone
linkedin.phone.find: Find phone number from a LinkedIn profile
-65
View File
@@ -10,8 +10,6 @@ pricing_url: https://dropleads.io/pricing
proposed_capabilities:
people.count: Count people matching search filters without returning records
people.enrich.verified: Enrich one person and require verified email data
people.enrich.verified.bulk: Synchronously enrich up to ten people with verified email data
people.enrich.bulk: Synchronously enrich up to ten people
companies.search.count: Count companies matching search filters without returning records
endpoints:
- id: dropleads.people.email.find
@@ -221,40 +219,6 @@ endpoints:
docs_url: https://dropleads.readme.io/reference/post_api-v2-prime-db-leads-enrich
verified: '2026-09-15'
example_response: examples/dropleads.people.enrich.verified.json
- id: dropleads.people.enrich.verified.bulk
capability: people.enrich.verified.bulk
platform: people
scope: any_account
domain: contacts
method: POST
path: /api/v2/prime-db/leads/bulk-enrich
name: Bulk enrich people with verified emails
summary: Synchronously enrich up to 10 people with verified email data.
input:
bodyType: json
body:
details:
type: array[object]
required: true
example: [{first_name: Jane, last_name: Doe, domain: example.com}]
note: 1-10 person identity objects using the same identity fields as the single endpoint.
email_verification_type:
type: string
required: false
default: valid_and_catchall
enum: [valid_only, valid_and_catchall]
test_request:
body: {details: [{id: treg-nonexistent-20260915}], email_verification_type: valid_and_catchall}
cost:
<<: *person_enrich_cost
type: per_result
unit: record
note: 0.2 credit per returned verified lead, not per submitted item. credits_consumed is authoritative; 1-10 inputs.
docs_url: https://dropleads.readme.io/reference/post_api-v2-prime-db-leads-bulk-enrich
verified: '2026-09-15'
example_response: examples/dropleads.people.enrich.verified.bulk.json
- id: dropleads.people.enrich
capability: people.enrich
platform: people
@@ -285,35 +249,6 @@ endpoints:
docs_url: https://dropleads.readme.io/reference/post_api-v2-prime-db-leads-simple-enrich
verified: '2026-09-15'
example_response: examples/dropleads.people.enrich.json
- id: dropleads.people.enrich.bulk
capability: people.enrich.bulk
platform: people
scope: any_account
domain: contacts
method: POST
path: /api/v2/prime-db/leads/simple-bulk-enrich
name: Bulk enrich people
summary: Synchronously enrich up to 10 people without requiring verified email results.
input:
bodyType: json
body:
details:
type: array[object]
required: true
example: [{first_name: Jane, last_name: Doe, domain: example.com}]
note: 1-10 person identity objects using the same fields as the single endpoint.
test_request:
body: {details: [{id: treg-nonexistent-20260915}]}
cost:
<<: *person_enrich_cost
type: per_result
unit: record
note: 0.2 credit per returned person, not per submitted item. credits_consumed is authoritative; 1-10 inputs.
docs_url: https://dropleads.readme.io/reference/post_api-v2-prime-db-leads-simple-bulk-enrich
verified: '2026-09-15'
example_response: examples/dropleads.people.enrich.bulk.json
- id: dropleads.companies.search
capability: companies.search
platform: companies
@@ -1 +0,0 @@
{"content":[],"totalElements":0,"totalPages":0,"number":0,"size":1,"numberOfElements":0,"first":true,"last":true,"empty":true}
@@ -1 +0,0 @@
{"content":[],"totalElements":0,"totalPages":0,"number":0,"size":1,"numberOfElements":0,"first":true,"last":true,"empty":true}
@@ -1,19 +0,0 @@
{
"result": "ok",
"cursor": null,
"items": [
{
"status": "success",
"email": "dev@bounceban.com",
"result": "deliverable",
"score": 99,
"is_disposable": false,
"is_accept_all": false,
"is_role": true,
"is_free": false,
"mx_records": ["aspmx.l.google.com"],
"smtp_provider": "Google",
"verify_at": "2026-09-16T00:00:00.000Z"
}
]
}
@@ -1,18 +0,0 @@
{
"result": "ok",
"items": [
{
"status": "success",
"email": "dev@bounceban.com",
"result": "deliverable",
"score": 99,
"is_disposable": false,
"is_accept_all": false,
"is_role": true,
"is_free": false,
"mx_records": ["aspmx.l.google.com"],
"smtp_provider": "Google",
"verify_at": "2026-09-16T00:00:00.000Z"
}
]
}
@@ -1,4 +0,0 @@
{
"result": "ok",
"download_url": "https://example.invalid/sanitized-bounceban-export.csv"
}
@@ -1,5 +0,0 @@
{
"id": "sanitized-bulk-id",
"status": "importing",
"credits_remaining": 9996
}
@@ -1,12 +0,0 @@
{
"id": "sanitized-bulk-id",
"status": "finished",
"total_count": 1,
"deliverable_count": 1,
"undeliverable_count": 0,
"risky_count": 0,
"unknown_count": 1,
"catchall_count": 1,
"credits_consumed": 1,
"credits_remaining": 9998
}
@@ -0,0 +1,6 @@
{
"status_code": 200,
"profile": {
"phone": ["+10000000000"]
}
}
@@ -0,0 +1,6 @@
{
"status_code": 200,
"profile": {
"work_email": ["routing-fixture@example.invalid"]
}
}
@@ -1 +0,0 @@
{"success":true,"total_requested_enrichments":1,"unique_enriched_records":1,"credits_consumed":0.2,"matches":[{"id":"example-person","first_name":"Jane","last_name":"Doe","name":"Jane Doe","email":"jane@example.com","organization_domain":"example.com"}]}
@@ -1 +0,0 @@
{"success":true,"status":"success","total_requested_enrichments":1,"unique_enriched_records":1,"verified_records":1,"missing_records":0,"credits_consumed":0.2,"matches":[{"id":"example-person","first_name":"Jane","last_name":"Doe","name":"Jane Doe","email":"jane@example.com","organization_domain":"example.com"}]}
@@ -1 +0,0 @@
{"ok":true,"results":[{"input":{"phone":"+1 202-555-0100"},"success":false,"data":null}]}
@@ -1 +0,0 @@
{"batch_id":12345,"name":"treg-test","type":"AdAudiences","status":"pending","requested_entity_count":1,"processed_entity_count":0,"created":"2026-09-17T00:00:00Z","status_url":"/api/v2/batch/results?batch_id=12345","pagination":null,"items":null}
@@ -1 +0,0 @@
{"batch_id":12346,"name":"treg-test","type":"EmailVerification","status":"completed","requested_entity_count":1,"processed_entity_count":1,"created":"2026-09-17T00:00:00Z","status_url":"/api/v2/batch/results?batch_id=12346","pagination":{"page":1,"page_size":100,"total_items":1,"total_pages":1,"has_next_page":false,"has_previous_page":false},"items":[{"input":"person@example.com","status":"completed","data":{"email":"person@example.com","result":"Risky"}}]}
@@ -1 +0,0 @@
{"batch_id":12346,"name":"treg-test","type":"EmailVerification","status":"pending","requested_entity_count":1,"processed_entity_count":0,"created":"2026-09-17T00:00:00Z","status_url":"/api/v2/batch/results?batch_id=12346","pagination":null,"items":null}
@@ -1 +0,0 @@
{"results":{"email":null,"risk_score":null},"status":"not_found","metadata":{}}
@@ -1 +0,0 @@
{"results":{"email":null,"risk_score":null,"type":null,"last_validated_at":null},"status":"not_found","metadata":{}}
@@ -1 +0,0 @@
{"results":{"email":null,"risk_score":null,"last_validated_at":null},"status":"not_found","metadata":{}}
@@ -1 +0,0 @@
{"results":{"email":null,"risk_score":null,"last_validated_at":null},"status":"not_found","metadata":{}}
@@ -1 +0,0 @@
{"results":{"mobile_phone":null,"last_validated_at":null},"status":"not_found","metadata":{},"phone_tokens_used":0,"phone_tokens_remaining":18}
@@ -1 +0,0 @@
{"batch_id":"00000000-0000-4000-8000-000000000030","task_ids":["00000000-0000-4000-8000-000000000031"]}
@@ -1 +0,0 @@
{"batch_id":"00000000-0000-4000-8000-000000000040","task_ids":["00000000-0000-4000-8000-000000000041"]}
@@ -1 +0,0 @@
{"batch_id":"00000000-0000-4000-8000-000000000010","task_ids":["00000000-0000-4000-8000-000000000011"]}
@@ -1 +0,0 @@
{"task_ids":["00000000-0000-4000-8000-000000000011"]}
@@ -1 +0,0 @@
{"batch_id":"00000000-0000-4000-8000-000000000010","status_counts":{"COMPLETED":1,"FAILED":0,"PENDING":0,"PROCESSING":0}}
@@ -1 +0,0 @@
{"task_id":"00000000-0000-4000-8000-000000000011","status":"COMPLETED","result":{"people":[{"name":"Treg Example","emails":["person@example.com"],"phones":[]}]}}
@@ -1 +0,0 @@
{"batch_id":"00000000-0000-4000-8000-000000000020","task_ids":["00000000-0000-4000-8000-000000000021"]}
@@ -1 +0,0 @@
{"error":false,"total_cost":0,"not_matched":[],"invalid_datapoints":[],"matched":[{"identifier":"public-example","company":{"company_id":"ccccpublicexample","name":"Intercom","website":"https://intercom.com","domain":"intercom.io","industry":"Software Development","employee_count":1117,"location":{"country":"United States","city":"San Francisco"}}}]}
@@ -1 +0,0 @@
{"error":false,"total_cost":0,"not_matched":[],"invalid_datapoints":[],"matched":[{"identifier":"public-example","person":{"person_id":"aaaapublicexample","first_name":"Jane","last_name":"Example","full_name":"Jane Example","linkedin_url":"https://www.linkedin.com/in/public-example","current_job_title":"CEO","email":{"status":"VERIFIED","revealed":true,"email":"jane@example.com"},"mobile":{"status":"VERIFIED","revealed":false,"mobile":"+1 555-***-****","mobile_country_code":"US"}},"company":{"company_id":"ccccpublicexample","name":"Fin","domain":"fin.ai"}}]}
@@ -1 +0,0 @@
{"status":"processing","identifier":"batch_sanitized_a1b2c3d4","fetch_result_endpoint":"/fetch_bulk_results","total":1,"credits_used":1,"remaining_credits":98,"retry_after_seconds":30}
@@ -1 +0,0 @@
{"status":"completed","identifier":"batch_sanitized_a1b2c3d4","retry_after_seconds":0,"results":{"verified@example.com":{"result":"Valid","status":"OK","quick_status":"OK"}}}
@@ -1 +0,0 @@
{"status":"processing","identifier":"deep_sanitized_a1b2c3d4","fetch_result_endpoint":"/fetch_bulk_results/deep","total":1,"credits_used":3,"remaining_credits":95,"retry_after_seconds":259200,"verification_type":"Deep"}
@@ -1 +0,0 @@
{"status":"processing","identifier":"deep_sanitized_a1b2c3d4","retry_after_seconds":259200,"results_24":{"verified@example.com":{"result":"pending","status":"pending","quick_status":"pending"}},"results_48":{"verified@example.com":{"result":"pending","status":"pending","quick_status":"pending"}},"results_72":{"verified@example.com":{"result":"pending","status":"pending","quick_status":"pending"}}}
+16 -42
View File
@@ -7,7 +7,7 @@ source:
docs: https://www.getleads.io/docs/
openapi: null
curated: '2026-09-16'
limits: '100 requests per minute by default across all endpoints. JSON enrichment batches accept up to 100 items. Search and company-contact lookups can return up to 5,000 records per request.'
limits: '100 requests per minute by default across all endpoints. Catalog enrichment calls accept exactly one item. Search and company-contact lookups can return up to 5,000 records per request.'
pricing_url: https://www.getleads.io/pricing/
proposed_capabilities:
people.count: Count people matching search filters without returning records
@@ -23,18 +23,19 @@ endpoints:
scope: any_account
method: POST
path: /api/v1/enrich/from-email
name: Enrich people from work emails
summary: Enrich up to 100 people from known work email addresses.
strict_body: true
name: Enrich one person from a work email
summary: Enrich one person from a known work email address.
input:
body:
items:
type: 'array[object]'
required: true
min: 1
max: 100
max: 1
example: [{email: nobody-treg-20260916@example.invalid}]
note: Each item needs an email string.
note: One credit for each result where success is true. A 100-item batch can consume 100 credits.
note: Exactly one item is accepted. One credit is charged when that result has success=true.
test_request:
body: {items: [{email: nobody-treg-20260916@example.invalid}]}
cost: &one_credit_success
@@ -59,19 +60,20 @@ endpoints:
scope: any_account
method: POST
path: /api/v1/enrich/from-linkedin
name: Enrich people from LinkedIn URLs
summary: Enrich up to 100 people from public LinkedIn profile URLs.
strict_body: true
name: Enrich one person from a LinkedIn URL
summary: Enrich one person from a public LinkedIn profile URL.
input:
body:
items:
type: 'array[object]'
required: true
min: 1
max: 100
max: 1
example: [{linkedin_url: https://www.linkedin.com/in/example}]
note: Each item needs linkedin_url.
limit_per_item: {type: integer, required: false, default: 1, min: 1, max: 10}
note: One credit for each result where success is true.
note: Exactly one item is accepted. One credit is charged when that result has success=true.
test_request:
body: {items: [{linkedin_url: https://www.linkedin.com/in/treg-nonexistent-20260916}], limit_per_item: 1}
cost:
@@ -88,18 +90,19 @@ endpoints:
scope: any_account
method: POST
path: /api/v1/enrich/from-person
name: Enrich people from name and company
summary: Enrich up to 100 people from names plus a company name or email domain.
strict_body: true
name: Enrich one person from name and company
summary: Enrich one person from a name plus a company name or email domain.
input:
body:
items:
type: 'array[object]'
required: true
min: 1
max: 100
max: 1
example: [{first_name: Jane, last_name: Doe, email_domain: example.com}]
note: Each item needs first_name and last_name plus company_name or email_domain.
note: One credit for each result where success is true.
note: Exactly one item is accepted. One credit is charged when that result has success=true.
test_request:
body: {items: [{first_name: Treg, last_name: Nonexistent, email_domain: treg-nonexistent-20260916.invalid}]}
cost:
@@ -139,35 +142,6 @@ endpoints:
docs_url: https://www.getleads.io/docs/#get-apiv1contactslookupphone
verified: '2026-09-16'
example_response: examples/getleadsio.people.phone.lookup.json
- id: getleadsio.people.phone.lookup_batch
capability: people.phone.find
platform: people
domain: contacts
scope: any_account
method: POST
path: /api/v1/contacts/from-phone
name: Look up contacts by phone in a batch
summary: Find contact records for a batch of phone numbers.
input:
body:
items:
type: 'array[object]'
required: true
min: 1
max: 100
example: [{phone: '+1 202-555-0100'}]
note: Each item accepts phone or cellphone.
limit_per_item: {type: integer, required: false, default: 1, min: 1, max: 10}
test_request:
body: {items: [{phone: '+1 202-555-0100'}], limit_per_item: 1}
cost:
<<: *one_credit_success
source_url: https://www.getleads.io/docs/#post-apiv1contactsfrom-phone
docs_url: https://www.getleads.io/docs/#post-apiv1contactsfrom-phone
verified: '2026-09-16'
example_response: examples/getleadsio.people.phone.lookup_batch.json
- id: getleadsio.people.colleagues
capability: people.organization_contacts
platform: people
+1 -101
View File
@@ -1,4 +1,4 @@
# LimaData Basic v2. All documented operations are available with a connected customer key.
# LimaData Basic v2. Batch submission and result operations are omitted from the catalog.
# Shared platform access is limited to synchronous operations whose full cost the generic money
# model can bound before the call. BYOK always wins and is never metered by treg.
provider: limadata
@@ -20,9 +20,6 @@ proposed_capabilities:
people.identity.from_email: Resolve an email address to social profiles
web.extract: Extract clean page content from URLs
web.research: Search and synthesize web research for a question
people.audience.batch.start: Start a batch advertising-audience job
people.email.verify.batch.start: Start a batch email-verification job
people.batch.results: Read a LimaData batch job and its results
endpoints:
- id: limadata.people.enrich
capability: people.enrich
@@ -572,100 +569,3 @@ endpoints:
docs_url: https://api.limadata.com/docs/basic_v2
verified: '2026-09-17'
example_response: examples/limadata.google.serp.organic.json
- id: limadata.people.audience.batch.start
capability: people.audience.batch.start
platform: people
scope: any_account
domain: contacts
method: POST
path: /api/v2/batch/ad_audiences
name: Start a batch audience job
summary: Start asynchronous advertising-audience resolution for up to 100,000 profiles.
input:
bodyType: json
body:
name: {type: string, required: false}
urls: {type: array, required: true, minItems: 1, maxItems: 100000, items: {type: string}}
target_network: {type: string, required: false, enum: [meta, google, x, reddit, tiktok, linkedin, snapchat, pinterest, amazon, universal]}
notification_url: {type: string, required: false}
test_request: {body: {name: treg-test, urls: ['https://www.linkedin.com/in/example'], target_network: universal}}
cost:
type: per_success
value: 3
currency: credit
per: 1
unit: result
source: docs
source_url: https://api.limadata.com/docs/basic_v2
checked: '2026-09-17'
confidence: verified
note: Three credits are held per submitted URL and misses are refunded after completion.
platform_blocked: Final-result refunds and shared-key task ownership require durable task settlement; connect your own LimaData key.
docs_url: https://api.limadata.com/docs/basic_v2
verified: '2026-09-17'
example_response: examples/limadata.people.audience.batch.start.json
- id: limadata.people.email.verify.batch.start
capability: people.email.verify.batch.start
platform: people
scope: any_account
domain: contacts
method: POST
path: /api/v2/batch/email_verify
name: Start batch email verification
summary: Start asynchronous deliverability checks for up to 100,000 email addresses.
input:
bodyType: json
body:
name: {type: string, required: false}
emails: {type: array, required: true, minItems: 1, maxItems: 100000, items: {type: string}}
notification_url: {type: string, required: false}
test_request: {body: {name: treg-test, emails: [person@example.com]}}
cost:
type: per_result
value: 0.3
currency: credit
per: 1
unit: item
source: docs
source_url: https://api.limadata.com/docs/basic_v2
checked: '2026-09-17'
confidence: verified
note: 0.3 credit per submitted email, charged when the job starts.
platform_blocked: Shared-key task ownership is not available; connect your own LimaData key.
docs_url: https://api.limadata.com/docs/basic_v2
verified: '2026-09-17'
example_response: examples/limadata.people.email.verify.batch.start.json
- id: limadata.people.batch.results
capability: people.batch.results
platform: people
scope: any_account
domain: contacts
kind: utility
method: GET
path: /api/v2/batch/results
name: Read batch results
summary: Read status and paginated results for a LimaData batch job.
input:
query:
batch_id: {type: integer, required: true, example: 12345}
page: {type: integer, required: false, min: 1, default: 1}
note: Results expire after 30 days. Poll no more often than once per 60 seconds.
test_request: {query: {batch_id: 12345, page: 1}}
cost:
type: free
value: 0
currency: USD
per: 1
unit: call
source: docs
source_url: https://api.limadata.com/docs/basic_v2
checked: '2026-09-17'
confidence: verified
note: Polling is free. A live response carried a task-related credit header but created no usage row, so it is not a polling price.
platform_blocked: Batch identifiers are account-scoped and shared-key task ownership is not available; connect your own LimaData key.
docs_url: https://api.limadata.com/docs/basic_v2
verified: '2026-09-17'
example_response: examples/limadata.people.batch.results.json
-100
View File
@@ -188,106 +188,6 @@ endpoints:
verified: '2026-09-16'
example_response: examples/moltsets.people.enrich.name.json
docs_url: https://docs.moltsets.com/api-reference/search-business-profile-by-name
- id: moltsets.people.email.find
domain: contacts
capability: people.email.find
platform: people
scope: any_account
method: POST
path: /linkedin_to_best_email
name: Find best email from profile
summary: Find the best business or personal email from a professional profile
input: &linkedin_batch_input
bodyType: json
body:
linkedin_url: {type: string}
linkedin_urls: {type: array, items: {type: string}, maxItems: 100}
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
cost: *record_cost
platform_blocked: The endpoint accepts batches of up to 100 and generic settlement cannot count per-item hits; connect your own MoltSets key.
verified: '2026-09-16'
example_response: examples/moltsets.people.email.find.json
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-best-email
- id: moltsets.people.email.find.business
domain: contacts
capability: people.email.find
platform: people
scope: any_account
method: POST
path: /linkedin_to_business_email
name: Find business email from profile
summary: Find a business email from a professional profile
input: *linkedin_batch_input
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
cost: *record_cost
platform_blocked: The endpoint accepts batches of up to 100 and generic settlement cannot count per-item hits; connect your own MoltSets key.
verified: '2026-09-16'
example_response: examples/moltsets.people.email.find.business.json
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-business-email
- id: moltsets.people.email.find.personal
domain: contacts
capability: people.email.find.personal
platform: people
scope: any_account
method: POST
path: /linkedin_to_personal_email
name: Find personal email from profile
summary: Find a personal email from a professional profile
input: *linkedin_batch_input
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
cost: *record_cost
platform_blocked: The endpoint accepts batches of up to 100 and generic settlement cannot count per-item hits; connect your own MoltSets key. Personal email availability also depends on the upstream plan.
verified: '2026-09-16'
example_response: examples/moltsets.people.email.find.personal.json
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-personal-email
- id: moltsets.people.email.find.personal-best
domain: contacts
capability: people.email.find.personal
platform: people
scope: any_account
method: POST
path: /linkedin_to_best_personal_email
name: Find best personal email from profile
summary: Find the best personal email from a professional profile
input: *linkedin_batch_input
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
cost: *record_cost
platform_blocked: The endpoint accepts batches of up to 100 and generic settlement cannot count per-item hits; connect your own MoltSets key. Personal email availability also depends on the upstream plan.
verified: '2026-09-16'
example_response: examples/moltsets.people.email.find.personal-best.json
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-best-personal-email
- id: moltsets.people.phone.find
domain: contacts
capability: people.phone.find
platform: people
scope: any_account
method: POST
path: /linkedin_to_mobile_phone
name: Find mobile phone from profile
summary: Find a mobile phone from a professional profile
input: *linkedin_batch_input
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
cost:
type: per_result
value: 0.51
currency: USD
per: 1
unit: result
source: observed
source_url: https://docs.moltsets.com/
checked: '2026-09-16'
confidence: documented
note: Each hit consumes one ordinary record ($0.01 shared-plan rate) plus one phone token (documented $0.50 upper-bound replacement cost); misses and errors are free.
platform_blocked: Phone hits use two upstream meters and batches can contain mixed outcomes; connect your own MoltSets key until the generic money contract can settle both safely.
verified: '2026-09-16'
example_response: examples/moltsets.people.phone.find.json
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-mobile-phone
- id: moltsets.people.enrich.email
domain: contacts
capability: people.enrich
+2 -174
View File
@@ -2,8 +2,8 @@
# Synchronous data reads whose price and returned-record shape are proven can use either a team's
# own key or treg's metered platform key. Openmart rounds 0.3-credit record prices up to whole
# credits per operation; the call runtime applies that provider-specific rule and caps shared-key
# requests at 25 records. Async lifecycles, account mutations, and unresolved prices remain
# BYOK-only. The shared balance endpoint is internal capacity infrastructure, not a tool.
# requests at 25 records. Async lifecycles are omitted; account mutations and unresolved prices
# remain BYOK-only. The shared balance endpoint is internal capacity infrastructure, not a tool.
provider: openmart
source:
docs: https://app.openmart.com/api-docs
@@ -16,9 +16,6 @@ proposed_capabilities:
companies.lookup: Retrieve company records by provider identifier.
companies.technologies: Detect technologies used by a company.
companies.email.find: Find company-level email addresses.
companies.tasks.status: Read an asynchronous company-task batch status.
companies.tasks.list: List task identifiers in an asynchronous company batch.
companies.tasks.get: Read one asynchronous company-task result.
companies.deny-rules.create: Add private company-account deny rules.
companies.deny-rules.check: Check private company-account deny rules.
companies.deny-rules.delete: Delete private company-account deny rules.
@@ -193,175 +190,6 @@ endpoints:
verified: '2026-09-17'
example_response: examples/openmart.companies.search.json
docs_url: https://app.openmart.com/api-docs
- id: openmart.people.find.batch
domain: contacts
capability: people.find
platform: people
scope: any_account
method: POST
path: /api/v1/task/batch/find_people
name: Find decision makers
summary: Start up to 100 decision-maker tasks for businesses
input: &task_batch_input
bodyType: json
body:
tasks: {type: array, items: {type: object, additionalProperties: true}, minItems: 1, maxItems: 100}
test_request: {body: {tasks: [{website: example.invalid, max_k: 1}]}}
cost:
type: per_result
value: 11
currency: credit
per: 1
unit: result
source: docs
source_url: https://www.openmart.com/pricing
checked: '2026-09-17'
confidence: verified
note: Email costs 3 credits and phone costs 8; requesting both can cost 11 per found person. A live both-fields task reserved 3 at submit and charged the remainder asynchronously.
platform_blocked: Async fan-out, mixed outcomes, and delayed charges cannot be assigned safely; connect your own Openmart key.
verified: '2026-09-17'
example_response: examples/openmart.people.find.batch.json
docs_url: https://app.openmart.com/api-docs
- id: openmart.technologies.find.batch
domain: companies
capability: companies.technologies
platform: companies
scope: any_account
method: POST
path: /api/v1/task/batch/find_tech
name: Find technologies
summary: Start up to 100 technology-detection tasks
input: *task_batch_input
test_request: {body: {tasks: [{website: example.invalid}]}}
cost:
type: per_result
value: 2
currency: credit
per: 1
unit: result
source: observed
source_url: https://www.openmart.com/pricing
checked: '2026-09-17'
confidence: verified
note: Two credits per completed result, reconciled from the live batch and the published $6 per 100 rate.
platform_blocked: Async batch ownership and delayed per-result charging cannot be assigned safely; connect your own Openmart key.
verified: '2026-09-17'
example_response: examples/openmart.technologies.find.batch.json
docs_url: https://app.openmart.com/api-docs
- id: openmart.companies.email.find.batch
domain: contacts
capability: companies.email.find
platform: companies
scope: any_account
method: POST
path: /api/v1/task/batch/lookup_business_email
name: Find company emails
summary: Start up to 100 company-email lookup tasks
input: *task_batch_input
test_request: {body: {tasks: [{website: example.invalid}]}}
cost:
type: per_result
value: 0.3
currency: credit
per: 1
unit: result
source: docs
source_url: https://www.openmart.com/pricing
checked: '2026-09-17'
confidence: documented
note: Dashboard price is 0.3 credit per returned company email. Integer balance deltas hide fractional usage and are not settlement evidence.
platform_blocked: Async ownership and delayed charging make this unsafe for a shared key; connect your own Openmart key.
verified: '2026-09-17'
example_response: examples/openmart.companies.email.find.batch.json
docs_url: https://app.openmart.com/api-docs
- id: openmart.people.enrich.batch
domain: contacts
capability: people.enrich
platform: people
scope: any_account
method: POST
path: /api/v1/task/batch/lookup_people
name: Enrich known people
summary: Start email or phone enrichment for known people
input: *task_batch_input
test_request: {body: {tasks: [{people: [{name: Treg Example}], info_access: [EMAIL]}]}}
cost:
type: per_result
value: 11
currency: credit
per: 1
unit: result
source: docs
source_url: https://www.openmart.com/pricing
checked: '2026-09-17'
confidence: verified
note: Email costs 3 credits and phone costs 8; callers choose EMAIL, PHONE, or both.
platform_blocked: Mixed requested fields and asynchronous per-person results cannot be settled safely; connect your own Openmart key.
verified: '2026-09-17'
example_response: examples/openmart.people.enrich.batch.json
docs_url: https://app.openmart.com/api-docs
- id: openmart.tasks.batch.status
domain: async
capability: companies.tasks.status
platform: companies
scope: own_account
method: GET
path: /api/v1/task/batch/{batch_id}/status
name: Get batch status
summary: Get status counts for an Openmart batch
input: &batch_id_input
pathParams:
batch_id: {type: string}
test_request: {pathParams: {batch_id: 00000000-0000-4000-8000-000000000000}}
cost: &free_task_read {type: free, value: 0, currency: USD, unit: call, source: observed, checked: '2026-09-17', confidence: verified, note: Live task reads used no credits.}
platform_blocked: Batch ownership is account-scoped; connect the Openmart key that created it.
verified: '2026-09-17'
example_response: examples/openmart.tasks.batch.status.json
docs_url: https://app.openmart.com/api-docs
- id: openmart.tasks.batch.ids
domain: async
capability: companies.tasks.list
platform: companies
scope: own_account
method: GET
path: /api/v1/task/batch/{batch_id}/task_ids
name: List batch task IDs
summary: List task IDs in a batch, optionally filtered by status
input:
pathParams: {batch_id: {type: string}}
query:
status: {type: string, enum: [PENDING, PROCESSING, COMPLETED, FAILED]}
test_request: {pathParams: {batch_id: 00000000-0000-4000-8000-000000000000}, query: {status: COMPLETED}}
cost: *free_task_read
platform_blocked: Batch ownership is account-scoped; connect the Openmart key that created it.
verified: '2026-09-17'
example_response: examples/openmart.tasks.batch.ids.json
docs_url: https://app.openmart.com/api-docs
- id: openmart.tasks.get
domain: async
capability: companies.tasks.get
platform: companies
scope: own_account
method: GET
path: /api/v1/task/{task_id}
name: Get task result
summary: Get one asynchronous task and its result
input:
pathParams: {task_id: {type: string}}
test_request: {pathParams: {task_id: 00000000-0000-4000-8000-000000000000}}
cost: *free_task_read
platform_blocked: Task ownership is account-scoped; connect the Openmart key that created it.
verified: '2026-09-17'
example_response: examples/openmart.tasks.get.json
docs_url: https://app.openmart.com/api-docs
- id: openmart.deny-rules.create
domain: governance
capability: companies.deny-rules.create
-81
View File
@@ -11,8 +11,6 @@ source:
limits: 'Starter: enrich routes 5/second, 300/minute, 2,000/day; search routes 1/second, 30/minute, 1,000/day. Search suggestions: 15/second on every plan.'
pricing_url: https://prospeo.io/pricing
proposed_capabilities:
people.enrich.bulk: Synchronously enrich up to 50 people
companies.enrich.bulk: Synchronously enrich up to 50 companies
people.search.suggestions: Resolve canonical Prospeo search-filter values
endpoints:
- id: prospeo.people.email.find
@@ -127,48 +125,6 @@ endpoints:
docs_url: https://prospeo.io/api-docs/enrich-person
verified: '2026-09-16'
example_response: examples/prospeo.people.enrich.json
- id: prospeo.people.enrich.bulk
capability: people.enrich.bulk
platform: people
scope: any_account
domain: contacts
method: POST
path: /bulk-enrich-person
name: Bulk enrich people
summary: Synchronously enrich up to 50 people with profile, email and optional mobile data.
input:
bodyType: json
body:
only_verified_email: {type: boolean, required: false, default: false}
enrich_mobile: {type: boolean, required: false, default: false}
only_verified_mobile: {type: boolean, required: false, default: false}
data:
type: array[object]
required: true
min: 1
max: 50
example: [{identifier: public-example, linkedin_url: https://www.linkedin.com/in/eoghanmccabe}]
note: Each record needs a unique identifier and one of the identity combinations accepted by the single-person endpoint.
test_request:
body: {only_verified_email: true, enrich_mobile: false, data: [{identifier: public-example, linkedin_url: https://www.linkedin.com/in/eoghanmccabe}]}
cost:
type: per_result
value: 1
currency: credit
per: 1
unit: record
source: observed
source_url: https://prospeo.io/api-docs/bulk-enrich-person
checked: '2026-09-16'
confidence: verified
note: One credit per charged email enrichment, or ten per charged mobile enrichment, up to 50 inputs. Response total_cost is authoritative, including zero.
modifiers:
enrich_mobile: {location: body, when: truthy, add_credits_per_result: 9}
docs_url: https://prospeo.io/api-docs/bulk-enrich-person
verified: '2026-09-16'
example_response: examples/prospeo.people.enrich.bulk.json
- id: prospeo.companies.enrich
capability: companies.enrich
platform: companies
@@ -194,43 +150,6 @@ endpoints:
docs_url: https://prospeo.io/api-docs/enrich-company
verified: '2026-09-16'
example_response: examples/prospeo.companies.enrich.json
- id: prospeo.companies.enrich.bulk
capability: companies.enrich.bulk
platform: companies
scope: any_account
domain: companies
method: POST
path: /bulk-enrich-company
name: Bulk company lookup and enrichment
summary: Synchronously enrich up to 50 companies.
input:
bodyType: json
body:
data:
type: array[object]
required: true
min: 1
max: 50
example: [{identifier: public-example, company_website: intercom.com}]
note: Each record needs a unique identifier and at least one company identity field accepted by the single endpoint.
test_request:
body: {data: [{identifier: public-example, company_website: intercom.com}]}
cost:
type: per_result
value: 1
currency: credit
per: 1
unit: record
source: observed
source_url: https://prospeo.io/api-docs/bulk-enrich-company
checked: '2026-09-16'
confidence: verified
note: One credit per matched company, up to 50 inputs. Response total_cost is authoritative, including zero.
docs_url: https://prospeo.io/api-docs/bulk-enrich-company
verified: '2026-09-16'
example_response: examples/prospeo.companies.enrich.bulk.json
- id: prospeo.people.search
capability: people.search
platform: people
+3 -114
View File
@@ -1,8 +1,6 @@
# Scrubby documents five POST endpoints. Single verification is synchronous; quick and deep bulk
# verification use separate submit/fetch pairs. Bulk submissions stay BYOK-only because Scrubby's
# request field is a singular `email` array with no documented maximum; treg cannot bound a shared-
# key reservation without rewriting the request or inventing a limit. Fetches are free and ownership-
# checked. Single verification is safe for platform access and settles from `credits_used`.
# Scrubby documents five POST endpoints. The catalog exposes only synchronous single verification;
# quick and deep bulk submit/fetch pairs are omitted because they have no documented batch maximum
# and require an owned asynchronous workflow. Single verification settles from `credits_used`.
provider: scrubby
source:
docs: https://docs.scrubby.io/
@@ -10,11 +8,6 @@ source:
curated: '2026-09-16'
pricing_url: https://scrubby.io/pricing/
limits: 25 requests/second (1,500/minute) per API key. Quick bulk normally completes in 30-60 seconds; deep verification returns first results in 24 hours and final results in 72 hours.
proposed_capabilities:
people.email.verify.bulk: Submit quick bulk email verification
people.email.verify.bulk.results: Fetch quick bulk verification results
people.email.verify.deep: Submit 72-hour deep email verification
people.email.verify.deep.results: Fetch deep verification results
endpoints:
- id: scrubby.people.email.verify
domain: contacts
@@ -45,107 +38,3 @@ endpoints:
verified: '2026-09-16'
example_response: examples/scrubby.people.email.verify.json
docs_url: https://docs.scrubby.io/
- id: scrubby.people.email.verify.bulk
domain: contacts
capability: people.email.verify.bulk
platform: people
scope: any_account
method: POST
path: /validate_bulk_emails
name: Submit quick bulk email verification
summary: Queue a list for quick verification and receive a batch identifier.
platform_blocked: "Shared-key bulk reservation and body-carried task ownership are unavailable; connect your own Scrubby key."
input:
bodyType: json
body:
email: {type: "array[string]", required: true, min: 1, example: [info@scrubby.io, support@scrubby.io]}
note: Save identifier, wait retry_after_seconds, then call scrubby.people.email.verify.bulk.results. BYOK-only because the provider publishes no batch maximum.
test_request: {body: {email: [info@scrubby.io]}}
cost:
type: per_result
value: 1
currency: credit
per: 1
unit: record
source: observed
source_url: https://scrubby.io/pricing/
checked: '2026-09-16'
confidence: verified
note: One credit per fresh address submitted; credits_used reports the exact batch charge, including cached zero-cost addresses.
verified: '2026-09-16'
example_response: examples/scrubby.people.email.verify.bulk.json
docs_url: https://docs.scrubby.io/
- id: scrubby.people.email.verify.bulk.results
kind: utility
capability: people.email.verify.bulk.results
platform: people
scope: any_account
method: POST
path: /fetch_bulk_results
name: Fetch quick bulk verification results
summary: Poll a quick-verification batch until status is completed.
platform_blocked: "Shared-key body-carried task ownership is unavailable; connect your own Scrubby key."
input:
bodyType: json
body:
identifier: {type: string, required: true, example: batch_sample_a1b2c3d4}
note: Poll after retry_after_seconds. Stop when status is completed.
test_request: {body: {identifier: batch_sample_a1b2c3d4}}
cost: {type: free, value: 0, currency: USD, unit: call, source: docs, source_url: https://docs.scrubby.io/, checked: '2026-09-16', confidence: documented, note: Scrubby prices verification when the batch is submitted; result polling reports no charge field.}
verified: '2026-09-16'
example_response: examples/scrubby.people.email.verify.bulk.results.json
docs_url: https://docs.scrubby.io/
- id: scrubby.people.email.verify.deep
domain: contacts
capability: people.email.verify.deep
platform: people
scope: any_account
method: POST
path: /validate_bulk_emails/deep/
name: Submit deep email verification
summary: Queue addresses for 72-hour verification based on observed delivery behavior.
platform_blocked: "Shared-key bulk reservation and body-carried task ownership are unavailable; connect your own Scrubby key."
input:
bodyType: json
body:
email: {type: "array[string]", required: true, min: 1, example: [info@scrubby.io]}
note: First results arrive after about 24 hours and final results after 72 hours. BYOK-only because the provider publishes no batch maximum.
test_request: {body: {email: [info@scrubby.io]}}
cost:
type: per_result
value: 3
currency: credit
per: 1
unit: record
source: observed
source_url: https://scrubby.io/pricing/
checked: '2026-09-16'
confidence: verified
note: Three credits per address. Live submission reported credits_used=3 for one input.
verified: '2026-09-16'
example_response: examples/scrubby.people.email.verify.deep.json
docs_url: https://docs.scrubby.io/
- id: scrubby.people.email.verify.deep.results
kind: utility
capability: people.email.verify.deep.results
platform: people
scope: any_account
method: POST
path: /fetch_bulk_results/deep
name: Fetch deep verification results
summary: Read the 24-, 48-, and 72-hour result windows for a deep batch.
platform_blocked: "Shared-key body-carried task ownership is unavailable; connect your own Scrubby key."
input:
bodyType: json
body:
identifier: {type: string, required: true, example: 2fbf182d25594091a2ab66a37bbe9f4d}
note: Stop polling when status is completed. The live processing response requested a 259,200-second retry.
test_request: {body: {identifier: 2fbf182d25594091a2ab66a37bbe9f4d}}
cost: {type: free, value: 0, currency: USD, unit: call, source: docs, source_url: https://docs.scrubby.io/, checked: '2026-09-16', confidence: documented, note: Scrubby prices deep verification when the batch is submitted; result polling reports no charge field.}
verified: '2026-09-16'
example_response: examples/scrubby.people.email.verify.deep.results.json
docs_url: https://docs.scrubby.io/
+2
View File
@@ -652,6 +652,7 @@ def _normalize(raw: dict, provider: str, directory: Path) -> dict:
if raw.get("platform_auth") is not None else None
),
"strict_query": raw.get("strict_query") is True,
"strict_body": raw.get("strict_body") is True,
"cost": _effective_cost(raw),
# Absent `tier` means core: the curated first wave predates the split, and treating an
# unmarked endpoint as extended would hide it from the platform view entirely.
@@ -762,6 +763,7 @@ def endpoint_view(ep: dict, provider_display: str, cat: Catalog | None = None) -
# the dashboard can show what comes BACK (example_response) but not what to SEND
"input": ep.get("input") or None,
**({"strict_query": True} if ep.get("strict_query") else {}),
**({"strict_body": True} if ep.get("strict_body") else {}),
# the exact request that live-verified this endpoint — the Try-it drawer prefills from it
# verbatim (it also carries the ground truth the input spec can't express: whether the
# body is a bare object or an ARRAY of tasks, which dataforseo requires)
+7 -9
View File
@@ -22,8 +22,8 @@ from treg import oauth_providers as P
def test_openmart_surface_separates_platform_reads_from_byok_lifecycles():
cat = cs.load()
rows = cat.for_provider("openmart")
assert len(rows) == 16
assert len({(e["method"], e["path"]) for e in rows}) == 16
assert len(rows) == 9
assert len({(e["method"], e["path"]) for e in rows}) == 9
assert {e["id"] for e in rows if cat.platform_eligible(e)} == {
"openmart.businesses.search",
"openmart.businesses.lookup.openmart",
@@ -36,17 +36,15 @@ def test_openmart_surface_separates_platform_reads_from_byok_lifecycles():
assert "openmart.account.balance" not in cat.by_id
def test_openmart_pricing_and_lifecycle_boundaries_stay_visible():
def test_openmart_pricing_and_account_boundaries_stay_visible():
cat = cs.load()
assert cat.by_id["openmart.people.find.batch"]["cost"]["value"] == 11
assert cat.by_id["openmart.technologies.find.batch"]["cost"]["value"] == 2
company_email = cat.by_id["openmart.companies.email.find.batch"]
assert company_email["cost"]["value"] == .3
assert company_email["cost"]["confidence"] == "documented"
assert not any(
eid.startswith("openmart.tasks.") or eid.endswith(".batch")
for eid in cat.by_id if eid.startswith("openmart.")
)
fast = cat.by_id["openmart.businesses.search.ids"]
assert fast["cost"]["value"] is None and fast["cost"]["confidence"] == "unknown"
assert all(cat.by_id[key]["scope"] == "own_account" for key in (
"openmart.tasks.batch.status", "openmart.tasks.batch.ids", "openmart.tasks.get",
"openmart.deny-rules.create", "openmart.deny-rules.check",
"openmart.deny-rules.delete",
))
+42 -36
View File
@@ -565,11 +565,12 @@ def test_contactout_catalog_distribution_preserves_ids_and_global_discovery():
cat = catalog_store.load()
entries = [e for e in cat.endpoints if e.get("provider") == "contactout"]
assert Counter(e["platform"] for e in entries) == {
"linkedin": 8, "people": 10, "companies": 2}
"linkedin": 2, "people": 16, "companies": 2}
for e in entries:
assert e["capability"].split(".")[0] == e["platform"]
assert cat.by_id["contactout.people.contact.work"]["platform"] == "linkedin"
results, _ = catalog_store.search("contactout linkedin work email", cat, limit=100)
assert cat.by_id["contactout.people.contact.work"]["platform"] == "people"
assert cat.by_id["contactout.people.contact.personal"]["capability"] == "people.email.personal.find"
results, _ = catalog_store.search("contactout people work email", cat, limit=100)
assert any(e["id"] == "contactout.people.contact.work" for e, _ in results)
@@ -580,12 +581,20 @@ def test_contactout_person_routes_cannot_recapture_pii():
endpoints = yaml.safe_load(path.read_text())["endpoints"]
safe = {"contactout.people.count", "contactout.people.email.verify",
"contactout.companies.search", "contactout.companies.enrich"}
structural = {"contactout.people.contact.work", "contactout.people.contact.phone"}
for ep in endpoints:
if ep["id"] in safe:
continue
assert ep["untestable"]
assert not any(key in ep for key in ("test_request", "verified", "example_response"))
assert not (path.parent / "examples" / (ep["id"] + ".json")).exists()
assert not any(key in ep for key in ("test_request", "verified"))
example = path.parent / "examples" / (ep["id"] + ".json")
if ep["id"] in structural:
assert ep["example_response"] == "examples/" + example.name
payload = example.read_text()
assert "example.invalid" in payload or "+10000000000" in payload
else:
assert "example_response" not in ep
assert not example.exists()
work = next(ep for ep in endpoints if ep["id"] == "contactout.people.enrich.work_email")
assert work["cost"]["value"] == 0.17
@@ -620,6 +629,20 @@ def test_strict_query_contract_validation(patch, valid):
assert bool(errors) is not valid
@pytest.mark.parametrize('patch,valid', [
({}, True),
({'strict_body': 'yes'}, False),
({'method': 'GET'}, False),
({'input': {'body': {'items': {'type': 'array[object]', 'min': 2, 'max': 1}}}}, False),
])
def test_strict_body_contract_validation(patch, valid):
ep = {'strict_body': True, 'method': 'POST', 'path': '/lookup',
'input': {'body': {'items': {'type': 'array[object]', 'min': 1, 'max': 1}}}}
errors = []
validator.check_strict_body(ep | patch, 'example', errors)
assert bool(errors) is not valid
@pytest.mark.parametrize('patch,valid', [
({}, True),
({'platform_auth': 'provider'}, False),
@@ -656,7 +679,7 @@ def test_missing_platform_auth_normalizes_as_absent():
def test_dropleads_catalog_surface_is_bounded_and_excludes_internal_routes():
catalog = catalog_store.load()
rows = [ep for ep in catalog.endpoints if ep["provider"] == "dropleads"]
assert len(rows) == 12
assert len(rows) == 10
assert all(catalog.platform_eligible(ep) for ep in rows)
assert not any(
"credits/balance" in ep["path"] or "export/cost" in ep["path"]
@@ -681,18 +704,15 @@ def test_dropleads_catalog_surface_is_bounded_and_excludes_internal_routes():
def test_prospeo_catalog_surface_excludes_account_info_and_prices_mobile_at_the_documented_maximum():
catalog = catalog_store.load()
rows = [ep for ep in catalog.endpoints if ep["provider"] == "prospeo"]
assert len(rows) == 9
assert len(rows) == 7
assert not any(ep["path"] == "/account-information" for ep in rows)
assert {ep["path"] for ep in rows} == {
"/enrich-person", "/bulk-enrich-person", "/enrich-company",
"/bulk-enrich-company", "/search-person", "/search-company",
"/enrich-person", "/enrich-company", "/search-person", "/search-company",
"/search-suggestions",
}
phone = catalog.by_id["prospeo.people.phone.find"]
assert not phone.get("platform_blocked")
assert phone["cost"]["value"] == 10
bulk_mobile = catalog.by_id["prospeo.people.enrich.bulk"]["cost"]["modifiers"]
assert bulk_mobile["enrich_mobile"]["add_credits_per_result"] == 9
assert all(catalog.platform_eligible(ep) for ep in rows)
@@ -703,21 +723,14 @@ def test_aiark_catalog_covers_the_selected_documented_surface():
"aiark.people.search", "aiark.people.preview", "aiark.companies.search",
"aiark.people.email.find", "aiark.people.phone.find", "aiark.people.enrich",
"aiark.people.personality.analyze", "aiark.lists.upsert",
"aiark.people.export.start", "aiark.people.export.results",
"aiark.people.export.statistics", "aiark.people.export.submissions",
"aiark.people.export.webhook.resend", "aiark.people.email.find.bulk",
"aiark.people.email.find.bulk.results", "aiark.people.email.find.bulk.statistics",
"aiark.people.email.find.bulk.submissions",
"aiark.people.email.find.bulk.webhook.resend",
}
assert not any(ep["path"] in {
"/v1/payments/credits", "/v1/people/export/single",
"/v1/people/mobile-phone-finder",
} for ep in endpoints.values())
assert all(
ep.get("platform_blocked")
for eid, ep in endpoints.items()
if ".export." in eid or ".bulk" in eid or eid == "aiark.lists.upsert"
ep.get("platform_blocked") for eid, ep in endpoints.items()
if eid == "aiark.lists.upsert"
)
assert endpoints["aiark.people.search"]["input"]["body"]["size"]["enum"] == [1]
assert endpoints["aiark.people.search"]["platform_request"] == {"body.size": 1}
@@ -754,12 +767,9 @@ def test_limadata_catalog_covers_basic_v2_and_keeps_unsafe_calls_byok_only():
("POST", "/api/v1/research/extract"),
("POST", "/api/v1/research/search"),
("POST", "/api/v1/search/web"),
("POST", "/api/v2/batch/ad_audiences"),
("POST", "/api/v2/batch/email_verify"),
("GET", "/api/v2/batch/results"),
}
platform = {ep["id"] for ep in rows if catalog.platform_eligible(ep)}
assert len(rows) == 24 and len(platform) == 14
assert len(rows) == 21 and len(platform) == 14
assert {
"limadata.people.enrich",
"limadata.people.count",
@@ -768,9 +778,6 @@ def test_limadata_catalog_covers_basic_v2_and_keeps_unsafe_calls_byok_only():
"limadata.people.employees.search",
"limadata.people.identity.resolve",
"limadata.web.extract",
"limadata.people.audience.batch.start",
"limadata.people.email.verify.batch.start",
"limadata.people.batch.results",
}.isdisjoint(platform)
@@ -794,18 +801,13 @@ def test_zerobounce_catalog_exposes_verified_single_record_tools_only():
assert all(catalog.platform_eligible(ep) for ep in rows)
def test_bounceban_catalog_has_one_platform_tool_and_complete_safe_byok_lifecycle():
def test_bounceban_catalog_has_one_platform_tool_and_no_bulk_lifecycle():
catalog = catalog_store.load()
rows = [ep for ep in catalog.endpoints if ep["provider"] == "bounceban"]
assert len(rows) == 9
assert len(rows) == 4
assert {ep["path"] for ep in rows} == {
"/v1/verify/single",
"/v1/verify/single/status",
"/v1/verify/bulk",
"/v1/verify/bulk/status",
"/v1/verify/bulk/emails",
"/v1/verify/bulk/dump",
"/v1/verify/bulk/export",
"/v1/account",
}
assert not any(ep["path"] in ("/v1/verify/bulk/file", "/v1/verify/bulk/destroy", "/v1/check")
@@ -821,13 +823,17 @@ def test_bounceban_catalog_has_one_platform_tool_and_complete_safe_byok_lifecycl
assert waterfall["platform_blocked"]
def test_getleadsio_original_routes_are_available_to_byok_and_platform_callers():
def test_getleadsio_scalar_routes_are_available_to_byok_and_platform_callers():
catalog = catalog_store.load()
rows = [ep for ep in catalog.endpoints if ep["provider"] == "getleadsio"]
assert len(rows) == 12
assert len(rows) == 11
assert not any(ep["path"] in {
"/api/v1/usage/fair-use", "/api/v1/contacts/health"
} for ep in rows)
assert all(catalog.platform_eligible(ep) for ep in rows)
assert not any(ep["id"].endswith(".trial") for ep in rows)
assert not any(ep.get("platform_request") or ep.get("platform_blocked") for ep in rows)
scalar = [ep for ep in rows if ep["id"].startswith("getleadsio.people.enrich.from_")]
assert len(scalar) == 3
assert all(ep["strict_body"] and ep["input"]["body"]["items"]["max"] == 1
for ep in scalar)
+22 -32
View File
@@ -2108,6 +2108,28 @@ async def test_getleadsio_platform_call_is_free_and_preserves_the_requested_limi
assert await _balance(clients) == before
@pytest.mark.parametrize("endpoint", [
"getleadsio.people.enrich.from_email",
"getleadsio.people.enrich.from_linkedin",
"getleadsio.people.enrich.from_person",
])
@pytest.mark.parametrize("own_key", [False, True])
async def test_getleadsio_enrichment_rejects_more_than_one_item_on_every_credential_tier(
clients: AsyncClient, getleadsio_trial_on, endpoint, own_key):
if own_key:
created = await clients.post(
"/secrets", json={"name": "getleadsio", "value": "OWN-GETLEADSIO"},
)
assert created.status_code == 200, created.text
result = await clients.post(
f"/call/{endpoint}", json={"items": [{"id": "one"}, {"id": "two"}]},
)
assert result.status_code == 400, result.text
detail = result.json()["detail"]
assert detail["error"] == "catalog_parameter_invalid"
assert detail["parameter"] == "body.items"
async def test_getleadsio_own_key_wins_and_is_unmetered(
clients: AsyncClient, getleadsio_trial_on):
await clients.post("/secrets", json={"name": "getleadsio", "value": "OWN-GETLEADSIO"})
@@ -3216,8 +3238,6 @@ async def test_dropleads_byok_wins_and_is_never_metered(
@pytest.mark.parametrize(
"endpoint,body,expected",
[
("dropleads.people.enrich.bulk", {"details": [{"id": str(i)} for i in range(10)]}, 36_000),
("dropleads.people.enrich.verified.bulk", {"details": [{"id": str(i)} for i in range(3)]}, 10_800),
("dropleads.companies.enrich", {"domains": [f"{i}.test" for i in range(25)],
"companyNames": [str(i) for i in range(25)]}, 90_000),
("dropleads.companies.search", {"filters": {},
@@ -3263,8 +3283,6 @@ def test_dropleads_request_shapes_reserve_exact_valid_maxima(endpoint, body, exp
{"error": False, "free": False, "results": [{"person": {"person_id": "p1"}}]}, 24_500),
("prospeo.companies.search",
{"error": False, "free": True, "results": [{"company": {"company_id": "c1"}}]}, 0),
("prospeo.people.enrich.bulk",
{"error": False, "total_cost": 3, "matched": []}, 73_500),
("prospeo.search.suggestions",
{"error": False, "location_results": []}, 0),
("prospeo.people.email.find", {"error": True, "error_code": "NO_MATCH"}, 0),
@@ -3275,14 +3293,6 @@ def test_prospeo_settles_only_from_response_evidence(endpoint, doc, expected):
assert call_settle._observed_cost_micro(mk, json.dumps(doc).encode()) == expected
@pytest.mark.parametrize("total_cost", [float("inf"), float("-inf"), float("nan")])
def test_prospeo_bulk_non_finite_cost_keeps_the_estimate(total_cost):
mk = _mk("prospeo", endpoint_id="prospeo.people.enrich.bulk",
cost_type="per_result", unit_micro=24_500)
body = json.dumps({"error": False, "total_cost": total_cost}).encode()
assert call_settle._observed_cost_micro(mk, body) is None
@pytest.mark.parametrize(
"doc,expected",
[
@@ -3302,26 +3312,6 @@ def test_prospeo_phone_settlement_requires_an_actual_mobile(doc, expected):
assert call_settle._observed_cost_micro(mk, json.dumps(doc).encode()) == expected
@pytest.mark.parametrize(
"endpoint,body,expected",
[
("prospeo.people.enrich.bulk", {"data": [{"id": i} for i in range(4)]}, 98_000),
("prospeo.people.enrich.bulk",
{"enrich_mobile": True, "data": [{"id": i} for i in range(4)]}, 980_000),
("prospeo.companies.enrich.bulk", {"data": [{"id": i} for i in range(50)]}, 1_225_000),
],
)
def test_prospeo_bulk_reserves_the_maximum_documented_charge(endpoint, body, expected):
catalog = catalog_store.load()
ep = catalog.by_id[endpoint]
cost = catalog.cost_view(ep["cost"], "prospeo")
estimate, unit = call_resolution._marketplace_pricing(
"prospeo", endpoint, cost, {}, json.dumps(body).encode()
)
assert estimate == expected
assert unit == 24_500
async def test_prospeo_platform_email_settles_one_credit(
clients, monkeypatch, prospeo_platform_on,
):
+12 -26
View File
@@ -23,8 +23,8 @@ def moltsets_on(monkeypatch, platform_on):
def test_surface_platform_boundary_and_shared_plan_rate():
cat = store.load()
rows = cat.for_provider("moltsets")
assert len(rows) == 17
assert len({(e["method"], e["path"]) for e in rows}) == 17
assert len(rows) == 12
assert len({(e["method"], e["path"]) for e in rows}) == 12
enabled = {e["id"] for e in rows if cat.platform_eligible(e)}
assert enabled == {
"moltsets.people.email.find.name",
@@ -41,6 +41,11 @@ def test_surface_platform_boundary_and_shared_plan_rate():
assert cat.shared_plans["moltsets"] == {"usd": .01, "fee_usd_month": 27}
assert all(e.get("verified") and e.get("example_file") for e in rows)
assert not any(e["path"].startswith("/get_") for e in rows)
assert {
"moltsets.people.email.find", "moltsets.people.email.find.business",
"moltsets.people.email.find.personal", "moltsets.people.email.find.personal-best",
"moltsets.people.phone.find",
}.isdisjoint(cat.by_id)
def test_connection_and_binding_contract():
@@ -105,13 +110,8 @@ async def test_platform_success_charges_and_miss_or_error_does_not(clients, molt
("people.search", {"query": "engineer", "limit": 2}),
("companies.search", {"query": "example", "limit": 2}),
("linkedin.profile.search", {"name": "Alex Example"}),
("people.email.find", {"linkedin_url": "https://linkedin.com/in/alex-example"}),
("people.email.find.business", {"linkedin_urls": ["https://linkedin.com/in/alex-example"]}),
("people.email.find.personal", {"linkedin_url": "https://linkedin.com/in/alex-example"}),
("people.email.find.personal-best", {"linkedin_url": "https://linkedin.com/in/alex-example"}),
("people.phone.find", {"linkedin_url": "https://linkedin.com/in/alex-example"}),
])
async def test_variable_batch_and_phone_tools_are_not_shared_key_offers(
async def test_variable_search_tools_are_not_shared_key_offers(
clients, moltsets_on, monkeypatch, endpoint, body):
async def forbidden(*args, **kwargs):
raise AssertionError("platform guard must precede relay")
@@ -122,13 +122,11 @@ async def test_variable_batch_and_phone_tools_are_not_shared_key_offers(
assert not [e for e in await _entries(clients) if e["kind"] in ("reserve", "settle", "release")]
async def test_byok_can_call_blocked_batch_and_phone_tools_without_treg_meter(clients, moltsets_on):
async def test_byok_can_call_blocked_search_tools_without_treg_meter(clients, moltsets_on):
await clients.post("/secrets", json={"name": "moltsets", "value": "OWN-MOLTSETS"})
before = await _balance(clients)
bodies = {
"people.search": {"query": "engineer", "limit": 2},
"people.email.find": {"linkedin_urls": ["https://linkedin.com/in/a", "https://linkedin.com/in/b"]},
"people.phone.find": {"linkedin_url": "https://linkedin.com/in/a"},
}
for endpoint, body in bodies.items():
response = await clients.post(
@@ -143,18 +141,6 @@ async def test_byok_can_call_blocked_batch_and_phone_tools_without_treg_meter(cl
assert not [e for e in await _entries(clients) if e["kind"] in ("reserve", "settle", "release")]
def test_phone_price_keeps_both_upstream_meters_visible():
from treg.routers.web import _price_label
cat = store.load()
endpoint = cat.by_id["moltsets.people.phone.find"]
cost = cat.cost_view(endpoint["cost"], "moltsets")
assert cost["usd"] == .51
assert _price_label(cost) == "$0.51/result"
assert "$0.01" in cost["note"] and "$0.50" in cost["note"]
assert endpoint["platform_blocked"]
@pytest.mark.parametrize("five_hour,weekly,token_balance,expected", [
(997, 4997, -1, 997),
(0, 4997, -1, 0),
@@ -205,8 +191,8 @@ async def test_provider_page_shows_complete_inventory_and_access_split(clients):
response = await clients.get("/tools/moltsets")
assert response.status_code == 200
html = response.text
assert "17 tools · 9 platform + BYOK · 8 BYOK only" in html
assert "17 of 17 tools on this page are live-verified" in html
assert "$0.51/result" in html
assert "12 tools · 9 platform + BYOK · 3 BYOK only" in html
assert "12 of 12 tools on this page are live-verified" in html
assert "moltsets.people.phone.find" not in html
for endpoint in store.load().for_provider("moltsets"):
assert "<code>" + endpoint["id"] + "</code>" in html
+16 -7
View File
@@ -1304,7 +1304,7 @@ async def test_lusha_is_the_last_rung_of_the_phone_waterfall_and_settles_on_its_
get_settings.cache_clear()
routed = "treg.people.phone.find"
plan = (await clients.get(f"/catalog/endpoints/{routed}")).json()["routing"]["plan"]
assert plan[-1]["endpoint_id"] == "lusha.people.phone.find" and len(plan) == 11, [c["endpoint_id"] for c in plan]
assert plan[-1]["endpoint_id"] == "lusha.people.phone.find" and len(plan) == 12, [c["endpoint_id"] for c in plan]
def misses():
return {"aviato": [(404, {"message": "Not Found"})], "tomba": [(200, {"data": {"e164_format": None}})],
"leadmagic": [(200, {"mobile_number": None, "credits_consumed": 0})],
@@ -1504,11 +1504,6 @@ def test_bounceban_verdicts_join_existing_email_verification_route(result, valid
assert cat.platform_eligible(cat.by_id[eid])
for blocked in (
"bounceban.people.email.verify.waterfall",
"bounceban.people.email.verify.bulk",
"bounceban.people.email.verify.bulk.status",
"bounceban.people.email.verify.bulk.emails",
"bounceban.people.email.verify.bulk.dump",
"bounceban.people.email.verify.bulk.export",
"bounceban.account.usage",
):
assert not cat.platform_eligible(cat.by_id[blocked])
@@ -1981,6 +1976,18 @@ def test_row_values_and_nested_lookup_expressions(value, expected):
_CONTACTOUT_DISCOVERY = [
('people.email.find', 'people.contact.work',
{'linkedin_url': 'https://www.linkedin.com/in/example'},
'GET', {'profile': 'https://www.linkedin.com/in/example',
'email_type': 'work', 'include_phone': False}, None,
{'status_code': 200, 'profile': {'work_email': ['work@example.test']}},
'email', 'work@example.test', 150_000),
('people.phone.find', 'people.contact.phone',
{'linkedin_url': 'https://www.linkedin.com/in/example'},
'GET', {'profile': 'https://www.linkedin.com/in/example',
'email_type': 'none', 'include_phone': True}, None,
{'status_code': 200, 'profile': {'phone': ['+10000000000']}},
'phone', '+10000000000', 250_000),
('companies.search', 'companies.search', {'domain': 'example.test'},
'POST', {}, {'domain': ['example.test']},
{'status_code': 200, 'companies': [{'name': 'Example'}]}, 'companies', [{'name': 'Example'}], 20_000),
@@ -2033,10 +2040,12 @@ async def test_contactout_discovery_empty_or_error_response_is_not_a_hit(
assert not (await db.execute(select(Hold))).scalars().all()
def test_contactout_pii_routes_are_not_enabled_without_verification_examples():
def test_contactout_unverified_pii_routes_stay_direct_only():
cat = catalog_store.load()
for cap, child in [('people.search', 'people.search'), ('people.enrich', 'people.enrich'),
('linkedin.user.profile', 'people.linkedin.enrich')]:
eid = 'contactout.' + child
assert eid not in cat.adapters
assert eid not in cat.by_id['treg.' + cap]['routed_children']
assert 'contactout.people.contact.personal' not in cat.adapters
assert cat.by_id['contactout.people.contact.personal']['platform'] == 'people'
+2 -11
View File
@@ -42,21 +42,12 @@ async def _balance(clients):
return (await clients.get(f"/orgs/{org_id}/balance")).json()["balance_micro"]
def test_scrubby_catalog_covers_the_documented_surface():
def test_scrubby_catalog_exposes_single_verification_only():
catalog = catalog_store.load()
endpoints = {eid: ep for eid, ep in catalog.by_id.items() if eid.startswith("scrubby.")}
assert set(endpoints) == {
"scrubby.people.email.verify",
"scrubby.people.email.verify.bulk",
"scrubby.people.email.verify.bulk.results",
"scrubby.people.email.verify.deep",
"scrubby.people.email.verify.deep.results",
}
assert set(endpoints) == {"scrubby.people.email.verify"}
assert endpoints["scrubby.people.email.verify"]["path"] == "/validate_email"
assert "ownership" in endpoints["scrubby.people.email.verify.bulk"]["platform_blocked"]
assert "ownership" in endpoints["scrubby.people.email.verify.deep"]["platform_blocked"]
assert catalog.cost_view(endpoints["scrubby.people.email.verify"]["cost"], "scrubby")["usd"] == 0.008
assert catalog.cost_view(endpoints["scrubby.people.email.verify.deep"]["cost"], "scrubby")["usd"] == 0.024
@pytest.mark.parametrize("credits,expected", [(0, 0), (1, 8_000), (3, 24_000), (-1, None), (True, None), (1.5, None), (None, None)])