Skip to content
Truenote

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