dataswap
v0.2.0
Published
Official TypeScript/JavaScript client for the Dataswap API — structured SERP, keyword, competitive and marketplace data, billed in credits.
Maintainers
Readme
dataswap
Official TypeScript/JavaScript client for the Dataswap API — structured SERP, keyword, competitive, on-page and marketplace data, plus the intelligence layer (grounding, schema extraction, AI visibility, entity cards, commerce matching, signed provenance), billed in prepaid credits.
- Zero dependencies. Node 18+, uses the built-in
fetch. - Handles the
202handoff for you. Long operations are polled transparently. - Safe retries. Every request carries an idempotency key, so a retry never double-charges.
- Typed errors with stable codes, and the credit cost of every call.
npm install dataswapQuick start
import { Dataswap } from 'dataswap';
const dataswap = new Dataswap({ apiKey: process.env.DATASWAP_API_KEY });
const serp = await dataswap.search({ q: 'best running shoes', gl: 'us' });
console.log(serp.organic_results?.[0]?.title);
console.log('this call cost', serp.billing.creditsUsed, 'credits');
console.log('balance left:', serp.billing.creditsRemaining);CommonJS works too:
const { Dataswap } = require('dataswap');Get an API key at dataswap.io. Without apiKey, the client reads
DATASWAP_API_KEY from the environment.
What it does that a plain fetch does not
It waits out long operations
Some endpoints (Amazon, reviews, business profiles, app data, on-page crawls) can take longer than
it makes sense to hold an HTTP connection open for. When that happens the API returns 202 with a
job id, and the work continues server-side.
The client polls it for you, with backoff:
// Just looks like it took a while. No job handling on your side.
const product = await dataswap.amazon.asin({ asin: 'B08N5WRWNW' });Prefer to manage jobs yourself? Turn the wait off and you get the handoff:
import { Dataswap, isAsyncHandoff } from 'dataswap';
const dataswap = new Dataswap({ apiKey, waitForJobMs: 0 });
const result = await dataswap.amazon.asin({ asin: 'B08N5WRWNW' });
if (isAsyncHandoff(result)) {
// { job_id: '…', status: 'processing', credits_reserved: 3 }
const status = await dataswap.job(result.job_id);
} else {
// the work fit inside the request — this is already the product
}With waitForJobMs: 0 the same call may return the handoff or the finished result, depending on
how long the work took. isAsyncHandoff narrows it for you — and checks at runtime, which a cast
does not.
Never resubmit a request that returned 202. The work is already running, and a second call is
a second job with a second credit reservation. That is exactly what this client protects you from.
It retries without double-charging
Every mutating request carries an Idempotency-Key, and retries reuse the same one — that is what
makes a retry after a timeout safe. 429 and 5xx are retried with backoff (honouring
Retry-After); 402 insufficient_credits never is, because insisting does not create balance.
It tells you what each call cost
Every response carries a non-enumerable billing property, so JSON.stringify(response) still
gives you exactly what the API sent:
const r = await dataswap.news({ q: 'ai' });
r.billing.creditsUsed; // 1
r.billing.creditsRemaining; // 74_320
r.billing.requestId; // 'req_…' — quote this in any support request
r.billing.idempotentReplay; // true if this was a replay, not a fresh charge
r.billing.jobId; // set when the call went through a 202 handoff; null otherwise
r.billing.creditsReserved; // ceiling held during the call, when it differed from what was chargedjobId is how you see the handoff when it matters. The same endpoint may answer 200 or 202
depending on how long the work takes, and the SDK hides that on purpose — but if a call took 40
seconds, or you need to correlate it with our logs, that's where the job is named.
Intelligence layer
Eight products that answer a question instead of returning rows. Inference runs on self-hosted models, and every response carries a signed provenance receipt.
// Grounding for an agent, trimmed to a token budget — reranked and deduplicated.
const pack = await dataswap.context({ query: 'eu ai act deadlines', token_budget: 2000 });
pack.blocks.map((b) => `[${b.url}] ${b.text}`).join('\n\n');
// Your JSON Schema, filled from the web, with a citation per field.
const filled = await dataswap.extract.schema({
query: 'acme corp pricing',
json_schema: { type: 'object', properties: { plan: { type: 'string' }, price_usd: { type: 'number' } } },
});
filled.fields.filter((f) => f.source_idx === null); // fields with no evidence come back null
// Share of answer across search, AI Overview and LLM answers.
const land = await dataswap.geo.answerLandscape({ query: 'best crm for startups' });
land.share_of_answer[0]?.domain;
// Domain 360, every signal captured at the same instant. Expect 10–20 s.
const card = await dataswap.entity({ domain: 'example.com', include: ['backlinks', 'tech'] });
// Are these offers the same product? same / variant / bundle / different, with evidence.
const m = await dataswap.commerce.match({ product: { title: 'Sony WH-1000XM5', gtin: '4548736132115' } });
// Share of digital shelf, including what the AI Overview cites.
const shelf = await dataswap.commerce.shelf({ brand: 'Acme', keywords: ['noise cancelling headphones'] });
// Verify any receipt — public, free, no key needed by whoever checks it.
const check = await dataswap.provenance.verify({ receipt: card.provenance, payload: body });Two things worth knowing before you budget:
- The price varies with the call. A ceiling is held while the work runs, the charge is sealed
against the real cost, and the difference is refunded.
billing.creditsUsedis what you paid;billing.creditsReservedis what was held, when the two differ. - They are slow by design — they compose several operations.
timeoutMsis per attempt and defaults to 60 s; raise it before callingcommerce.match(up to ~50 s) over a slow link.
Numbers (shares, percentiles, consensus) are computed in code, not by a model, so the same inputs
give the same output. Missing data is reported (partial, unavailable, coverage), never faked.
Errors
import { DataswapError } from 'dataswap';
try {
await dataswap.search({ q: 'x' });
} catch (err) {
if (err instanceof DataswapError) {
if (err.isInsufficientCredits) { /* top up — retrying will not help */ }
if (err.isRateLimited) { /* wait err.retryAfter seconds */ }
if (err.isMissingScope) { /* the key is VALID, it just lacks a scope — do not discard it */ }
if (err.isAuthError) { /* the key itself is the problem */ }
console.error(err.code, err.requestId); // branch on `code`, not on the message
}
}code is the stable contract. message is human-readable and may change — do not match on it.
Credits
Prepaid. cost = base × freshness multiplier + Σ enrichments.
| freshness | multiplier | behaviour |
|---|---|---|
| cached | ×0.5 | returns only if a cached result exists; on a miss → 404 cache_miss at 0 credits |
| fresh (default) | ×1 | serves from cache while valid, otherwise fetches |
| live | ×1.5 | always fetches |
Read the live price book with dataswap.pricing() instead of hard-coding a table — and note that
for the intelligence layer, and for any operation billed per result, the price book gives the
base call: the final charge depends on how much work the call actually produced.
API surface
// SERP
dataswap.search(params) · news(params) · maps(params) · shopping(params) · images(params)
// AI tools (12)
dataswap.ai.answer · contentBrief · visibility · serpDiffExplain · competitorBrief
· reviewSummary · keywordCluster · serpIntent · contentGap · paaExpand
· summarize · rerank
// Long-running (may hand off with 202 — handled for you)
dataswap.amazon.products · asin · sellers · reviews
dataswap.business.info · updates · questionsAndAnswers · hotelSearches · hotelInfo
dataswap.reviews.google · trustpilot · tripadvisor
dataswap.onpage.summary · instant
dataswap.apps.search
// Intelligence layer (variable price: a ceiling is held, the real cost is sealed)
dataswap.context({ query, token_budget })
dataswap.extract.schema({ json_schema, query | urls })
dataswap.geo.answerLandscape({ query, engines })
dataswap.entity({ domain, include })
dataswap.commerce.match({ product, targets }) · shelf({ brand, keywords })
dataswap.provenance.publicKey() · verify({ receipt, payload }) // public, free
// Catalog extraction (always asynchronous; billed per product produced)
dataswap.extract.catalog({ url, max_products })
dataswap.extract.catalogStatus(jobId)
// Jobs and account
dataswap.job(jobId) · usage() · pricing()
// Anything not covered yet
dataswap.request('POST', '/v1/some/endpoint', body)Options
new Dataswap({
apiKey, // or DATASWAP_API_KEY
baseUrl: 'https://api.dataswap.io',
timeoutMs: 60_000, // per attempt
maxRetries: 2, // on 429/5xx/network
waitForJobMs: 300_000, // 0 disables the automatic 202 wait
fetch: customFetch, // bring your own (proxies, instrumentation)
});Every call also takes per-request options:
await dataswap.search({ q: 'x' }, {
idempotencyKey: 'your-own-key', // reuse it to replay a charge, not repeat it
signal: controller.signal,
waitForJobMs: 0,
});Full reference
- Rendered docs: https://dataswap.io/reference
- OpenAPI 3.1 spec: https://dataswap.io/openapi.yaml
- Guides: https://dataswap.io/docs
Licence
MIT
