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

ad-preflight

v0.1.2

Published

Meta Ads, Google Ads and LinkedIn Ads MCP server and CLI for pre-launch campaign audits: checks bid, destination, approval, geo, spend cap and landing page before you spend, plus spend reconciliation. Read-only.

Readme

ad-preflight

A read-only Meta Ads, Google Ads and LinkedIn Ads MCP server and CLI that audits a campaign before you spend.

Can this campaign actually serve, and if it serves, can it convert?

ad-preflight reads a Meta, LinkedIn or Google Ads campaign from the platform's API and checks the fields that decide that. It also reconciles real spend across all three against what your records say. It's a CLI, a library and an MCP server, and it is read-only: it never activates, pauses or edits anything.

$ node examples/offline-demo.mjs        # a real preflight against a fake, broken campaign

LinkedIn campaign 123456789  "Q3 webinar - video"
status: ACTIVE

FAIL  Can win an auction: CPV / NONE / unitCost 0
      why: No optimization target and no manual bid is no bid at all. It shows ACTIVE and APPROVED and serves nothing.
      fix: Set an auto-bid target for the objective (e.g. MAX_LEAD, MAX_VIDEO_VIEW) or a manual unitCost above 0.
FAIL  Has somewhere to send people: webinar_15s: media post with no contentLandingPage and no contentCallToActionLabel
      why: Video and image posts without a landing page still serve and log clicks (profile, expand, play), but landing-page clicks stay at 0 forever. A placeholder form URN means no lead form.
      fix: Set contentLandingPage and contentCallToActionLabel on the post, or a real urn:li:adForm destination for lead gen.
PASS  Schedule lets it serve: ACTIVE, ends 2026-11-30
PASS  Creative is approved and serving: webinar_15s: ACTIVE serving
PASS  Geo resolves to the intended place: Columbus, Ohio, United States
PASS  Spend is bounded: total budget $1500.00, $50.00/day
SKIP  Landing page works: no click-out URLs (lead form or no destination)

DO NOT ACTIVATE: Can win an auction; Has somewhere to send people.
It is live now, spending up to $50.00/day while it can't do its job.

Try it with no credentials: node examples/offline-demo.mjs runs the real checks against mocked API responses for a campaign with two classic faults.

Why

A broken ad looks exactly like a working one in every dashboard summary: status ACTIVE, creative APPROVED, impressions rising. The difference is usually in one field that nobody checks. Each check here exists because that field was wrong on a real account:

  • No bid. A LinkedIn campaign with optimizationTargetType: NONE and a $0 unit cost sat ACTIVE and APPROVED for three days and served one impression. The same config shipped again on the next campaign, because the "fix" was patched onto live campaigns and never reached the tool that created them.
  • No destination. Every video ad an uploader built went out with no landing page. The ads served and logged clicks (profile views, video expands), so nothing looked wrong. About $2,000 bought around a dozen landing-page clicks and no leads, and for two months the copy got the blame.
  • Wrong place. City targeting keys collide across states. A spec that says "Springfield" proves nothing about which Springfield the platform resolved.
  • Wrong numbers. The record said a pilot was "paused, about $300 spent". The API said about $2,000. The gap stood for five weeks, and every decision made in that time used the wrong figure.

Install

npm install -g ad-preflight      # or use npx

Use a read-only token (ads_read, r_ads): the tool never writes, and a token that can't write can't be misused through it. Credentials come only from environment variables. Nothing is written to disk, and tokens travel in request headers, never URLs, so they stay out of platform error messages and logs. Errors are scrubbed of credential values before printing. Read-only scopes are enough. See .env.example.

| Platform | Variables | Scope | |---|---|---| | Meta | META_ACCESS_TOKEN | ads_read | | LinkedIn | LINKEDIN_ACCESS_TOKEN, optional LINKEDIN_AD_ACCOUNT_ID | r_ads, r_ads_reporting | | Google Ads | GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, optional GOOGLE_ADS_LOGIN_CUSTOMER_ID | adwords |

API versions default to Meta v24.0, LinkedIn 202609 and Google Ads v25. LinkedIn retires monthly versions; if it answers "version is no longer active", set a newer one. Override them with META_API_VERSION, LINKEDIN_API_VERSION or GOOGLE_ADS_API_VERSION.

Preflight a campaign

ad-preflight meta <campaign_id> --region "Ohio"
ad-preflight linkedin <campaign_id> --account <ad_account_id> --country "United States"
ad-preflight google <customer_id> <campaign_id> --country US

Exit code 0 = SAFE TO ACTIVATE, 1 = DO NOT ACTIVATE, 2 = error. Add --json for machine output.

| Check | Meta | LinkedIn | Google Ads | |---|---|---|---| | Schedule lets it serve | stop_time | status, runSchedule.end | serving status, end date | | Bid can win an auction | cost/bid cap needs bid_amount | auto-bid target, or unitCost > 0 | manual bids > 0, tCPA/tROAS targets set | | Destination exists | link, CTA link, or an active lead form | contentLandingPage + CTA on media posts, article source, real urn:li:adForm | final URLs | | Live | effective_status, review feedback | intendedStatus, review, isServing + hold reasons | approval and review status | | Geo resolves where you meant | every city/region/zip's resolved region | every location URN resolved to its name | geo target canonical names | | Spend is bounded | lifetime budget, end date, account spend_cap | totalBudget or end date | total budget or end date | | Landing page works | fetched, status + redirects | fetched | fetched |

Pass --region or --country whenever you know where the campaign should run. Without it, geo is listed for you to read but not verified.

Reconcile spend

ad-preflight spend --meta act_123 --linkedin 500000000 --google 123-456-7890 --days 30
ad-preflight spend --meta act_123 --ledger ledger.json          # exit 1 on any discrepancy

It pulls real spend per campaign from each platform's reporting endpoint, then reports:

  • What can spend right now, as a headline number. On Google Ads, a paused campaign whose serving status is still SERVING is listed as armed: one switch from spending. A past end date (serving status ENDED) is the real brake.
  • Discrepancies against your ledger: spend gaps over 2% (or $1), and campaigns your records call paused that can spend.
  • Drift: live campaigns missing from the ledger, Meta accounts with no spend cap, and campaigns that spent money for zero clicks and zero leads (usually a structural break, so preflight them).

The ledger is what your records claim. See examples/ledger.example.json:

[{ "platform": "LinkedIn", "campaign": "123456789", "spend": 300, "status": "paused" }]

MCP server

claude mcp add ad-preflight -- npx -y ad-preflight mcp     # Claude Code

For Cursor, Claude Desktop and others:

{ "mcpServers": { "ad-preflight": { "command": "npx", "args": ["-y", "ad-preflight", "mcp"] } } }

Tools: preflight_meta_campaign, preflight_linkedin_campaign, preflight_google_ads_campaign, reconcile_ad_spend. All four are annotated read-only.

Claude Code plugin

/plugin marketplace add GroMarketing/ad-preflight
/plugin install ad-preflight@ad-preflight

It installs the MCP server and a skill that runs a preflight before any activation and reconciles spend before any CAC figure is quoted. Results are reported the same way every time: failures first, observed values instead of "looks fine", and an explicit verdict.

Library

import { preflightMeta, reconcile, formatReport } from 'ad-preflight';

const report = await preflightMeta('120000000000000000', { region: 'Ohio' });
console.log(formatReport(report));

What it doesn't do

  • It doesn't judge creative, copy or targeting strategy. It answers whether the campaign is structurally able to serve and convert.
  • It doesn't verify conversion tracking end to end. Whether your pixel or CAPI event fires on the landing page needs a browser test.
  • It never writes. Fixes are printed as instructions; making them stays your call.

License

MIT