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

@nytka/plugin-ads

v0.1.1

Published

Google Ads connector for nytka projects. Collects monthly campaign performance into datasets/ and registers it with provenance. Read-only, and no dependencies.

Readme

@nytka/plugin-ads

Google Ads connector for nytka projects. Collects monthly campaign performance into datasets/ and registers it with provenance. Read-only, and no dependencies.

npm i @nytka/plugin-ads

nytka-ads auth          # mint the refresh token — start here
nytka-ads discover      # which accounts can this credential reach
nytka-ads collect       # the last 13 months, per campaign per month

Should you use this, or Google's own MCP?

Both, for different jobs. Google publishes an official Ads MCP server — self-hosted, free, strictly read-only, with a GAQL search tool. If you want an agent to look at an Ads account and answer a question in conversation, use that. It is better at it than this is, and there is nothing to maintain.

What it cannot do is leave anything behind. Every answer lands in a context window and is gone. There is no collectedAt, no payload on disk, no registry entry, and no way to ask what the number was last month rather than what it is now.

This connector exists for exactly that gap, and in Google Ads the gap is unusually wide. See "Conversions are never final" below — an Ads number you read today can legitimately be a different number tomorrow for the same month, so "what did we see, and when did we see it" is not bookkeeping here. It is the data.

No dependencies, and why that is worth mentioning

This was planned as the heaviest connector in the line. google-ads-api depends on google-ads-node at 121 MB — about 126 MB installed — and unlike Search Console or Tag Manager there is no @googleapis/* per-API split to escape into.

That was true and it was also beside the point, because the library is not required. The Ads API transcodes to REST and publishes a discovery document. Two fetch calls do the whole job: one form POST to exchange the refresh token, one JSON POST to run the query.

So this package installs @nytka/core and nothing else. Measured 2026-07-29 with npm pack:

| | unpacked | |---|---| | @nytka/plugin-ads | 57.3 kB | | @nytka/core, its only dependency | 12.5 kB | | total installed | ~70 kB | | the google-ads-api path this replaces | ~126 MB |

That is roughly 1,800× smaller, and it also sidesteps google-gax and with it the rimraf → glob → minimatch → brace-expansion advisory chain, rather than accepting it.

The cost is that OAuth refresh, pagination and error shaping are hand-rolled rather than inherited. That is a fair trade at one query shape and a bad one at ten — if this connector ever grows a broad surface, the arithmetic changes.

Read-only, and the honest caveat

Google Ads publishes exactly one OAuth scope, and it is read-write. There is no adwords.readonly. This is the only connector in this line that cannot promise read-only from the credential itself — @nytka/plugin-gtm holds tagmanager.readonly and could not write if its code tried to.

Read-only is held three ways instead, none of them the scope:

| | | |---|---| | The account role | The refresh token can do whatever the consenting account can do in the Ads UI. Restricting that account to the Read only role in Google Ads → Admin → Access and security is the only place Ads can enforce read-only, and it is outside this code — read the caveat below before you try | | The endpoint allowlist | Every mutating call in the Ads API shares one URL suffix. This package builds URLs from a fixed list of two read endpoints and never from a caller-supplied path. A test greps src/ for that suffix and fails if it ever appears | | The query gate | Only SELECT is accepted, checked before any socket is opened |

Take the first one seriously. A user who believes the credential is read-only will grant it more freely than they should, and here that belief would be wrong.

Two things about that first row, because it is written above as though it were a step and it is not one:

It is containment, not a requirement. This connector works under any role and only ever issues reads. The role decides what the token could do if it leaked — not what this code does with it.

You often cannot apply it to yourself. Google Ads requires an account to keep at least one admin, so the sole admin of an account cannot demote themselves, and would not want to: it costs them UI management of their own campaigns. If the token must be genuinely incapable of writes, invite a second Google account, give it the Read only role, and mint the refresh token signed in as that one. Otherwise accept that the token carries your access and treat GOOGLE_ADS_REFRESH_TOKEN as the admin credential it is.

Setup

Four values. All free. They come from two different Google consoles, and that is the part that trips people up:

| | comes from | |---|---| | GOOGLE_ADS_DEVELOPER_TOKEN | ads.google.com — the Ads UI you already use | | GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET | console.cloud.google.com — a different product | | GOOGLE_ADS_REFRESH_TOKEN | your terminal, via nytka-ads auth | | the customer id | your terminal, via nytka-ads discover |

Nothing links the two consoles to each other. You are not associating a Cloud project with an Ads account anywhere — the only connection is that all four values land in one .env.

1. A developer token — in the Ads UI

Google Ads → Tools → API Center (ads.google.com/aw/apicenter), inside a manager (MCK) account.

A manager account is a separate account type, not a setting on the account you already have. If you sign into your normal Ads account and there is no API Center, that is why — it is not a mistake at your end. Creating one is free, at ads.google.com/home/tools/manager-accounts. Link your existing account to it and API Center appears.

Check which tier you were granted before going further. The signup default is Test Account access, which cannot query production accounts at all. Explorer can, and is often granted automatically, but it is not guaranteed and it is capped at 2,880 operations/day against production. Basic lifts that to 15,000 and takes about five business days to review; Standard is about ten.

A 13-month backfill across several accounts can reach the Explorer cap. If collect starts failing on quota rather than on credentials, that is where to look.

Tiers and limits verified 2026-07-29 against Google's access-levels page. Google moves these — re-check if it is much past 2027-01.

2. An OAuth client — in Cloud console

Reuse the Cloud project you already have. If GA4, GSC or GTM is installed on this project, one exists with APIs already enabled on it. On that project:

  1. Enable the Google Ads API — APIs & Services → Library. Creating the client does not do this. Skipping it fails at discover and never at auth, so it presents as a credential problem when it is not one.
  2. Credentials → Create credentials → OAuth client ID → Desktop app.

Put the id and secret in .env. The secret is shown once.

Do not reuse the service account. Every other Google connector in this line runs on GOOGLE_SERVICE_ACCOUNT_KEY, so it is the first thing to reach for, and it cannot work here. Google Ads has no way to admit a service account — you cannot add one under Access and security, because that flow sends an invitation a service account has no way to accept. The only service-account route into Ads is Workspace domain-wide delegation, which is a different setup and not one this connector asks for.

The OAuth client sits alongside the service account on the same Cloud project. They do not conflict.

You do not need to configure a redirect URI. Desktop-app clients accept any loopback port and auth binds a throwaway one. People go hunting for that setting; it does not exist for this client type.

3. Publish the consent screen — in Cloud console

Google Auth Platform → Audience → Publish app, before running auth.

While the app is in Testing, consent is refused outright for any account not on the test-user list — you are blocked long before the seven-day token expiry can reach you. Publishing fixes both at once.

On a personal @gmail.com the user type is External, and External + unverified is a working configuration for a connector with one user:

  • You will see a "Google hasn't verified this app" screen at consent. Click Advanced → Go to (unsafe). Expected, and once.
  • Unverified apps are capped at 100 users. You are one.
  • Verification — privacy policy, domain ownership, weeks of review — is only needed to remove that screen or to exceed 100 users. Neither applies.

Workspace accounts can choose Internal and skip the warning screen. That is the only thing Internal buys here; it is not a reason to acquire Workspace.

The alternative is staying in Testing, adding yourself under Audience → Test users, and re-minting the token every seven days. Publishing is the better trade.

4. A refresh token — back in your terminal

npx nytka-ads auth

It prints a URL, waits on a loopback port, exchanges the code and prints the token. Open the URL, sign in as an account with access to the Ads account, approve. Paste the result into .env as GOOGLE_ADS_REFRESH_TOKEN.

It does not expire, so this is a one-time step — and re-running is always safe: consent is forced every time, so you get a fresh token rather than the silent empty response Google returns on a repeat approval.

auth is the one command in this line that prints a credential, because a refresh token that isn't shown to the person who just consented exists nowhere else. It lands in your terminal scrollback; the command says so when it runs.

5. The customer id — in your terminal

nytka-ads discover lists every account the token reaches. A manager account holds no campaigns of its own, so collect the client accounts under it and set GOOGLE_ADS_LOGIN_CUSTOMER_ID to the manager.

Every variable, with the same prose, is in .env.example:

cat node_modules/@nytka/plugin-ads/.env.example >> .env

Unset, every command names what is missing and exits 0. An unconfigured project is not a broken install (decision 0008).

collect

nytka-ads collect                                  # last 13 whole months
nytka-ads collect --customer 123-456-7890
nytka-ads collect --from 2025-01-01 --to 2025-12-31
nytka-ads collect --no-register

One row per campaign per month — impressions, clicks, cost, conversions, conversion value — plus a per-month total. Cost is kept in micros exactly as the API returns it and also converted, because micros is the only lossless form.

The dataset id is keyed to the customer and the period at month granularity, not the exact window. Running this daily for a month replaces one entry rather than leaving thirty near-identical ones.

Conversions are never final

Search Console and GA4 finalise: wait two or three days and the number stops moving. Ads does not work that way. Impressions, clicks and cost settle within about a day, but conversions keep accruing for as long as the account's conversion window, which can be 90 days. No lag makes a recent month final.

So this connector does not pretend otherwise. The default window trims one day to drop the partial tail, collectedAt is written onto the payload as well as the registry entry, and validUntil is null — one expiry cannot honestly describe a window whose older months are settled and whose newest month can move tomorrow.

Re-collect, and the entry is replaced. The difference between the two readings is a real finding about attribution, not a bug.

Importing it

import { run, client, collectCampaigns, authorize } from '@nytka/plugin-ads'

const res = await run({ customerId: '1234567890' })

run returns counts, totals and paths — never the campaign rows. A year of per-campaign spend belongs in a payload to be queried from a file, not loaded into an agent's context.

Every network-touching function takes an injectable client or fetchImpl, which is how the 85 tests in this package run with no credential and no network.

Licence

MIT