Guide: Data fusion — combine purchased data with data you already have
Data fusion here means producing one dataset out of several sources: merging a purchased file with a table you already hold, or enriching your rows by calling a lookup API once per key. The hard part of fusion has never been the merge itself — it’s knowing, before you pay, which listings can join with your data and on what key. That is exactly what the marketplace’s join cards make possible.
The mechanism, end to end
SELLER PLATFORM BUYER (agent)
declares join handles → validates them against → searches by handle,
on the listing the response schema reads the card,
("join on `domain`, (deterministic check, plans the merge,
lowercase, no www") badge when verified) THEN pays and joins
Nobody guesses: the seller states the keys once, the platform verifies the columns exist, and every buying agent inherits that knowledge for free — in the listing feed, in search, and in the free sample.
The two fusion patterns
| Pattern | Listing type | How the join happens |
|---|---|---|
| Table-to-table | Dataset (downloadable file) | Buy once, download, JOIN your table with the file on a shared key column |
| Lookup enrichment | API endpoint / MCP tool | Call once per key you hold (accepts → returns); each call appends columns to your rows |
A card’s join_keys power the first pattern; its accepts/returns power the
second. Many fusions chain both (worked example below).
Step by step (for a buying agent)
- Know your own handles. Look at the data you already hold and list the columns that can act as join keys: identifiers, domains, emails, product codes, country codes, coordinates, timestamps.
- Search by handle.
search_marketplace(joinable_on="domain")returns only listings whose join card offers a matching handle (column name or description —joinable_on="domain"also finds awebsitecolumn described as “company domain”). Results carry the card,fusion_ready, andjoins_withinline. No account, no payment. - Read the card, check the formats.
get_endpoint_sample(provider, endpoint)→ the card + the response JSON Schema. The card’s format notes are your normalization spec: if it sayslowercase, no wwwand your column holdshttps://www.Acme.com/about, you now know exactly what to strip before the join. Mismatched grain (one row per companyvs your per-transaction table) is also visible here — catch it before paying. - Optionally try real data first. For API listings,
call_endpointwith no payment takes a metered free trial (real values) — the cheapest way to sanity-check that your keys actually resolve. - Buy.
call_endpoint(..., confirmed=true). A dataset purchase returns a relay URL + one-time pickup key; fetch it to stream the file to disk (a failed download re-streams against the same receipt — no second charge). - Normalize, then join. Apply the card’s format notes to your keys, then
merge (DuckDB/pandas/Polars — the listing’s
dataset_formattells you the loader). - Validate the match rate. Count how many of your rows matched. The card
declares intent, not overlap — this is the step where you learn the real
coverage, so always measure it and decide whether to proceed, buy a
different source, or post a
seek_dataask.
A complete worked example
You hold crm.csv with a website column and want revenue + traffic per
customer.
# 1 — find fusable listings (free)
search_marketplace(joinable_on="domain", type="dataset")
→ "acme/firmographics" card: row_represents "one company",
join_keys [{column: "domain", description:
"company website domain, lowercase, no www"}]
search_marketplace(joinable_on="domain", type="api")
→ "acme/traffic-by-domain" card: accepts "a company domain via the
`domain` param", returns "monthly traffic stats"
# 2 — inspect before paying (free)
get_endpoint_sample("acme", "firmographics") → card + schema + preview rows
get_endpoint_sample("acme", "traffic-by-domain") → card + schema
# 3 — buy the dataset, stream it to disk
call_endpoint("acme", "firmographics", confirmed=true)
→ { download_url, pickup key } # fetch → firmographics.parquet
-- 4 — normalize per the card ("lowercase, no www"), then join (DuckDB)
CREATE TABLE crm AS SELECT *,
regexp_replace(lower(website), '^https?://(www\.)?([^/]+).*', '\2') AS domain
FROM read_csv('crm.csv');
CREATE TABLE enriched AS
SELECT crm.*, f.hq_country, f.revenue_usd
FROM crm LEFT JOIN read_parquet('firmographics.parquet') f USING (domain);
-- 5 — measure the match rate BEFORE trusting the result
SELECT count(f.domain) * 1.0 / count(*) AS match_rate FROM crm
LEFT JOIN read_parquet('firmographics.parquet') f USING (domain);
# 6 — enrich the matched rows via the lookup API (pattern 2)
for each distinct domain in enriched:
call_endpoint("acme", "traffic-by-domain",
params={"domain": domain}, confirmed=true)
One search, two reads, one download, one join, one per-key loop — the entire fusion was planned before the first cent moved.
When nothing matches
A joinable_on search that returns nothing is a supply gap, not a dead end:
post it with seek_data(what="firmographics joinable on company domain", format="dataset", max_price_usd=...). Zero-result searches are logged and the
ask lands on the public requested-data board vendors build against — naming
the handle you need makes the ask buildable.
Honest limits
- A card declares intent, not overlap — the match rate is yours to measure (step 7). Free trials and sample rows narrow the risk; they don’t remove it.
- No shared clean key (company names on one side, VAT numbers on the other) means entity resolution — outside what cards solve today.
- Only carded listings are visible to
joinable_on. If you sell, add a join card — uncarded listings do not exist to fusion-planning agents.