Your first script
From a personal access token to a script that reads your money, keeps a copy in step with updatedSince, writes a note back, and handles an error it can fix.
Twenty lines of Node, no library. Everything here works the same from Python, a spreadsheet’s script editor or curl.
1. A token
In Truenote, Connected agents → New access token. Name it, allow accounts:read, transactions:read and context:write (writing a note needs it), pick how long it works and which accounts it must never see. Copy it: it’s shown once.
export TRUENOTE_TOKEN=tn_pat_…
curl https://app.usetruenote.com/v1/overview -H "Authorization: Bearer $TRUENOTE_TOKEN"
2. Read
const BASE = "https://app.usetruenote.com/v1";
const headers = { authorization: `Bearer ${process.env.TRUENOTE_TOKEN}` };
async function call(path, init = {}) {
const res = await fetch(BASE + path, { ...init, headers: { ...headers, ...init.headers } });
const body = await res.json();
if (!res.ok) throw new Error(`${body.error}: ${body.detail}\n${body.fix ?? ""}`);
return body;
}
const overview = await call("/overview");
console.log("Net worth:", overview.netWorth.netWorthCents / 100);
// Every page: 50 rows and a cursor for the next one.
let cursor = "";
for (;;) {
const page = await call(`/transactions?from=2026-10-01&limit=200${cursor && `&cursor=${cursor}`}`);
for (const t of page.transactions) console.log(t.date, t.description, t.amountCents / 100);
if (!page.nextCursor) break;
cursor = page.nextCursor;
}
Money is integer cents; a transaction’s amountCents is positive for money in. Days are YYYY-MM-DD.
3. Keep a copy in step
Save the syncedAt each answer carries and pass it back as updatedSince: you get the rows added or changed since, and deleted lists the ids gone (a pending charge that posted, a bank the person disconnected). Apply them by id; a row may come twice, which is harmless.
let since = loadSavedSyncedAt(); // undefined the first time
let next;
let cursor = "";
for (;;) {
const q = new URLSearchParams({ limit: "200" });
if (since) q.set("updatedSince", since);
if (cursor) q.set("cursor", cursor);
const page = await call(`/transactions?${q}`);
next ??= page.syncedAt;
for (const t of page.transactions) upsertRow(t);
for (const id of page.deleted ?? []) deleteRow(id);
if (!page.nextCursor) break;
cursor = page.nextCursor;
}
saveSyncedAt(next);
/investment-transactions works the same way. Accounts, the plan, goals and the rest are small: read them whole each time.
4. Write
Every MCP write tool is POST /v1/tools/<name> with the tool’s own body, amounts in dollars. The API reference lists each one’s fields.
await call("/tools/add_note", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ body: "Lease ends 2027-06-30", tags: ["lease"] }),
});
Writes change only Truenote’s own records (the plan, buckets, goals, notes, categories…), never a bank. Each one is in Connected agents → Activity, with the token that made it.
5. When it fails
Errors are application/problem+json: error (a stable code), detail (what went wrong), fix (what to do), field (the parameter to correct) and, for a 403, scope (what the token lacks). The call above prints the fix. The codes are on Errors.
{ "status": 403, "error": "insufficient_scope", "scope": ["context:write"],
"fix": "Ask the person to allow context:write for this connection under Connected agents in Truenote …" }
Next
- API reference: every route and body, from the spec at /v1/openapi.json.
- Auth, scopes and exclusions: OAuth for an MCP client, what each scope allows.
- Export: everything at once, as JSON or a zip of CSVs.