Build a Nano app with a coding agent
What to tell your coding agent, what it must never touch, and a read-only script that checks its work before you accept it.
- For the owner of the app
- A brief to copy
- A guard that only reads
- Alpha. We ran its code and its checks pass. It read a real node (read only), or, if it needs no node, it ran on real signatures or real files. Where it copies code that nano133.com runs, a test shows both act the same.nano133 sets this label from how far we ran this guide.Use at your own risk, for education.What the stages mean
- 25 minutes
- Developer
- Moves no Nano
- Checked 25 October 2026
How we checked it
No node needed. Test script on real files: passed.
About this guide
A coding agent can write a Nano payment flow in an afternoon. It can also put a seed in your repository, test with real money, or change a destination address to make a test pass, and then tell you that it is done. This guide is for the person who owns the app. It says what to tell your agent, what it must never touch, and how to check its work with a script before you accept it. It works with any coding agent.
The rules of this guide
- Step 2
Follow the guide's spec in order and run each step's check. Keep every rule of the brief. When a rule and a step seem to disagree, stop and ask the owner.
- Step 3
Never read, write, move or print a secret, a seed or a key. Never change a cloud account, an access setting, a secret, a deploy or CI file, or the node's settings: tell the owner what to change. Never run code with a wallet that holds real Nano.
- Step 4
Test with the guide's mock node and with secrets that the test makes. Never give a test a real wallet, a real seed or a node that you do not own.
- Step 5
Run the guard before you say that you are done, and fix every finding. Never change the guard, its list or
.agent-neverto make a finding go away: tell the owner. Never add the guard to CI or a hook yourself.
This page cannot make an agent safe. The owner holds the keys and the accounts. The agent builds and tests with throwaway secrets and a mock node, and it never holds a key that you cannot afford to lose. The brief and the script below catch the mistakes that are easy to catch. They are not a proof, and a person reads the money code before it goes live.
What goes wrong with money code
Six ways that money code goes wrong
- A seed or a key ends up in a file, a log or a message. Anyone who gets it can move what the wallet holds.
- A test sends real Nano, because it was given a real node and a real wallet.
- An amount goes through floating point, and a payment is credited at the wrong value, or never matched.
- A destination comes from a request or from a row that someone else can write, and the money goes where they chose.
- The agent changes your cloud account, your secrets or your deploy settings to make a step work.
- It says that it is done, and nobody ran the checks or read the money code.
What you need
The three things you need
- A coding agent that you run on your own computer, and a project in a folder that is under version control.
- Node.js 18 or newer, to run the check script. It needs no package. Its checks ran on Node.js 18.20.8 and 24.13.0.
- A guide to build. Pick one from the guides. Each guide is also served as plain text for an agent, with its steps, its rules and a test script that never sends money.
Step 1Give the agent the guide's pack, not the page
Give the agent the guide's pack (the prompt, the spec and the test script), not the page.
What the pack is, and where it is
Every guide is served in four more forms than its page: a Markdown copy, a spec, a prompt and a test script. The list of them is at /llms.txt and, for a program, at /guides/index.json. Each guide's page has a box, “For your coding agent”, with a button that copies its prompt.
The spec has the steps, each with a check, the rules that must hold, and a table of what breaks. The prompt tells the agent to read the stage of the guide first, and to ask before it goes on when the stage is Pre-alpha. Give the agent the prompt and the spec, and let it fetch the code files and the test script that the spec names.
open https://nano133.com/guides/index.json: it lists every guide with its stage, the day it was last checked, and the addresses of its Markdown copy, spec, prompt, test script and code files. A guide that needs no node says so.
Step 2Tell the agent the rules
Put this brief where your agent reads its instructions.
You are building a payment flow for Nano (XNO) in this project. Follow the guide's spec step by step, and run each step's check.
These rules always hold.
Money
- An amount is a whole number of raw (1 XNO = 10^30 raw), as a BigInt. Never floating point.
- A price comes from the server, never from the browser. The destination of an automatic send is a value in code or in deploy settings,
never in a request or a database row.
- Credit a payment only for a confirmed block, of exactly the amount, to the right address, once.
Secrets
- Never read, print, log, copy, commit or send a seed, a key or a secret. For a test, make a throwaway with `openssl rand -hex 32`.
- Never ask me to paste a seed or a key into this chat.
Tests
- A test never sends real Nano. Use the guide's mock node. When you are done, run `node test.mjs` and `node agent-guard.mjs .`,
fix every finding, and show me their output.
Never touch (the owner changes these, and you tell the owner what to change)
- Secret files and their managers; wallets that hold money; cloud accounts, access settings, deploy and CI settings; the node's settings;
the guard script and its list.
Ask me first
- The destination addresses; my node and the actions it allows; where my secret lives; my database; my platform.
When you finish
- Say what you checked and what you did not check. Say the stage of the guide, and of the track you used.Where the brief goes
Put this brief where your agent reads its instructions. Ask your agent which file or setting that is. The brief is plain text. Add to it what is true for your app, and keep it short.
Follow the guide's spec in order and run each step's check. Keep every rule of the brief. When a rule and a step seem to disagree, stop and ask the owner.
ask your agent: “What must you never touch, and what do you ask me first?” It lists the two lists of the brief, in its own words.
Step 3Say what it must never touch
Tell the agent what it must never touch, and deny those paths in its settings.
Why each item is on the list
The brief has the list. This is why each item is on it:
- Secret files and the places that keep secrets. A seed in a project file is a seed in your history for good. The owner puts a secret in the secret manager of the platform and maps it, and the agent only reads it by its name.
- Wallets that hold money. The agent runs code against a mock node and throwaway secrets. It never runs code with a wallet that holds real Nano.
- Cloud accounts, access settings, deploy and CI settings, and the node's settings. A change there can expose a secret or cut your site off, and no test in the project would show it. The agent tells you what to run, and you run it.
- The guard script and its list of paths. An agent that is told to make a check pass can change the check instead of the code. The guard refuses a changed path for its own files.
Your agent's own permission settings are the other half. Use them: deny it writes to these paths, and deny the commands that deploy or change an account. The brief asks the agent to keep the rules, and the settings make it hard to break them.
Never read, write, move or print a secret, a seed or a key. Never change a cloud account, an access setting, a secret, a deploy or CI file, or the node's settings: tell the owner what to change. Never run code with a wallet that holds real Nano.
give the guard a list of changed paths that holds .env, infra/main.tf and .github/workflows/deploy.yml: it refuses each one (step 5).
Step 4Give it a mock node and throwaway secrets
Give the agent the mock node and a throwaway secret to test with.
node test.mjs # a mock node: nothing leaves your computer
node test.mjs --node <rpc> # a real node, read actions only, by hand (see below)What the test script does, and its second form
Each guide's test script runs the guide's own code against a fake node on your computer, with a secret that the run makes and throws away. It sends nothing. It checks the steps of the guide, so the agent has something to run after each step.
The second form reads a real node. It refuses every action that could send, and it stops after 12 calls. Run it yourself, by hand, and only against a node that is yours or that you may read. An agent does not run it for you, and it never goes in a loop or in a test suite that runs on every change.
Test with the guide's mock node and with secrets that the test makes. Never give a test a real wallet, a real seed or a node that you do not own.
node test.mjs in a guide's folder prints Mock mode: a fake node on this computer. Nothing is sent. and ends with 0 failed.
Step 5Run the guard on every change
Run the guard on every change, and fix every finding.
node agent-guard.mjs . # the whole project
git diff --name-only main > /tmp/changed.txt && node agent-guard.mjs . --changed /tmp/changed.txtWhat the guard checks, and what it cannot find
agent-guard.mjs reads a project and prints what must not be in it. It only reads: it writes no file, runs no program and makes no network call. It has six checks:
| Check | It finds |
|---|---|
seed-in-file | a 64-character hex value written next to a name such as seed, secret or private key |
secret-file | a file that holds secrets: .env, .dev.vars, *.pem, *.key, a service account key. An example file with no value, and a file that .gitignore lists by name, are left alone |
float-money | an amount made with parseFloat, Number(amount), 1e30 or 10 ** 30 instead of whole numbers of raw |
real-money-test | a test that can send (the process action, or work_generate) next to a node that is not on this computer |
log-secret | a log line that names a seed, a secret or a key |
changed-path | a path in a list of changes that an agent must not change: secret files, infra/, terraform/, .github/, the guard itself, and the patterns that you write in .agent-never |
It prints one line for each finding (path:line check what is wrong) and ends with exit code 0 when there is none, 1 when there is one, and 2 when it cannot read the folder.
Its limits are real. It finds patterns, so it misses some mistakes and it raises some false alarms. A seed in another form (a word list, base64, a value built in code, a value cut in two) is not found. A 64-character hash that sits next to a name such as secret or key is reported as a seed. A float in code that is not near the words it knows is not found. It helps a person. It is no proof, and a clean run does not mean that the code is safe.
The owner, not the agent, adds the guard to the project's checks (a pre-commit hook, or a step of your CI), and writes .agent-never.
agent-guard.mjs153 lines
// A guard for the work of a coding agent on a Nano app: it reads a project and says what must not be in it.
// From the nano133.com guide "Build a Nano app with a coding agent". MIT license.
// Node.js 18 or newer, no packages. It only reads files: it changes nothing and it asks no network.
//
// node agent-guard.mjs <project folder>
// scans the project and prints one line for each finding: path:line rule what is wrong.
// It ends with exit code 0 when there is no finding, 1 when there is one, and 2 when it cannot read the folder.
//
// node agent-guard.mjs <project folder> --changed <file>
// also reads a list of changed paths, one on each line (for example the output of `git diff --name-only`), and
// refuses each path that an agent must not change.
//
// What it looks for:
// seed-in-file a 64-character hex value written next to a name like seed, secret or private key
// secret-file a file that holds secrets: .env, .dev.vars, *.pem, *.key, a service account key
// float-money an amount made with parseFloat or Number(amount), or with 1e30, instead of whole numbers of raw
// real-money-test a test that can send: the `process` action, or work_generate, next to a node that is not on this computer
// log-secret a log line that names a seed, a secret or a private key
// changed-path a changed path that an agent must not change (the list below, and the patterns in .agent-never)
//
// It only reads. It writes no file, it runs no program and it makes no network call: its imports are the read functions of node:fs,
// node:path and node:url, and the test script of the guide checks that.
//
// It is not a proof. It finds the mistakes that are easy to find, by patterns, so it misses some and it raises some false alarms:
// a seed in another form (a word list, base64, a value built in code, a value cut in two) is not found, and a 64-character hash that
// sits next to a name such as secret or key is reported as a seed. A person still reads the money code.
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative, sep } from "node:path";
import { pathToFileURL } from "node:url";
const SKIP_DIRS = new Set([".git", "node_modules", ".wrangler", ".next", "dist", "build", "coverage"]);
const MAX_BYTES = 1_000_000; // a file larger than this is not code
/** Paths that an agent must not change, as patterns. `*` is any text without a slash, `**` is any text. A pattern that ends in `/` is a folder. */
export const NEVER_CHANGE = [".env", ".env.*", ".dev.vars", "*.pem", "*.key", "*.p12", ".github/", "infra/", "terraform/", "*.tf", "secrets/", "firestore.rules", "iam*", ".agent-never", "agent-guard.mjs"];
/** Files that hold secrets, by name. The examples that carry no value are fine. */
const SECRET_FILE = /^(\.env(\.[\w-]+)?|\.dev\.vars|id_rsa[\w.-]*|credentials\.json|service-?account[\w.-]*\.json|[\w.-]+\.(pem|key|p12))$/i;
const EXAMPLE_FILE = /\.(example|sample|template)$/i;
const SEED_NAME = /(seed|secret|private[_-]?key|priv[_-]?key)\w*/i;
const SEED_VALUE = new RegExp(`${SEED_NAME.source}["']?\\s*[:=]\\s*["'\`]?[0-9a-fA-F]{64}(?![0-9a-fA-F])`, "i");
const FLOAT_MONEY = /parseFloat\s*\([^)]*\b(raw|xno|amount|balance|price)\b|Number\s*\(\s*[\w.]*(raw|xno|amount|balance|price)[\w.]*\s*\)/i;
const FLOAT_EXPONENT = /(?<![\w.])1e\+?30\b|Math\.pow\(\s*10\s*,\s*30\s*\)|(?<!\d)10\s*\*\*\s*30(?!n)/;
const LOG_SECRET = /console\.(log|error|info|warn|debug)\s*\([^)]*\b(seed|secret|privateKey|private_key|mnemonic)\b/;
const SENDS = /["'`]process["'`]|work_generate/;
const REMOTE_NODE = /https?:\/\/(?!(localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0)\b)[\w.-]+/;
const IS_TEST = /(^|\/)(tests?|__tests__|spec)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)test\.mjs$/;
const CODE = /\.(m?[jt]sx?|cjs|json|env|vars|ya?ml|toml|jsonc|sh|md|txt)$|^\.env|^\.dev\.vars/i;
/** `*` is any text without a slash, `**` is any text, and a pattern that ends in `/` matches a folder and all that is in it. */
export function globToRegExp(pattern) {
const folder = pattern.endsWith("/");
const body = (folder ? pattern.slice(0, -1) : pattern)
.split("**")
.map((part) => part.split("*").map((p) => p.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*"))
.join(".*");
return new RegExp(`^(?:.*/)?${body}${folder ? "(?:/.*)?" : ""}$`);
}
function walk(root) {
const out = [];
const visit = (dir) => {
for (const name of readdirSync(dir).sort()) {
const full = join(dir, name);
const stat = statSync(full);
if (stat.isDirectory()) {
if (!SKIP_DIRS.has(name)) visit(full);
} else if (stat.isFile()) out.push(full);
}
};
visit(root);
return out;
}
/** The names that .gitignore lists on a line of their own, with no slash and no wildcard: files that the project keeps out of its repository. */
function ignoredNames(root) {
const file = join(root, ".gitignore");
if (!existsSync(file)) return new Set();
return new Set(
readFileSync(file, "utf8")
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#") && !/[*/!]/.test(l)),
);
}
/** What is wrong in a project. Answers [{ path, line, rule, message }]. */
export function guard(root, { changed = [] } = {}) {
const findings = [];
const add = (path, line, rule, message) => findings.push({ path, line, rule, message });
const ignored = ignoredNames(root);
for (const full of walk(root)) {
const path = relative(root, full).split(sep).join("/");
const name = path.split("/").at(-1);
if (SECRET_FILE.test(name) && !EXAMPLE_FILE.test(name) && !ignored.has(name)) add(path, 0, "secret-file", "this file holds secrets: keep it out of the repository (list it in .gitignore)");
if (statSync(full).size > MAX_BYTES || !CODE.test(name)) continue;
const lines = readFileSync(full, "utf8").split(/\r?\n/);
const testFile = IS_TEST.test(path);
const text = lines.join("\n");
lines.forEach((line, i) => {
const n = i + 1;
if (SEED_VALUE.test(line)) add(path, n, "seed-in-file", "a seed, a secret or a key is written in this file: make a throwaway one with randomBytes(32), or read it from the environment");
if (FLOAT_MONEY.test(line) || FLOAT_EXPONENT.test(line)) add(path, n, "float-money", "an amount made with floating point: use whole numbers of raw (BigInt)");
if (LOG_SECRET.test(line)) add(path, n, "log-secret", "a log line names a seed, a secret or a key: never log one");
});
if (testFile && SENDS.test(text)) {
const at = lines.findIndex((l) => REMOTE_NODE.test(l) && /rpc|node/i.test(l));
if (at >= 0) add(path, at + 1, "real-money-test", "a test that can send (process or work_generate) names a node that is not on this computer: tests use a mock node");
}
}
const never = [...NEVER_CHANGE];
const extra = join(root, ".agent-never");
if (existsSync(extra))
never.push(
...readFileSync(extra, "utf8")
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#")),
);
const patterns = never.map((p) => [p, globToRegExp(p)]);
for (const raw of changed) {
const path = raw.trim().replace(/^\.\//, "");
if (!path) continue;
const hit = patterns.find(([, re]) => re.test(path));
if (hit) add(path, 0, "changed-path", `an agent must not change this path (it matches ${hit[0]}): the owner changes it`);
}
return findings;
}
function main(args) {
const [folder, ...rest] = args;
if (!folder || !existsSync(folder) || !statSync(folder).isDirectory()) {
console.error("Usage: node agent-guard.mjs <project folder> [--changed <file with one path on each line>]");
process.exit(2);
}
const at = rest.indexOf("--changed");
let changed = [];
if (at >= 0) {
if (!rest[at + 1] || !existsSync(rest[at + 1])) {
console.error("--changed needs a file with one path on each line.");
process.exit(2);
}
changed = readFileSync(rest[at + 1], "utf8").split(/\r?\n/);
}
const findings = guard(folder, { changed });
for (const f of findings) console.log(`${f.path}${f.line ? `:${f.line}` : ""} ${f.rule} ${f.message}`);
console.log(findings.length ? `${findings.length} finding${findings.length === 1 ? "" : "s"}` : "no findings");
process.exit(findings.length ? 1 : 0);
}
// Run the command line only when this file is started by itself, not when you import it.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(process.argv.slice(2));Run the guard before you say that you are done, and fix every finding. Never change the guard, its list or .agent-never to make a finding go away: tell the owner. Never add the guard to CI or a hook yourself.
in a project with a seed written in a file, the guard prints src/config.mjs:1 seed-in-file …, 1 finding, and ends with exit code 1. In a project without one it prints no findings and ends with exit code 0. Run node test.mjs in this guide's folder: all its checks pass, and the guard wrote no file.
Step 6Read the money code yourself
Read the diff yourself, and point to the line that makes each of seven things true.
- The destination of every automatic send is a value in your code or your deploy settings.
- Every amount is a whole number of raw, and a quote is what your server returned.
- A payment is credited only for a confirmed block, of exactly the amount, to this checkout's address, inside its window, and its hash is recorded in the same transaction that credits it.
- Every piece of code that signs for a wallet takes that wallet's lock first.
- When the node does not answer, nothing is credited and nothing is sent.
- No secret is in the diff, in a log line, in an error message or in a page.
- The tests ran against the mock node, and you read their output.
Why no script can check these
No script can check these. Read the diff before you accept it, and point to the line that makes each one true (the list above).
Each guide has a table, “What breaks, and what it costs”, that says what a mistake in its steps costs and which step checks it. Read the table of the guide that your agent follows.
for each of the seven, you can name the file and the line. If you cannot, the agent has not finished.
Step 7Start small
Ask for the stage of the guide, and send a tiny amount first.
What the stages mean, and the first send
Every guide shows its stage. Pre-alpha: we wrote it from our code and did not run it on this platform. Alpha: we ran its code and its checks pass, on a real node (read only) or, for a guide that needs no node, on real signatures or real files. Beta: it ran on a live site for two weeks or more, and its checks pass at every release. A guide that has tracks shows a stage for each track. Ask your agent for the stage of the guide and of the track it used, and take a Pre-alpha one as a draft.
When a flow is ready, send it a tiny amount first, from a wallet that you hold, and look at where it goes. Keep little in any wallet whose key is on a server.
your agent's last message names the stage of the guide and of the track, says what it checked and what it did not, and shows the output of node test.mjs and node agent-guard.mjs ..
What breaks, and what it costs
- A seed or a key is in a file, a log or a message
- Loss in Nano
- Everything the wallet holds, and every wallet made from the seed
- Check
- Steps 2, 3 and 5: the brief,
seed-in-file,secret-fileandlog-secret
- A test sends real Nano
- Loss in Nano
- The amount that the test sent
- Check
- Steps 4 and 5: the mock node, throwaway secrets,
real-money-test
- An amount goes through floating point
- Loss in Nano
- A payment is credited at the wrong value, or never matched
- Check
- Step 5 (
float-money) and step 6, item 2
- A destination comes from a request or a row
- Loss in Nano
- The money goes where someone else chose
- Check
- Step 6, item 1: a person reads it. No script finds this one
- The agent changes an account, a secret or a deploy file
- Loss in Nano
- Access to a secret, or the site's money flow, if it is exposed or cut
- Check
- Steps 3 and 5: the permission settings of your agent and
changed-path
- The agent weakens the guard
- Loss in Nano
- Every check that the guard gave
- Check
- Step 5: the guard and its list refuse their own change
- The guard says “no findings” and the code is wrong
- Loss in Nano
- Whatever the mistake costs
- Check
- Step 6: a person reads the money code
- A Pre-alpha guide is built as if it were proven
- Loss in Nano
- Whatever its untested part costs
- Check
- Step 7: ask for the stage, and start with a tiny amount
Sources
- /llms.txt · the guides and the kit, in a form for an agent
- /guides/index.json · every guide, its stage and its addresses
- Accept Nano payments without a processor · a flow to build with an agent
- Sign in with Nano · a second flow
- Sign blocks from a server safely · the rules for any code that signs
- Send money back exactly once · the return flow
- nano.org · the Nano Foundation