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

sandstormapp

v1.1.0

Published

The ads.dev API for Node, Deno, Bun and edge runtimes: image, video and audio generation on your brand, the brand kit and library, and competitor ads

Readme

ads.dev

The ads.dev API for Node, Deno, Bun and edge runtimes: image, video and audio generation with 34 models on your brand, the brand kit and library, and the ads your competitors run. Every model's input is typed, so the model you name decides what input takes.

It is published as ads.dev, adsdotdev, sandstormapp and @sandstormapp/sdk, the same package under four names, and needs nothing at runtime but fetch and Web Crypto.

npm install ads.dev
import { Sandstorm } from 'ads.dev'

const sandstorm = new Sandstorm()

const generation = await sandstorm.generations.run('nano-banana-pro', {
  prompt: 'A ceramic mug on a sunlit bench, steam rising',
  aspect_ratio: '4:5',
})
console.log(generation.output[0]?.url)

A key

new Sandstorm() reads SANDSTORM_API_KEY. Make a key in Sandstorm under Settings → API keys, or in one command, which signs you in in the browser and writes the key to .env:

curl -fsSL https://ads.dev/login.mjs -o sandstorm-login.mjs && node sandstorm-login.mjs
new Sandstorm({
  apiKey: process.env.SANDSTORM_API_KEY,
  brand: '6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f',
  baseUrl: 'https://ads.dev',
  maxRetries: 2,
  timeoutMs: 60_000,
})

A key acts on the brand it was made on; brand names another brand of the same organization. baseUrl defaults to SANDSTORM_API_URL, then https://ads.dev. Keep the key on your server: anyone who holds it spends your credits.

Generations

run makes a generation and answers it once it has finished, holding each request on the API with Prefer: wait rather than polling. A generation that fails or is canceled throws GenerationFailed, which carries it.

const still = await sandstorm.generations.run('nano-banana-2-lite', { prompt: 'Two lemons in a blue bowl' })

const clip = await sandstorm.generations.run('seedance-2.5/image-to-video', {
  start_image: still.output[0]!.id,
  prompt: 'Slow push in as the light warms',
  resolution: '720p',
})

Every media input takes an asset id (asset_…) or a public https URL, so the output of one generation goes straight into the next.

For work that runs long, queue it and come back to it, or take a webhook:

const queued = await sandstorm.generations.create('kling-3-pro/image-to-video', input, { webhookUrl: 'https://yourapp.com/hooks/sandstorm' })
const finished = await sandstorm.generations.wait(queued.id)

await sandstorm.generations.get(queued.id)
await sandstorm.generations.cancel(queued.id)

A generation is queued, then running, then succeeded, failed or canceled; credits is what it was charged once it finished, and a failed one is free. Cancelling a queued generation costs nothing; one the model has already started stops where the model allows it, and otherwise finishes and is charged.

create sends an Idempotency-Key with every call and keeps it across its retries, so a retry never makes a second generation. Pass your own to make a call safe across process restarts:

await sandstorm.generations.create('nano-banana-pro', input, { idempotencyKey: 'campaign-42-hero' })

Lists

Every list answers one page when awaited and walks every page when iterated:

const page = await sandstorm.generations.list({ status: 'succeeded', limit: 50 })

for await (const asset of sandstorm.assets.list({ type: 'image' })) console.log(asset.id, asset.url)

Everything else

await sandstorm.models.list({ type: 'video' })
await sandstorm.models.get('nano-banana-pro/edit')

await sandstorm.brand.get()
await sandstorm.assets.import('https://yourcdn.com/hero.png', { name: 'Hero shot' })
await sandstorm.assets.get('asset_9c1e77aa')
await sandstorm.offers.list({ query: 'cold brew' })
await sandstorm.offers.get('offer_0f8e2b6a4c1d4e7f9a3b5c7d9e1f2a4b')
await sandstorm.characters.list({ kind: 'subject' })
await sandstorm.voices.list({ use_case: 'advertisement', limit: 5 })

const [advertiser] = (await sandstorm.advertisers.list({ query: 'Liquid Death' })).data
await sandstorm.ads.list({ advertiser: advertiser!.id, status: 'active' })
await sandstorm.ads.get('ad_5e1c9a3b')

Webhooks

A generation made with webhookUrl is POSTed to it once it finishes, signed in Sandstorm-Signature. Verify the raw body before you trust it:

import { verifyWebhook } from 'ads.dev'

const secret = await sandstorm.webhooks.secret()

const event = await verifyWebhook({ secret, body: await request.text(), signature: request.headers.get('sandstorm-signature') })
if (event.type === 'generation.succeeded') console.log(event.data.output)

It refuses a body that was changed, a signature from another secret and a delivery older than five minutes. An event's id is the same on every retry of it, so a handler can skip one it has seen.

Errors

A refusal throws SandstormError with the HTTP status, its type and code, the param at fault and the requestId to quote to support. Requests Sandstorm could not take (408, 429, 500, 502, 503, 504) and network failures are retried twice with backoff, after the Retry-After the API asks for.

import { SandstormError } from 'ads.dev'

try {
  await sandstorm.generations.create('nano-banana-pro', { prompt: 'A mug on a bench' })
} catch (error) {
  if (error instanceof SandstormError && error.code === 'insufficient_credits') console.log('Add credits in Sandstorm')
}

Developing

openapi.json is the API's own document, held to what the API serves by the agent project's tests; yarn generate turns it into src/openapi.ts, and yarn ci:test fails when the two disagree. yarn release publishes the build as ads.dev, adsdotdev, sandstormapp and @sandstormapp/sdk.