Ads
Save reusable competitor Ads searches for free, or run a governed provider search and inspect its price.
Ads is competitor ad intelligence: the ads a company is currently running, read out of the public ad libraries, so you can tell whether an account buys paid acquisition and what messaging it is testing. It does not buy ads and does not push audiences to an ad platform — audience activation is roadmap, not shipped.
Saved searches are reusable query definitions at /ads/searches. Saving one makes no provider call and costs no credits. A saved search can carry a workspace tag, so a campaign tag links it to its signal types, tables, and other work. Individual ads are results, not tag carriers. To fetch ads, run the saved platform and query through the governed provider tool; that is a separate action with its own price and approval.
oxygen ads searches save --name "Acme Meta ads" --platform facebook_ads --company-domain acme.com --tags competitor-watch
oxygen ads searches list --tag competitor-watch
oxygen ads searches get <id>
oxygen tags attach ad_search_collection <id> --tags competitor-watchFor a keyword search, use --platform facebook_ad_search, linkedin_keyword_search, or tiktok_search with --keyword instead of --company-domain. Domain searches support facebook_ads, google_ads, and linkedin_ads. Add --country-code US when needed. ads searches update edits the saved query; ads searches delete archives it. Over MCP, use oxygen_ads_searches to save or read the definition and oxygen_tags_attach to change its tags.
Ad data can also land in a table column: one column across a list of companies shows who is spending and on what.
Find the operation first
oxygen tools search "competitor ads" --json
oxygen tools get adyntel.linkedin_ads --jsontools get carries the input schema, output shape, availability, and current price. Every operation below is a paid provider call when run live; saving its query above is free.
| What you want | Operation |
|---|---|
| Facebook + Instagram ads for a domain | adyntel.facebook_ads |
| Google search, image, and video ads for a domain | adyntel.google_ads |
| LinkedIn ads for a domain or page id | adyntel.linkedin_ads |
| Search an ad library by keyword | adyntel.facebook_ad_search, adyntel.linkedin_keyword_search, adyntel.tiktok_search |
| Paid vs organic keyword mix, estimated traffic, budget, CPC | adyntel.domain_keywords |
| One LinkedIn ad, or the LinkedIn Ad Library by advertiser | scraper.linkedin_ad, scraper.linkedin_ad_search |
Adyntel runs on Oxygen's managed key. To use your own, open Connections (/connections) → Adyntel → Configure, paste the Adyntel API key and Adyntel account email, then Save key. The scraper.* ad reads are managed only — nothing to connect.
As a table column
Tables (/tables) → your table → the Add column button at the right end of the header row → type ads into Search all tools (e.g. apollo, hunter, firmographics) → pick the operation and map the input column.
From the CLI:
oxygen columns add <table> --label "Meta ads" --kind tool --data-type jsonb \
--definition-json '{"toolId":"adyntel.facebook_ads","inputMapping":{"company_domain":{"type":"column","columnKey":"domain"}}}' --json
oxygen columns run <table> meta_ads --dry-run --json # cost, no credits
oxygen columns run <table> meta_ads --limit 1 --approved --max-credits <cap> --json
oxygen columns run <table> meta_ads --all --background --approved --max-credits <cap> --jsoncolumns add never spends. You can also create the table with its tool columns in one call: put "kind":"tool" and a "definition" (toolId plus inputMapping) on a column in tables create --columns-json.
The domain column must hold a bare domain such as acme.com: https:// and www. are refused before any request, so nothing is charged. Read the first cell before batching, and read the run's creditsUsed to see what an empty answer cost.
Reading the cell
A cell is one JSON object with provider, operation, request, response (Adyntel's body) and meta. The ads are under response: response.ads for LinkedIn and Google, response.results for Meta (each entry is an array of ad variants). A Meta page can be around 80 KB, so pull the values you need with a free formula column instead of reading the whole cell:
oxygen columns add <table> --label "LinkedIn ad count" --kind formula \
--definition-json '{"expression":"path(linkedin_ads, \"response.total_ads\")"}' --jsonFacebook, LinkedIn and Google ad-library lookups return one synchronous page. Oxygen refuses all_ads=true and webhook_url before a supplier call. An unexpected job acknowledgement or unusable page fails with provider_outcome_unknown; inspect it before another paid request. Do not poll these synchronous endpoints. Google Shopping is separate: its supported asynchronous lookup retains the returned ID and reads google_shopping_status without another start.
A usable HTTP 200 page has meta.outcome:ok, including an empty page with terminal cursor state. An inline HTTP 204 has no usable page or cursor and fails with provider_outcome_unknown; Adyntel documents cases where the Meta source cannot be scraped. It is not a successful empty library. The separate Shopping status lookup retains meta.outcome:no_content for HTTP 204. Synthetic examples show shape, not a guaranteed number of ads per page.
Collect bounded pages into a Table
First inspect the free single-page quote. For LinkedIn, the Table ingestion uses rows_path:response.ads, cursor_path:response.continuation_token and cursor_request_key:continuation_token. Google uses the same paths. Meta has arrays of ad variants under response.results; inspect and map that different shape before Table ingestion. Preserve the company and other request filters on every page.
oxygen table-ingestions create-tool-page <table> --tool adyntel.linkedin_ads \
--request-json '{"company_domain":"acme.com"}' \
--rows-path response.ads --row-mapping-json '{"ad_id":"ad_id","advertiser":"advertiser.name"}' \
--cursor-path response.continuation_token --cursor-request-key continuation_token \
--max-pages 1 --upsert-key ad_id --max-credits <approved-cap> --json
oxygen table-ingestions wait <run-id> --json
oxygen table-ingestions items <run-id> --jsonThe Table needs the mapped columns. Ad-ID upsert updates the same row when a page repeats an ID. max_pages bounds one initial item's cursor chain and defaults to 1; the current quote prices each request, not a fixed number of records.
A run's completed status means its bounded work finished. Inspect the last completed item: output.has_next_cursor:true means more pages remain; output.stopped_by:provider_exhausted records an exhausted cursor. A failed or completed_with_errors run is partial. To continue a successful capped run, create another approved run in the same Table, adding the saved item.nextCursor as continuation_token to the same request. Keep the filters, mapping and upsert key; authorize a new cap. Do not restart from page one or automatically retry an unknown supplier outcome.
MCP uses oxygen_table_ingestions_create with source_type:tool_page; read progress with oxygen_table_ingestions_get/wait and the completed items with oxygen_table_ingestions_items. The exact CLI/MCP examples and recovery rules are in oxygen-signals/provider-runbooks/adyntel.md, available through skills get oxygen-signals --file provider-runbooks/adyntel.md or oxygen_skills_get.
One-off lookup
oxygen tools run adyntel.linkedin_ads --input-json '{"company_domain":"acme.com"}' --mode dry-run --json
oxygen tools run adyntel.linkedin_ads --input-json '{"company_domain":"acme.com"}' \
--mode live --approved --max-credits <cap> --jsonA live run is refused without both --approved and --max-credits. Adyntel operations default to dry-run; the scraper.* ad reads default to live — pass --mode dry-run explicitly for a quote.
Over MCP the same path is oxygen_adyntel_ads_search (mode, approved, max_credits). Inspect its schema, then run it through oxygen_capabilities_call from core; ?toolset=enrichment optionally lists it directly.
Related
- Provider catalog — how operations, prices, and availability are exposed.
- LinkedIn data access —
scraper.linkedin_adand the rest of the cookieless LinkedIn reads. - Approvals — the paid-action gate.