npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dataswap

v0.2.0

Published

Official TypeScript/JavaScript client for the Dataswap API — structured SERP, keyword, competitive and marketplace data, billed in credits.

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 202 handoff 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 dataswap

Quick 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 charged

jobId 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.creditsUsed is what you paid; billing.creditsReserved is what was held, when the two differ.
  • They are slow by design — they compose several operations. timeoutMs is per attempt and defaults to 60 s; raise it before calling commerce.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

Licence

MIT