# 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.

Canonical: https://usetruenote.com/docs/first-script

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.

```sh
export TRUENOTE_TOKEN=tn_pat_…
curl https://app.usetruenote.com/v1/overview -H "Authorization: Bearer $TRUENOTE_TOKEN"
```

## 2. Read

```js
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.

```js
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](/docs/api#writes) lists each one's fields.

```js
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](/docs/errors).

```json
{ "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](/docs/api): every route and body, from the spec at [/v1/openapi.json](https://app.usetruenote.com/v1/openapi.json).
- [Auth, scopes and exclusions](/docs/auth-and-scopes): OAuth for an MCP client, what each scope allows.
- [Export](/docs/export): everything at once, as JSON or a zip of CSVs.
