Skip to content
Gauge

ax-check.com

Netlify

netlify.com

ax-check.com

Checked 6 October 2026

A

98/100

Agents need your login

Netlify's docs, CLI, and pricing are all readable without logging in

22 of 23 checklist items passed; only gap is llms.txt not mentioning Netlify's API, MCP server, or skills directly.

2 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

List every offered API, MCP, and skills surface in /llms.txt.

llms.txt lists platform/docs pages but never mentions the offered API, MCP servers, or skills.

Recorded coding sessions

Same task, 3 models, no netlify.com account. Recorded 6 October 2026 on Pi.

3 recorded · 3m 48s

3m 48s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using Netlify. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

The user is asking for a simple example using Netlify, and wants a pricing explanation and a feasibility assessment. Let me check the environment—can I even install things? I'll look at the sandbox and see what's available.

Let me take a look at the current directory.

ls -la /sandbox/repo && node --version && npm --version && git log --oneline -3 2>/dev/null
total 16
total 16
drwxr-xr-x 3 root root 4096 Sep 18 08:00 .
drwxr-xr-x 7 root root 4096 Oct  6 19:28 ..
drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
v22.23.2
10.9.8
9ebf5fb Initial workspace
env | grep -i -E "netlify|token" | sed 's/=.*/=<redacted>/'; ls ~/.netlify 2>/dev/null; echo "---"; cat /sandbox/repo/package.json 2>/dev/null
Failed---
---


Command exited with code 1
Thinking…

Empty repository. No Netlify token. Let me check network access to the Netlify API and try to see if I can install netlify-cli / js-client. Without an auth token, I can't deploy anything. Let me check network connectivity.

cd /sandbox/repo && timeout 25 npm view netlify version 2>&1 | tail -3; echo "=== registry:$?"; timeout 15 curl -s -o /dev/null -w "api.netlify.com -> %{http_code}\n" https://api.netlify.com/api/v1/ 2>&1
npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0
npm notice To update run: npm install -g npm@12.2.0
npm notice
=== registry:0
api.netlify.com -> 401
timeout 15 curl -s -o /dev/null -w "google -> %{http_code}\n" https://www.google.com; timeout 15 curl -s -o /dev/null -w "npmjs -> %{http_code}\n" https://registry.npmjs.org/
google -> 200
google -> 200
npmjs -> 200
Thinking…

Network is working. No NETLIFY_AUTH_TOKEN. So I can't create an actual deploy (would require login/token). But I can still build a working sample project using the Netlify JS SDK (netlify npm package) — unauthenticated calls fail, but I can write code and validate against the API surface, plus mock out things.

Honest approach: Netlify has a developer workflow (API/SDK) that I can handle. I can build a sample: a small static site + serverless function + a Node script using the netlify JS client to create the site and deploy via zip. But an actual deploy requires NETLIFY_AUTH_TOKEN, which doesn't exist → blocked there. Per the instructions, "let me know if you get stuck."

Should I install the SDK? That's fine — npm install isn't a long-running service stack. Let me install netlify (JS client) and validate that the script can run (e.g., call netlify.status() without a token → expect auth error, proving connectivity and SDK wiring are working).

Let me do this quickly.

Plan:

  1. npm init, install netlify SDK.
  2. Create a small site: public/index.html + netlify/functions/hello.mjs (function using the built-in handler format) + netlify.toml.
  3. Create scripts/deploy.mjs using the SDK: createSite + deploy with a zip (use built-in deploy with a directory. Needs zip streaming — the SDK handles it).
  4. Run a smoke test without a token → shows 401, confirming the only blocker is credentials.

Let me build it.

Network and npm work; no Netlify credentials in the environment. Let me scaffold the example and verify how far I can get.

cd /sandbox/repo && timeout 180 npm install netlify 2>&1 | tail -5 && node -e "const N=require('netlify');console.log('sdk ok', typeof new N({}).deploy)"
Failedadded 1000 packages in 2m

added 1000 packages in 2m

301 packages are looking for funding
  run `npm fund` for details
[eval]:1
const N=require('netlify');console.log('sdk ok', typeof new N({}).deploy)
                                                        ^

TypeError: N is not a constructor
    at [eval]:1:57
    at runScriptInThisContext (node:internal/vm:209:10)
    at node:internal/process/execution:446:12
    at [eval]-wrapper:6:24
    at runScriptInContext (node:internal/process/execution:444:60)
    at evalFunction (node:internal/process/execution:279:30)
    at evalTypeScript (node:internal/process/execution:291:3)
    at node:internal/main/eval_string:74:3

Node.js v22.23.2


Command exited with code 1
Thinking…

ESM-style exports. Let's check the package.

cd /sandbox/repo && cat node_modules/netlify/package.json | head -30
{
{
  "name": "netlify",
  "version": "27.11.2",
  "description": "Netlify command line tool",
  "keywords": [
    "api",
    "cli",
    "netlify",
    "static"
  ],
  "homepage": "https://github.com/netlify/cli",
  "bugs": {
    "url": "https://github.com/netlify/cli/issues"
  },
  "license": "MIT",
  "author": "Netlify Inc.",
  "repository": "https://github.com/netlify/cli",
  "bin": {
    "npxnetlify": "./bin/run.js"
  },
  "files": [
    "/bin",
    "/scripts",
    "/functions-templates",
    "/dist",
    "!dist/**/*.map"
  ],
  "type": "module",
  "scripts": {
    "build": "node ./node_modules/typescript-native/bin/tsc --project tsconfig.build.json",
Thinking…

The netlify package is currently the CLI itself, not the JS client. The JS client is netlify-js-client... actually, the official one is netlify (formerly js-client), but now this is the CLI. The separate package @netlify/js-client? Let's check. Historically: netlify-js-client → renamed to netlify. Currently, the CLI occupies the netlify name, and the JS client is @netlify/js-client? Let's search for it.

cd /sandbox/repo && timeout 60 npm view @netlify/js-client version description 2>&1 | head -5; echo "==="; timeout 60 npm view netlify-js-client version deprecated 2>&1 | head -5
npm error code E404
npm error code E404
npm error 404 Not Found - GET https://registry.npmjs.org/@netlify%2fjs-client - Not found
npm error 404
npm error 404  '@netlify/js-client@*' is not in this registry.
npm error 404
===
npm error code E404
npm error 404 Not Found - GET https://registry.npmjs.org/netlify-js-client - Not found
npm error 404
npm error 404  'netlify-js-client@*' is not in this registry.
npm error 404
Thinking…

In other words, the JS client currently only exists inside the CLI, exported as netlify via the CLI's exports? Let me check the CLI's exports field.

cd /sandbox/repo && node -e "
const p=require('./node_modules/netlify/package.json');
console.log(JSON.stringify({main:p.main,exports:p.exports,types:p.types},null,2));
" && ls node_modules/netlify/dist | head -20
{
{
  "main": "./dist/index.js"
}
commands
index.d.ts
index.js
lib
recipes
utils
Thinking…

In other words, the netlify npm package = the CLI. A programmatic deployment path does exist: netlify deploy CLI commands, and the underlying API client located at dist/lib/api.js. Let me verify.

Actually, the cleanest approach is to use the Netlify REST API directly via fetch (no SDK confusion), plus optionally use the CLI for zip deployments. The CLI's netlify deploy requires an auth token. The REST API's zip deployment endpoint: PUT /api/v1/sites/{site_id}/deploys with a zip body. To create a zip, you need the zip binary — let me check whether zip is available.

Plan:

  • site/: a small static index.html
  • netlify/functions/hello.mjs: a serverless function
  • netlify.toml
  • scripts/netlify.mjs: a zero-dependency API client wrapper using fetch (createSite, zip deploy, log listing) + also uses the CLI if it's installed
  • Use the CLI's internal api client? More simply: write a small fetch-based client. That's the API path they asked for ("SDK or API").

Let me check for the presence of zip and the availability of the CLI.

cd /sandbox/repo && ls node_modules/netlify/dist/lib/ | head -30; echo "=== zip:"; which zip unzip; echo "=== cli:"; timeout 90 npx netlify --version 2>&1 | tail -3
account.d.ts
account.d.ts
account.js
api.d.ts
api.js
blobs
build.d.ts
build.js
completion
edge-functions
exec-fetcher.d.ts
exec-fetcher.js
extensions.d.ts
extensions.js
fs.d.ts
fs.js
functions
geo-location.d.ts
geo-location.js
http-agent.d.ts
http-agent.js
images
log.d.ts
log.js
path.d.ts
path.js
settings.d.ts
settings.js
spinner.d.ts
spinner.js
string.d.ts
=== zip:
/usr/bin/zip
/usr/bin/unzip
=== cli:
netlify/27.11.2 linux-x64 node-v22.23.2
Thinking…

The CLI works. zip is available. Next, write the sample project.

Structure:

netlify.toml
site/index.html
netlify/functions/hello.mjs
scripts/netlify-api.mjs   (small fetch client)
scripts/deploy.mjs        (create site + zip deploy via REST API)
scripts/smoke.mjs         (verifies the deployed site)
README.md

Actually, keep it simple and functional: one script deploy.mjs that does create + deploy, and a check.mjs that hits the deployed URL. Plus a smoke test of the function logic locally without a server (importing the handler and calling it directly — no long-running server).

Alright, let's start writing.

netlify.toml:

[build]
  publish = "site"
  functions = "netlify/functions"

[functions]
  node_bundler = "esbuild"

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200

hello.mjs:

export default async (req, context) => {...}

Netlify Functions' modern handler format: export default async (req, context) => new Response(...).

The site's index.html fetches /api/hello.

deploy.mjs: use fetch against api.netlify.com:

  • GET /api/v1/user (auth check)
  • POST /api/v1/sites {name?} → site_id
  • cd site+functions... Actually, for a zip deploy, you need a directory that contains both index.html and netlify/functions/hello.mjs. Build dist/ and copy the site files + netlify/functions. Then zip -r.
  • PUT /api/v1/sites/{id}/deploys, Content-Type: application/zip, body = zip buffer.

Important: for zip deploys, the functions need to be in netlify/functions/ inside the zip, and the zip deploy processes them. Yeah — the docs say to include netlify/functions/hello.js in the zip.

Also note: zip deploys require the functions to be bundled/compiled? For plain JS using ESM handlers, Netlify handles it. Good enough as a sample.

Let's write the files.

CLI and zip are available, and the netlify npm package is now the CLI (the standalone JS client is gone). I'll build against the REST API + CLI.

mkdir -p /sandbox/repo/site /sandbox/repo/netlify/functions /sandbox/repo/scripts
(no output)
(no output)

File edits

1 file

Completed
Writtennetlify.tomlView changes

/sandbox/repo/netlify.toml

Written content · previous contents were not recorded

[build]
publish = "site"
functions = "netlify/functions"
# No build command: this is a static site + a serverless function.
# Deploy is done by zipping the tree (see scripts/deploy.mjs), so no build step is needed.
[functions]
node_bundler = "esbuild"
# /api/hello -> /.netlify/functions/hello
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
Thinking…

Next, the site and functions.

File edits

1 file

Completed
Writtensite/index.htmlView changes

/sandbox/repo/site/index.html

Written content · previous contents were not recorded

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Netlify minimal example</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 4rem auto; padding: 0 1rem; line-height: 1.6; }
code { background: #f2f2f2; padding: .1rem .3rem; border-radius: 3px; }
pre { background: #f7f7f7; padding: .75rem; border-radius: 6px; overflow-x: auto; }
</style>
</head>
<body>
<h1>Netlify minimal example</h1>
<p>
This page is served straight from <code>site/index.html</code>. The button below calls the
serverless function in <code>netlify/functions/hello.mjs</code> through the
<code>/api/*</code> redirect defined in <code>netlify.toml</code>.
</p>
<button id="go">Call /api/hello</button>
<pre id="out">no response yet</pre>
<script>
document.getElementById("go").addEventListener("click", async () => {
const out = document.getElementById("out");
out.textContent = "loading...";
try {
const res = await fetch("/api/hello?name=Netlify");
out.textContent = JSON.stringify(await res.json(), null, 2);
} catch (err) {
out.textContent = "error: " + err.message;
}
});
</script>
</body>
</html>

File edits

1 file

Completed
Writtennetlify/functions/hello.mjsView changes

/sandbox/repo/netlify/functions/hello.mjs

Written content · previous contents were not recorded

// Netlify Function — modern handler signature: (request, context) => Response
// Served at /.netlify/functions/hello and, via the netlify.toml redirect, at /api/hello.
export default async (request, context) => {
const url = new URL(request.url);
const name = url.searchParams.get("name") || "world";
return Response.json({
message: `Hello, ${name}!`,
path: url.pathname,
method: request.method,
// Provided by the Netlify runtime; useful to confirm you are on the platform.
site: context.site?.name ?? null,
requestId: context.requestId ?? null,
at: new Date().toISOString(),
});
};
Thinking…

Next up is the API client script. Zero dependencies, using fetch. Also including zip creation via the system zip.

Contents of deploy.mjs:

  1. Read the token from NETLIFY_AUTH_TOKEN (or NETLIFY_TOKEN).
  2. GET /api/v1/user → account info.
  3. Look up or create a site (with a name hint from the SITE_NAME environment variable, or auto-generated).
  4. Build dist/: copy site/* and netlify/functions/*, plus netlify.toml? netlify.toml isn't needed for zip deploy. The function directory has to be netlify/functions inside the zip. Actually, in the case of zip deploy, Netlify detects functions at netlify/functions/** in the zip root. Good.
  5. zip -r deploy.zip (run from inside dist)
  6. PUT /api/v1/sites/{site_id}/deploys with a zip body → deploy object including the deploy_ssl_url.
  7. Poll GET /api/v1/deploys/{deploy_id} until state === "ready".
  8. Print the URL.

Also create verify.mjs, fetch the deployed site + /api/hello, and display the output.

Keep it clean. Write both.

File edits

1 file

Completed
Writtenscripts/netlify-api.mjsView changes

/sandbox/repo/scripts/netlify-api.mjs

Written content · previous contents were not recorded

// Minimal Netlify REST API client (zero dependencies, uses global fetch).
// Docs: https://docs.netlify.com/api/get-started/
const BASE = "https://api.netlify.com/api/v1";
export function tokenFromEnv() {
return (
process.env.NETLIFY_AUTH_TOKEN ||
process.env.NETLIFY_TOKEN ||
null
);
}
export function netlify(token) {
if (!token) {
throw new Error(
"No Netlify credentials. Set NETLIFY_AUTH_TOKEN (personal access token from " +
"https://app.netlify.com/user/applications/personal-access-tokens)."
);
}
async function call(path, { method = "GET", body, headers = {}, raw = false } = {}) {
const init = { method, headers: { ...headers } };
if (!init.headers.Authorization) init.headers.Authorization = `Bearer ${token}`;
if (body !== undefined) {
if (raw) {
init.body = body; // already-encoded payload (e.g. a zip Buffer)
} else {
init.body = JSON.stringify(body);
init.headers["Content-Type"] = "application/json";
}
}
const res = await fetch(`${BASE}${path}`, init);
const text = await res.text();
if (!res.ok) {
throw new Error(`${method} ${path} -> ${res.status} ${res.statusText}\n${text.slice(0, 500)}`);
}
return text ? JSON.parse(text) : null;
}
return {
/** GET /user — proves the token works and shows the account/team. */
currentUser: () => call("/user"),
/** GET /accounts — teams the token can deploy into. */
accounts: () => call("/accounts"),
/** POST /sites — create a site. `name` is a suggestion; Netlify may suffix it. */
createSite: ({ name, accountSlug } = {}) =>
call(`/sites${accountSlug ? `?account_slug=${accountSlug}` : ""}`, {
method: "POST",
body: { name: name || undefined },
}),
/** GET /sites/:site_id */
getSite: (siteId) => call(`/sites/${siteId}`),
/** GET /sites?name=... — lookup by exact site name. */
findSiteByName: async (name) => {
const sites = await call(`/sites?name=${encodeURIComponent(name)}`);
return sites.find((s) => s.name === name) || null;
},
/**
* PUT /sites/:site_id/deploys — publish a zip atomically.
* The zip must contain the publish dir contents at its root and functions
* under netlify/functions/.
*/
deployZip: (siteId, zipBuffer) =>
call(`/sites/${siteId}/deploys`, {
method: "POST",
body: zipBuffer,
raw: true,
headers: { "Content-Type": "application/zip" },
}),
/** GET /deploys/:deploy_id — poll until state === "ready". */
getDeploy: (deployId) => call(`/deploys/${deployId}`),
/** GET /sites/:site_id/deploys — deploy history (rollback targets). */
deploys: (siteId, perPage = 10) =>
call(`/sites/${siteId}/deploys?per_page=${perPage}`),
/** DELETE /sites/:site_id — tear the example down. */
deleteSite: (siteId) => call(`/sites/${siteId}`, { method: "DELETE" }),
};
}

File edits

1 file

Completed
Writtenscripts/build-zip.mjsView changes

/sandbox/repo/scripts/build-zip.mjs

Written content · previous contents were not recorded

// Package the repo into a deployable zip without running any build or local server.
//
// dist/
// index.html <- from site/ (publish dir contents at zip root)
// netlify/functions/hello.mjs (functions keep their standard path)
// netlify.toml (redirects are read from the zip too)
//
// Exports buildZip() -> { zipPath, distDir }; also runnable directly.
import { execFileSync } from "node:child_process";
import { cpSync, mkdirSync, rmSync, existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export function buildZip(outDir = join(repoRoot, "dist")) {
rmSync(outDir, { recursive: true, force: true });
mkdirSync(join(outDir, "netlify", "functions"), { recursive: true });
// publish dir contents -> zip root
cpSync(join(repoRoot, "site"), outDir, { recursive: true });
// functions must live at netlify/functions/ inside the zip
cpSync(join(repoRoot, "netlify", "functions"), join(outDir, "netlify", "functions"), {
recursive: true,
});
// redirects / function config travel with the deploy
cpSync(join(repoRoot, "netlify.toml"), join(outDir, "netlify.toml"));
const zipPath = join(repoRoot, "deploy.zip");
rmSync(zipPath, { force: true });
// -r recursive, -q quiet, -X no extra file attributes
execFileSync("zip", ["-r", "-q", "-X", zipPath, "."], { cwd: outDir });
if (!existsSync(zipPath)) throw new Error(`zip failed: ${zipPath} not created`);
return { zipPath, distDir: outDir };
}
if (import.meta.url === `file://${process.argv[1]}`) {
const { zipPath } = buildZip();
console.log(zipPath);
}
Thinking…

Next up are deploy.mjs, verify.mjs, and the package.json scripts. Plus a test that doesn't need a token: calling the handler directly (test-function.mjs) — proving that the function logic works even without a Netlify account.

Writing deploy.mjs.

File edits

1 file

Completed
Writtenscripts/deploy.mjsView changes

/sandbox/repo/scripts/deploy.mjs

Written content · previous contents were not recorded

// Create (or reuse) a Netlify site and publish the zip. No build step, no local server.
//
// NETLIFY_AUTH_TOKEN=xxx node scripts/deploy.mjs
// NETLIFY_AUTH_TOKEN=xxx SITE_NAME=my-demo node scripts/deploy.mjs
//
// Tear everything down afterwards with: node scripts/teardown.mjs
import { readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { netlify, tokenFromEnv } from "./netlify-api.mjs";
import { buildZip } from "./build-zip.mjs";
const SITE_NAME = process.env.SITE_NAME || null;
const STATE_FILE = join(process.cwd(), ".netlify-site.json");
const POLL_MS = 2000;
const POLL_MAX = 30;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function main() {
const api = netlify(tokenFromEnv());
const user = await api.currentUser();
console.log(`authenticated as: ${user.email}`);
// Reuse a site we created before if we recorded one.
let site = null;
try {
const saved = JSON.parse(readFileSync(STATE_FILE, "utf8"));
site = await api.getSite(saved.id);
console.log(`reusing site: ${site.name} (${site.id})`);
} catch {
if (SITE_NAME) site = await api.findSiteByName(SITE_NAME);
if (!site) {
site = await api.createSite({ name: SITE_NAME || undefined });
console.log(`created site: ${site.name} (${site.id})`);
} else {
console.log(`found site: ${site.name} (${site.id})`);
}
writeFileSync(STATE_FILE, `${JSON.stringify({ id: site.id, name: site.name }, null, 2)}\n`);
}
const { zipPath } = buildZip();
const zip = readFileSync(zipPath);
console.log(`deploying ${(statSync(zipPath).size / 1024).toFixed(1)} KB from ${zipPath}`);
const deploy = await api.deployZip(site.id, zip);
console.log(`deploy ${deploy.id} state=${deploy.state}`);
// Zip uploads are processed asynchronously; wait for "ready".
let current = deploy;
for (let i = 0; i < POLL_MAX && current.state !== "ready"; i++) {
await sleep(POLL_MS);
current = await api.getDeploy(deploy.id);
console.log(` ...state=${current.state}`);
}
if (current.state !== "ready") {
console.error(`deploy did not become ready (state=${current.state}). Check:`);
console.error(` https://app.netlify.com/sites/${site.name}/deploys/${deploy.id}`);
process.exit(1);
}
console.log(`\nlive: ${current.deploy_ssl_url || current.deploy_url}`);
console.log(`api: ${current.deploy_ssl_url || current.deploy_url}/api/hello?name=Netlify`);
console.log(`admin: https://app.netlify.com/sites/${site.name}`);
}
main().catch((err) => {
console.error(`\nFAILED: ${err.message}`);
process.exit(1);
});

File edits

1 file

Completed
Writtenscripts/test-function.mjsView changes

/sandbox/repo/scripts/test-function.mjs

Written content · previous contents were not recorded

// Exercise the function handler in-process: no Netlify account, no local server.
// node scripts/test-function.mjs
import handler from "../netlify/functions/hello.mjs";
const cases = [
{ url: "https://example.com/.netlify/functions/hello", expect: "Hello, world!" },
{ url: "https://example.com/.netlify/functions/hello?name=Netlify", expect: "Hello, Netlify!" },
];
let failures = 0;
for (const { url, expect } of cases) {
const res = await handler(new Request(url, { method: "GET" }), {
requestId: "local-test",
site: { name: null },
});
const body = await res.json();
const ok = res.status === 200 && body.message === expect;
console.log(`${ok ? "PASS" : "FAIL"} ${url}\n ${JSON.stringify(body)}`);
if (!ok) failures++;
}
console.log(`\n${cases.length - failures}/${cases.length} passed`);
process.exit(failures ? 1 : 0);
Thinking…

Wait — deploy.mjs uses api.deployZip with method POST to /sites/:id/deploys. The docs say POST /sites/{site_id}/deploys with a zip body. In my client comment I wrote PUT but used POST — fix the comment. Let me verify: Netlify API "createSiteDeploy" is POST /sites/{site_id}/deploys. Right. Fix the comment.

Additionally verify. verify.mjs and teardown.mjs, package.json.

File edits

1 file

Completed
Writtenscripts/verify.mjsView changes

/sandbox/repo/scripts/verify.mjs

Written content · previous contents were not recorded

// Verify a deployed site: fetch the page and the function through /api/hello.
// node scripts/verify.mjs [url] (defaults to the URL from .netlify-site.json)
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { netlify, tokenFromEnv } from "./netlify-api.mjs";
let base = process.argv[2];
if (!base) {
const saved = JSON.parse(readFileSync(join(process.cwd(), ".netlify-site.json"), "utf8"));
const api = netlify(tokenFromEnv());
const [latest] = await api.deploys(saved.id, 1);
base = latest.deploy_ssl_url || latest.deploy_url;
}
base = base.replace(/\/$/, "");
let failures = 0;
const checks = [
{ name: "index.html", url: `${base}/`, test: (t) => t.includes("Netlify minimal example") },
{
name: "function via /api/hello redirect",
url: `${base}/api/hello?name=Netlify`,
json: true,
test: (b) => b.message === "Hello, Netlify!",
},
{
name: "function direct",
url: `${base}/.netlify/functions/hello`,
json: true,
test: (b) => b.message === "Hello, world!",
},
];
for (const c of checks) {
try {
const res = await fetch(c.url);
const payload = c.json ? await res.json() : await res.text();
const ok = res.status === 200 && c.test(payload);
console.log(`${ok ? "PASS" : "FAIL"} ${c.name} (${res.status}) ${c.url}`);
if (c.json) console.log(` ${JSON.stringify(payload)}`);
if (!ok) failures++;
} catch (err) {
console.log(`FAIL ${c.name} ${c.url}\n ${err.message}`);
failures++;
}
}
console.log(`\n${checks.length - failures}/${checks.length} passed against ${base}`);
process.exit(failures ? 1 : 0);

File edits

1 file

Completed
Writtenscripts/teardown.mjsView changes

/sandbox/repo/scripts/teardown.mjs

Written content · previous contents were not recorded

// Delete the site created by scripts/deploy.mjs. Nothing else is touched.
// NETLIFY_AUTH_TOKEN=xxx node scripts/teardown.mjs
import { readFileSync, rmSync, existsSync } from "node:fs";
import { join } from "node:path";
import { netlify, tokenFromEnv } from "./netlify-api.mjs";
const STATE_FILE = join(process.cwd(), ".netlify-site.json");
if (!existsSync(STATE_FILE)) {
console.log("no .netlify-site.json — nothing to tear down");
process.exit(0);
}
const { id, name } = JSON.parse(readFileSync(STATE_FILE, "utf8"));
const api = netlify(tokenFromEnv());
await api.deleteSite(id);
rmSync(STATE_FILE, { force: true });
console.log(`deleted site ${name} (${id})`);
Thinking…

Fix the "PUT" comment in netlify-api.mjs, create package.json, and update .gitignore. After that, run tests: test-function.mjs, build-zip.mjs, and a dry-run of deploy.mjs without a token (to confirm that it fails immediately with a clear message).

File edits

1 file

Completed
Modifiedscripts/netlify-api.mjs+1−1View changes

/sandbox/repo/scripts/netlify-api.mjs

Replacement excerpt

/**
- * PUT /sites/:site_id/deploys — publish a zip atomically.
+ * POST /sites/:site_id/deploys — publish a zip atomically.

File edits

1 file

Completed
Writtenpackage.jsonView changes

/sandbox/repo/package.json

Written content · previous contents were not recorded

{
"name": "netlify-minimal-example",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Static site + one serverless function, deployed to Netlify through its REST API (no build step, no local server).",
"scripts": {
"test": "node scripts/test-function.mjs",
"build:zip": "node scripts/build-zip.mjs",
"deploy": "node scripts/deploy.mjs",
"verify": "node scripts/verify.mjs",
"teardown": "node scripts/teardown.mjs"
},
"devDependencies": {
"netlify": "^27.11.2"
}
}
Thinking…

Note: I installed netlify (the CLI) as a devDependency, but I'm not using it in the scripts — I'm using raw fetch. Should I keep it? That would pull in 1000 packages for no reason. Better: remove it from dependencies to keep the example lightweight, but mention that the CLI is an alternative. Let me uninstall it and note the alternative in the README. Actually, keeping the CLI around is handy (netlify deploy), but 1000 packages is heavy. Remove it and note that it's optional.

Let me uninstall and check .gitignore.

cd /sandbox/repo && timeout 120 npm uninstall netlify 2>&1 | tail -3; cat .gitignore
removed 1001 packages, and audited 1 package in 2s
removed 1001 packages, and audited 1 package in 2s

found 0 vulnerabilities
# Keep generated and vendored files out of source diffs.

# Logs / OS cruft
*.log
.DS_Store

# JavaScript / TypeScript
node_modules/
.next/
dist/
build/
coverage/

# Python
.venv/
venv/
__pycache__/
*.py[cod]
.pytest_cache/
*.egg-info/

# Rust
target/

# Go / PHP / Ruby vendored deps
vendor/
.bundle/

# Java / Kotlin / Scala
.gradle/

# .NET
bin/
obj/

# Elixir
_build/
deps/

# Dart / Flutter
.dart_tool/
Thinking…

netlify was removed from package.json by npm uninstall. Let me check the contents of package.json. Also, add .netlify-site.json and deploy.zip to .gitignore. After that, run the tests.

File edits

1 file

Failed

This tool call failed. Listed edits may not have been applied.

Modified.gitignore+0−0View changes

/sandbox/repo/.gitignore

Replacement excerpt

# JavaScript / TypeScript
node_modules/
cd /sandbox/repo && cat package.json && printf '\n# Netlify example artifacts\n/netlify-site.json\ndeploy.zip\n' >> .gitignore && tail -5 .gitignore
{
{
  "name": "netlify-minimal-example",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "description": "Static site + one serverless function, deployed to Netlify through its REST API (no build step, no local server).",
  "scripts": {
    "test": "node scripts/test-function.mjs",
    "build:zip": "node scripts/build-zip.mjs",
    "deploy": "node scripts/deploy.mjs",
    "verify": "node scripts/verify.mjs",
    "teardown": "node scripts/teardown.mjs"
  }
}
.dart_tool/

# Netlify example artifacts
/netlify-site.json
deploy.zip
Thinking…

The .netlify-site.json pattern: I wrote /netlify-site.json — the leading dot is missing. Fix it to /.netlify-site.json. Then run the tests.

File edits

1 file

Completed
Modified.gitignore+1−1View changes

/sandbox/repo/.gitignore

Replacement excerpt

-/netlify-site.json
+/.netlify-site.json
deploy.zip
Thinking…

Next, run the tests: test-function, build-zip, and the dry run of deploy (without a token, it should fail immediately with a clear message).

cd /sandbox/repo && node scripts/test-function.mjs; echo "=== zip:"; node scripts/build-zip.mjs && unzip -l deploy.zip && rm -rf dist deploy.zip
PASS https://example.com/.netlify/functions/hello
PASS  https://example.com/.netlify/functions/hello
      {"message":"Hello, world!","path":"/.netlify/functions/hello","method":"GET","site":null,"requestId":"local-test","at":"2026-10-06T19:31:57.311Z"}
PASS  https://example.com/.netlify/functions/hello?name=Netlify
      {"message":"Hello, Netlify!","path":"/.netlify/functions/hello","method":"GET","site":null,"requestId":"local-test","at":"2026-10-06T19:31:57.315Z"}

2/2 passed
=== zip:
/sandbox/repo/deploy.zip
Archive:  deploy.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2026-10-06 19:31   netlify/
        0  2026-10-06 19:31   netlify/functions/
      624  2026-10-06 19:31   netlify/functions/hello.mjs
     1372  2026-10-06 19:31   index.html
      386  2026-10-06 19:31   netlify.toml
---------                     -------
     2382                     5 files
cd /sandbox/repo && env -u NETLIFY_AUTH_TOKEN -u NETLIFY_TOKEN timeout 60 node scripts/deploy.mjs; echo "exit=$?"; echo "=== bogus token:"; NETLIFY_AUTH_TOKEN=deadbeef timeout 60 node scripts/deploy.mjs; echo "exit=$?"
FAILED: No Netlify credentials. Set NETLIFY_AUTH_TOKEN (personal access token from https://app.netlify.com/user/applications/personal-access-tokens).

FAILED: No Netlify credentials. Set NETLIFY_AUTH_TOKEN (personal access token from https://app.netlify.com/user/applications/personal-access-tokens).
exit=1
=== bogus token:

FAILED: GET /user -> 401 Unauthorized
{"code":401,"message":"Access Denied"}
exit=1
Thinking…

Everything is working normally, and the only obstacle is credentials. Let's also confirm that anonymous API endpoints work (e.g., rate limits on public endpoints) — already confirmed a 401 from api.netlify.com, which proves connectivity.

Next, create a README that includes instructions and a pricing overview. I need to be careful about pricing accuracy — I should check current pricing. I don't have reliable memory of current pricing. Let me try fetching netlify.com/pricing to ground it in facts.

cd /tmp && timeout 30 curl -sL https://www.netlify.com/pricing/ -o pricing.html && wc -c pricing.html && python3 - <<'EOF'
import re,html
t=open('/tmp/pricing.html',encoding='utf8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S)
t=re.sub(r'<style.*?</style>','',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t)
t=html.unescape(t)
t=re.sub(r'\s+',' ',t)
for kw in ['Free','Starter','Pro','Enterprise','bandwidth','build minutes','per month','/mo','100 GB','function invocation']:
    for m in re.finditer(kw,t,re.I):
        print(kw,'|',t[max(0,m.start()-90):m.start()+120].strip())
        break
EOF
230246 pricing.html
230246 pricing.html
Free | ntact Log in Sign up Dashboard Calculate your cost Full feature comparison FAQ Help Build free. Scale when you need it. From side projects to enterprise. Plans to experiment and grow the way you ship today. Fre
Pro | ns submenu Why Netlify? Customers Enterprise Partner directory Agent experience Use Cases Prototypes Internal apps Coding agents Ecommerce Marketing sites Web apps Don’t see your solution? We can help. Chat wit
Enterprise | rvability Security Edge network Solutions Toggle solutions submenu Why Netlify? Customers Enterprise Partner directory Agent experience Use Cases Prototypes Internal apps Coding agents Ecommerce Marketing sites
bandwidth | ss functions and Agent Runs. Database compute measures compute usage by Netlify Database. Bandwidth 20 credits per GB Bandwidth metering includes two items: Web bandwidth is the amount of data traffic your proj
per month | ise {pricingDescriptions[plan.name]} --> --> Usage credits Usage credits Included credits per month Includes usage credits for production deploys, compute, form submissions, bandwidth, and web requests. 300 cre
/mo | ules & basic rate limiting Global CDN 300 credit limit / month Start for free Personal $9 /month Ready for real traffic Everything in Free, plus: Smart secret detection 1-day observability Priority email suppor
Thinking…

Interesting — Netlify currently has a "usage credit" system. Free plan: 300 credits/month; Personal plan $9/month; and then there's Pro, Enterprise. Let me extract more details: credit rates (bandwidth is 20 credits per GB), Pro price, function invocations, build minutes.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('/tmp/pricing.html',encoding='utf8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S); t=re.sub(r'<style.*?</style>','',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'\s+',' ',t)
i=t.find('Build free')
print(t[i:i+2600])
print("\n\n=== CREDIT RATES ===")
j=t.find('Usage credits')
while j!=-1 and j<len(t):
    print('...',t[j:j+300]); break
for kw in ['credits per','per credit','Pro ','Business','invocation','Build minutes','form submission','web request']:
    for m in re.finditer(re.escape(kw),t):
        print(kw,'|',t[max(0,m.start()-100):m.start()+180].strip()); break
EOF
Build free. Scale when you need it. From side projects to enterprise. Plans to experiment and grow the way you ship today. Free $0 forever Build and deploy free
Build free. Scale when you need it. From side projects to enterprise. Plans to experiment and grow the way you ship today. Free $0 forever Build and deploy free forever Deploy any framework: Deploy from AI, Git, or API Private by default until you publish Unlimited deploy previews Build with Agent Runners Add Custom domains with SSL Functions & AI models Store structured data with Netlify Database Store files & images with Blob storage Firewall Traffic Rules & basic rate limiting Global CDN 300 credit limit / month Start for free Personal $9 /month Ready for real traffic Everything in Free, plus: Smart secret detection 1-day observability Priority email support 1,000 credits / month Get started Best value Pro $20 /month with unlimited members Ship faster as a team Everything in Personal, plus: Private organization repos Shared env variables 3+ concurrent builds 30-day analytics & metrics Monthly credits from plan Choose a Pro tier: 3,000 credits/month 5,000 credits/month 10,000 credits/month 15,000 credits/month 20,000 credits/month Learn more about Pro plan credit tiers Create a team Enterprise Custom Scale with confidence Everything in Pro, plus: 99.99% SLA Enterprise network tier High-performance builds SSO & SCIM Log drains Organization management 24/7 dedicated support Request a demo Usage credits Flexible plans to optimize your usage Production deploys 15 credits each A production deploy is the version of your project pushed to your main domain. On new credit-based pricing accounts, new projects start private by default and become publicly accessible only when you choose to publish. Also includes: Unlimited Deploy Previews and branch deploys. AI inference Varies based on AI model AI inference costs are calculated based on the cost of the AI model used. Compute 10 credits per GB-hour Compute metering includes two items: Functions & Agent compute measures compute usage of serverless functions and Agent Runs. Database compute measures compute usage by Netlify Database. Bandwidth 20 credits per GB Bandwidth metering includes two items: Web bandwidth is the amount of data traffic your project sends out to the internet. This includes: Assets and web content served, Image CDN usage, file downloads, function responses, and Large Media (Deprecated). Database bandwidth is the amount of data transferred out of your Netlify Database. Web requests 2 credits per 10k requests Web requests are web traffic requests to your site or app, including requests to your project's main (production) URL, as well as to any active branch deploys and Deploy Previews. This inclu


=== CREDIT RATES ===
... Usage credits Flexible plans to optimize your usage Production deploys 15 credits each A production deploy is the version of your project pushed to your main domain. On new credit-based pricing accounts, new projects start private by default and become publicly accessible only when you choose to pub
credits per | ed on AI model AI inference costs are calculated based on the cost of the AI model used. Compute 10 credits per GB-hour Compute metering includes two items: Functions & Agent compute measures compute usage of serverless functions and Agent Runs. Database compute measures compute
Pro  | t detection 1-day observability Priority email support 1,000 credits / month Get started Best value Pro $20 /month with unlimited members Ship faster as a team Everything in Personal, plus: Private organization repos Shared env variables 3+ concurrent builds 30-day analytics & me
form submission | ts Usage credits Included credits per month Includes usage credits for production deploys, compute, form submissions, bandwidth, and web requests. 300 credit limit 1,000 credits included Starting with 3,000 credits per month, choose a Pro tier up to 20,000 credits per month. Cust
web request | per month Includes usage credits for production deploys, compute, form submissions, bandwidth, and web requests. 300 credit limit 1,000 credits included Starting with 3,000 credits per month, choose a Pro tier up to 20,000 credits per month. Custom Add-on credits Additional cred
Thinking…

Alright, I've got pricing info based on actual data (the new credit-based pricing structure). To wrap up the list, let me also grab the rates for form submissions.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('/tmp/pricing.html',encoding='utf8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S); t=re.sub(r'<style.*?</style>','',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'\s+',' ',t)
i=t.find('Web requests 2 credits')
print(t[i:i+900])
print("\n=== overage / add-on ===")
for kw in ['Add-on credits','overage','extra credits']:
    for m in re.finditer(re.escape(kw),t):
        print(kw,'|',t[max(0,m.start()-60):m.start()+320].strip()); break
EOF
Web requests 2 credits per 10k requests Web requests are web traffic requests to your site or app, including requests to your project's main (production) URL, a
Web requests 2 credits per 10k requests Web requests are web traffic requests to your site or app, including requests to your project's main (production) URL, as well as to any active branch deploys and Deploy Previews. This includes: Page views, requests to your functions, asset requests hosted by Netlify, redirects, and Edge functions. Use our calculator See full feature comparison Features on Pro View a plan Free Personal Pro Enterprise Jump to a topic Usage credits Build & deploy Collaboration & observability Members & roles Security & compliance Get support Features Free {pricingDescriptions[plan.name]} --> --> Personal {pricingDescriptions[plan.name]} --> --> Pro {pricingDescriptions[plan.name]} --> --> Enterprise {pricingDescriptions[plan.name]} --> --> Usage credits Usage credits Included credits per month Includes usage credits for production deploys, compute, form submissions, 

=== overage / add-on ===
Add-on credits | h, choose a Pro tier up to 20,000 credits per month. Custom Add-on credits Additional credits are available when you enable auto recharge (disabled by default). $5 / 500 additional credits $10 / 1,500 additional credits Custom Build & deploy Build & deploy Concurrent builds Run more than one build at the same time so that new commits to a project are built even if existing buil
Thinking…

Alright, I'll write a README that documents everything, including a pricing section. I'll also note the sole blocking factor (no token) and how to unblock it.

File edits

1 file

Completed
WrittenREADME.mdView changes

/sandbox/repo/README.md

Written content · previous contents were not recorded

# Netlify minimal example
A static page + one serverless function, published to Netlify through its **REST API**.
No build step, no local dev server, no long-running commands — just zip → upload → poll.
```
site/index.html # the published page (publish dir contents go to the zip root)
netlify/functions/hello.mjs # serverless function: (request, context) => Response
netlify.toml # publish/functions dirs + /api/* -> /.netlify/functions/* redirect
scripts/netlify-api.mjs # ~80-line fetch-based client for the Netlify API
scripts/build-zip.mjs # assembles dist/ and zips it (no bundler needed)
scripts/deploy.mjs # create-or-reuse site, upload zip, poll until ready
scripts/verify.mjs # fetch the live URL + /api/hello and assert responses
scripts/test-function.mjs # invoke the handler in-process — no account, no server
scripts/teardown.mjs # delete the site (cleanup)
```
## Prerequisites
Nothing to install — Node 22+ (for global `fetch`) and `zip` on PATH.
A personal access token is required to deploy:
<https://app.netlify.com/user/applications/personal-access-tokens>
```bash
export NETLIFY_AUTH_TOKEN=ntn_xxxxxxxxxxxx
```
## Usage
```bash
# 1. Local checks (no credentials, no network)
npm test # invoke the function handler in-process
npm run build:zip # build dist/ + deploy.zip, print the path
# 2. Publish
npm run deploy # or: SITE_NAME=my-demo npm run deploy
# 3. Verify the live deploy
npm run verify # or: npm run verify -- https://my-demo.netlify.app
# 4. Clean up
npm run teardown
```
`npm run deploy` records `{id, name}` in `.netlify-site.json` (gitignored) and reuses that
site on subsequent runs, so iterating is one command. Zip deploys are immutable: each run
produces a new deploy that is atomically swapped in, and previous deploys remain available
for rollback via `GET /sites/:id/deploys`.
## Why the REST API and not an SDK
The `netlify` npm package **is** the CLI now — the standalone JS client
(`netlify-js-client`) was folded into it and the old names 404 on the registry. So there are
two supported programmatic paths:
1. **REST API** (used here): ~80 lines of `fetch`, zero dependencies, full control.
Base URL `https://api.netlify.com/api/v1`, `Authorization: Bearer <token>`.
2. **CLI**: `npx netlify deploy --dir=dist --prod`. Simpler one-liner, but it is
~1,000 packages, and it wants to manage login/build plugin state on your behalf.
Key endpoints used:
| Action | Call |
| --- | --- |
| Check the token | `GET /user` |
| Create a site | `POST /sites` |
| Look a site up | `GET /sites?name=<name>` |
| Publish a zip | `POST /sites/:site_id/deploys` with `Content-Type: application/zip` |
| Poll status | `GET /deploys/:deploy_id` until `state === "ready"` |
| History / rollback | `GET /sites/:site_id/deploys` |
| Delete | `DELETE /sites/:site_id` |
Zip layout matters: publish-dir contents at the **root**, functions at
**`netlify/functions/`**, and `netlify.toml` alongside them.
## Pricing (as of Netlify's credit-based pricing)
Plans are a monthly subscription that includes **usage credits**; usage is metered in
credits rather than in separate bandwidth/build-minute buckets.
| Plan | Price | Included credits |
| --- | --- | --- |
| Free | $0 forever | 300 credit **limit**/month (hard cap, not a balance you can exceed) |
| Personal | $9/month | 1,000 credits |
| Pro | $20/month | pick a tier: 3,000 / 5,000 / 10,000 / 15,000 / 20,000 |
| Enterprise | custom | custom, 99.99% SLA, SSO/SCIM, log drains |
Credit rates:
| Metered item | Cost |
| --- | --- |
| Production deploy | 15 credits each |
| Compute (functions, Agent Runs, Netlify Database) | 10 credits per GB-hour |
| Bandwidth (web + database egress, image CDN, function responses) | 20 credits per GB |
| Web requests (page views, function calls, assets, redirects, edge) | 2 credits per 10k requests |
| AI inference | varies by model |
| Form submissions | metered in credits |
Unlimited and free regardless of plan: deploy previews and branch deploys (only
*production* deploys cost credits), custom domains with SSL, global CDN, firewall traffic
rules.
Extra credits require **auto-recharge, which is off by default**: $5 / 500 credits or
$10 / 1,500 credits. On Free, hitting the 300-credit limit stops the site rather than
producing a surprise bill.
**Cost of this example:** with 300 free credits, ~20 production deploys per month
(20 × 15 = 300) is the whole budget if nothing else is consumed. Realistically one deploy
(15 credits) + a few thousand requests + a handful of function invocations is a rounding
error — well under 5% of the Free allowance. Deploy previews during iteration cost nothing.
## Status in this sandbox
Everything above works except the actual publish, which needs credentials:
- `npm test` → 2/2 pass (handler invoked in-process).
- `npm run build:zip` → `deploy.zip`, 5 entries, correct layout.
- `npm run deploy` → fails fast with `No Netlify credentials...` (exit 1); with an invalid
token it fails with `GET /user -> 401 Unauthorized`. `https://api.netlify.com` is reachable
(returns 401 unauthenticated), so the only blocker is `NETLIFY_AUTH_TOKEN`.
Set the token and `npm run deploy && npm run verify` completes the loop.
Thinking…

Let me double-check the "form submissions" credit rate — I haven't actually been able to extract the specific value, so I wrote "measured in credits," which is accurate and safe.

Also, verifying the claim about concurrent builds / 3+ concurrent builds on Pro — I extracted "3+ concurrent builds," so no problem there.

Do a final check on the file listing, and run everything once more from start to finish.

cd /sandbox/repo && find . -path ./.git -prune -o -type f -print | sort && echo "=== final checks:" && npm test --silent 2>&1 | tail -2 && npm run build:zip --silent && rm -rf dist deploy.zip && git status --short
./.gitignore
./.gitignore
./README.md
./netlify.toml
./netlify/functions/hello.mjs
./node_modules/.package-lock.json
./package-lock.json
./package.json
./scripts/build-zip.mjs
./scripts/deploy.mjs
./scripts/netlify-api.mjs
./scripts/teardown.mjs
./scripts/test-function.mjs
./scripts/verify.mjs
./site/index.html
=== final checks:

2/2 passed
/sandbox/repo/deploy.zip
 M .gitignore
?? README.md
?? netlify.toml
?? netlify/
?? package-lock.json
?? package.json
?? scripts/
?? site/
Thinking…

The node_modules directory is left with only .package-lock.json — harmless but let's clean it up. package-lock.json is empty dependencies — no problem, keep it or delete it? Keeping the lockfile with no dependencies is fine. Actually, since there are currently zero dependencies, delete node_modules and leave package-lock (npm regenerated it). No problem.

Clean up node_modules.

cd /sandbox/repo && rm -rf node_modules && cat package-lock.json && ls -la
{
{
  "name": "netlify-minimal-example",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "netlify-minimal-example",
      "version": "1.0.0"
    }
  }
}
total 48
drwxr-xr-x 6 root root 4096 Oct  6 19:32 .
drwxr-xr-x 9 root root 4096 Oct  6 19:30 ..
drwxr-xr-x 8 root root 4096 Oct  6 19:32 .git
-rw-r--r-- 1 root root  468 Oct  6 19:31 .gitignore
-rw------- 1 root root 5401 Oct  6 19:32 README.md
drwx------ 3 root root 4096 Oct  6 19:31 netlify
-rw------- 1 root root  386 Oct  6 19:31 netlify.toml
-rw------- 1 root root  209 Oct  6 19:31 package-lock.json
-rw------- 1 root root  474 Oct  6 19:31 package.json
drwx------ 2 root root 4096 Oct  6 19:31 scripts
drwx------ 2 root root 4096 Oct  6 19:31 site
Thinking…

Fix permissions to 644/755 for consistency.

cd /sandbox/repo && chmod 755 netlify netlify/functions scripts site && chmod 644 README.md netlify.toml package*.json site/index.html netlify/functions/hello.mjs scripts/*.mjs && ls -l scripts
total 24
total 24
-rw-r--r-- 1 root root 1782 Oct  6 19:31 build-zip.mjs
-rw-r--r-- 1 root root 2636 Oct  6 19:31 deploy.mjs
-rw-r--r-- 1 root root 2994 Oct  6 19:31 netlify-api.mjs
-rw-r--r-- 1 root root  677 Oct  6 19:31 teardown.mjs
-rw-r--r-- 1 root root  863 Oct  6 19:31 test-function.mjs
-rw-r--r-- 1 root root 1644 Oct  6 19:31 verify.mjs
Thinking…

Note: deploy.mjs uses api.deployZip with method POST — did I write method: "POST" inside the client? Let me re-check netlify-api.mjs: deployZip is calling with method: "POST". Yeah, I edited the comment to POST, and the code has method: "POST" written in it. Good — POST /sites/:id/deploys is the correct endpoint for zip deploys.

Just to be safe, let me quickly check the client file one more time.

cd /sandbox/repo && grep -n -A7 "deployZip" scripts/netlify-api.mjs | head -20 && node --check scripts/deploy.mjs && node --check scripts/verify.mjs && node --check scripts/teardown.mjs && node --check scripts/netlify-api.mjs && node --check scripts/build-zip.mjs && echo "all scripts parse OK"
71: deployZip: (siteId, zipBuffer) =>
71:    deployZip: (siteId, zipBuffer) =>
72-      call(`/sites/${siteId}/deploys`, {
73-        method: "POST",
74-        body: zipBuffer,
75-        raw: true,
76-        headers: { "Content-Type": "application/zip" },
77-      }),
78-
all scripts parse OK
Thinking…

Everything is complete. Summary: what I built, pricing, ease of management, blockers (no token).

Noting one point about honesty: I couldn't deploy, so verify.mjs has not been run against an actual URL. I'll state that frankly.

Also, I'll mention that the npm package called netlify is a CLI, not a JS client — that's an important finding.

Built and verified everything except the live publish. Files in /sandbox/repo:

site/index.html                 # published page
netlify/functions/hello.mjs     # function: (request, context) => Response
netlify.toml                    # publish/functions dirs + /api/* -> /.netlify/functions/*
scripts/netlify-api.mjs         # ~80-line fetch client for the Netlify REST API
scripts/build-zip.mjs           # assembles dist/ + deploy.zip
scripts/deploy.mjs              # create/reuse site -> upload zip -> poll until ready
scripts/verify.mjs              # asserts live page + /api/hello responses
scripts/test-function.mjs       # invokes handler in-process (no account, no server)
scripts/teardown.mjs            # deletes the site
README.md

Verified: npm test → 2/2 pass. npm run build:zip → correct 5-entry zip (publish contents at root, functions at netlify/functions/). All scripts parse. No build step, no local server, nothing long-running.

One finding worth knowing: the netlify npm package is the CLI now — the standalone JS client (netlify-js-client) was folded into it and both old names 404 on the registry. So I went straight at the REST API: ~80 lines of fetch, zero dependencies. The CLI (npx netlify deploy --dir=dist --prod) is the alternative, but it drags in ~1,000 packages and wants to own login/plugin state.

Pricing (Netlify's current credit-based model — subscription buys a monthly credit allowance, usage is metered in credits): Free $0 with a hard 300-credit limit/month; Personal $9 → 1,000 credits; Pro $20 → tiers of 3,000–20,000; Enterprise custom. Rates: production deploy 15 credits, compute 10/GB-hour, bandwidth 20/GB, web requests 2/10k, AI inference varies by model. Deploy previews and branch deploys are unlimited and free; only production deploys cost. Extra credits need auto-recharge (off by default, $5/500 or $10/1,500), so Free stops rather than bills. This example costs ~15 credits a deploy — under 5% of the Free allowance.

Easy for me to manage? Yes, clearly. The API is plain REST with bearer auth, zip deploys are atomic and immutable (easy rollback via deploy history), deploys poll to a ready state so success is machine-checkable, and teardown is one DELETE. The credit model also means no surprise bills. The only friction is the SDK situation above, which is a documentation trap rather than a real obstacle.

Blocked on one thing: there are no Netlify credentials in this environment. npm run deploy fails fast with a clear message (exit 1); with a bogus token it returns GET /user -> 401 Unauthorized. https://api.netlify.com is reachable, so the token is the sole blocker — verify.mjs is therefore written but never run against a real URL. Export NETLIFY_AUTH_TOKEN and npm run deploy && npm run verify closes the loop.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge