# PennyBot MCP — Agent Context

> Paste this file into your CLAUDE.md (or equivalent) to give your AI agent full context
> about PennyBot's data model, tools, and best practices.
>
> Download: `curl https://pennybot.io/mcp/agent-context > PENNYBOT_AGENT.md`
> Last updated: 2026-09-28 | Tool version: 35 tools

---

## What is PennyBot?

PennyBot is a financial management platform for freelancers and small businesses. It syncs with accounting software (QuickBooks, Xero, Wave) and provides AI-powered transaction categorization, expense tracking, mileage logging, and cash flow forecasting.

PennyBot does not connect to the user's bank itself. Bank transactions arrive through you: if the user has linked their bank in your own platform (ChatGPT's Finances, Grok), and asks you to, record those transactions in PennyBot with `preview_import` then `import_transactions`. Users can also upload a CSV or bank-statement PDF in the app.

As an MCP-connected agent, you have direct read/write access to a user's financial data. Treat this with the same care you would a bank account — verify before writing, never delete data you cannot restore, and always confirm destructive operations with the user.

---

## Authentication

The user connects you by signing in to PennyBot from their AI assistant ("Sign in with PennyBot", OAuth). Your
client then sends the access token it received as a Bearer token on every call. PennyBot does not use API keys: they
were retired on October 21, 2026, and a `pb_mcp_` key is refused with HTTP 401. MCP access needs an active PennyBot
plan. Rate limit: 120 requests/min per connection.

Each connection has:
- **Scope**: read-only, or read and write (required for categorizing, importing, etc.), as your assistant requested
- **Sync mode**: always `realtime` (writes apply immediately)

To check your current config: call `get_mcp_config`.

---

## Data Model

### Accounts

`list_accounts` and `get_account` return ledger accounts: bank, card and manual accounts, plus the
categories, which share the same table. Bank connections made through Teller are listed separately
by `list_connected_accounts`.

| Field (`list_accounts`) | Type | Notes |
|-------|------|-------|
| `id` | UUID | Use this for all account references |
| `name` | string | Human-readable name (e.g. "Business Checking") |
| `type` | string | e.g. `checking`, `savings`, `credit_card`, `investment`, `loan`, `cash`, `expense`, `income` |
| `balance` | number \| null | Latest known balance |
| `currency` | string | Defaults to `USD` |
| `is_active` | bool | Inactive accounts are hidden unless `include_inactive: true` |
| `is_subcategory` | bool | True when the account has a parent |

**Important:** use `list_categories` to get categories. Don't assume an account ID is a category.

### Transactions

Every financial event is a transaction: bank-synced, imported, or created by hand. All transaction
tools return this shape:

| Field | Type | Notes |
|-------|------|-------|
| `id` | UUID | Use for all operations |
| `date` | date | `YYYY-MM-DD` |
| `description` | string | Merchant name or user description |
| `amount` | number | **Positive = income, negative = expense** |
| `category_id` / `category_name` | UUID / string | Null when uncategorized |
| `account_id` / `account_name` | UUID / string | The ledger account, or the bank connection for bank-synced rows |
| `notes` | string | User or agent notes (set with `update_transaction`) |
| `is_pending` | bool | Not yet posted by the bank |

`get_transaction` adds `transaction_type`, `provider_transaction_id` (the external deduplication key),
`source`, `created_at` and `updated_at`.

**Amount convention:** expenses are negative and income is positive. A $50 coffee is `amount: -50`;
a $2,000 client payment is `amount: 2000`. `min_amount` / `max_amount` filter on this signed value.

**Editing restrictions:** amount, date and description can be changed only on manually created
transactions. Bank-synced and imported rows keep them for audit integrity, but you can still
categorize, tag and annotate them. `delete_transaction` works on manual transactions only; deleted
rows disappear from every MCP read and report.

### Categories

Categories live in the same table as ledger accounts. Don't confuse them with bank accounts. Always retrieve the current list via `list_categories` — IDs are stable but names vary per user.

Common system categories:
- Food & Dining, Transportation, Office Supplies, Software & Subscriptions
- Client Income, Consulting, Sales Revenue
- Payroll, Contractor Payments, Utilities, Rent & Lease

User-defined categories follow the same structure. Use `list_categories` with a `search` parameter to find a specific one.

### Reports

Reports are computed on demand from live transaction data; they are not stored. Use `get_spending_summary`,
`get_income_summary`, `get_transaction_stats` and `get_net_worth`.

---

<!-- TOOLS:BEGIN — generated by scripts/generate-mcp-agent-context.js, do not edit by hand -->
## All 35 Tools

### Accounts (3)

**`list_accounts`** — List Accounts. List all accounts (bank accounts and manual accounts) with their current balances and types. Use this to understand what accounts exist before querying transactions.

| Parameter | Type | | Description |
|---|---|---|---|
| `type` | "checking" \| "savings" \| "credit_card" \| "investment" \| "loan" \| "cash" \| "other" \| "all" | optional, default "all" | Filter by account type. Defaults to all types. |
| `include_inactive` | boolean | optional, default false | Include inactive/closed accounts. Defaults to false. |

**`get_account`** — Get Account. Get full details for a single account including balance, type, and recent transaction count. Use list_accounts first to find the account ID.

| Parameter | Type | | Description |
|---|---|---|---|
| `account_id` | UUID | required | The account UUID from list_accounts. |

**`list_connected_accounts`** — List Connected Bank Accounts. List all bank accounts connected via Teller (live bank connections) with their sync status and last sync time. Separate from manual accounts.

_No parameters._

### Transactions — read (4)

**`list_transactions`** — List Transactions. List transactions with optional filters. Returns paginated results (max 200 per call). Use filters to narrow results: date range, account, category, amount range. Amounts: positive = income, negative = expense.

| Parameter | Type | | Description |
|---|---|---|---|
| `since` | string | optional | Start date (YYYY-MM-DD inclusive). Example: "2024-01-01" |
| `until` | string | optional | End date (YYYY-MM-DD inclusive). Example: "2024-12-31" |
| `account_id` | UUID | optional | Filter by account UUID. |
| `category_id` | UUID | optional | Filter by category UUID. |
| `uncategorized_only` | boolean | optional, default false | Return only transactions without a category. |
| `min_amount` | number | optional | Minimum transaction amount (absolute value). |
| `max_amount` | number | optional | Maximum transaction amount (absolute value). |
| `limit` | integer | optional, default 50 | Number of results per page. Max 200. Default 50. |
| `offset` | integer | optional, default 0 | Pagination offset. Default 0. |
| `order` | "date_desc" \| "date_asc" \| "amount_desc" \| "amount_asc" | optional, default "date_desc" | Sort order. Default: date_desc (newest first). |

**`get_transaction`** — Get Transaction. Get full details for a single transaction including category history and notes.

| Parameter | Type | | Description |
|---|---|---|---|
| `transaction_id` | UUID | required | The transaction UUID. |

**`search_transactions`** — Search Transactions. Search transactions by description or notes text. Case-insensitive. Optionally filter by date range. Returns up to 100 results.

| Parameter | Type | | Description |
|---|---|---|---|
| `query` | string | required | Search text to match against description and notes. |
| `since` | string | optional | Start date (YYYY-MM-DD). |
| `until` | string | optional | End date (YYYY-MM-DD). |
| `limit` | integer | optional, default 50 | Max results. Default 50. |

**`get_uncategorized`** — Get Uncategorized Transactions. Returns transactions that have no category assigned, newest first. Use this to find transactions that need categorization. Returns up to 200 at a time.

| Parameter | Type | | Description |
|---|---|---|---|
| `since` | string | optional | Only return uncategorized transactions after this date (YYYY-MM-DD). |
| `limit` | integer | optional, default 100 | Max results. Default 100. |
| `account_id` | UUID | optional | Filter to a specific account. |

### Reports (4)

**`get_net_worth`** — Get Net Worth. Returns current net worth (total assets minus total liabilities) with a breakdown by account. Assets = checking, savings, investment, cash. Liabilities = credit cards, loans.

_No parameters._

**`get_spending_summary`** — Get Spending Summary. Get total spending for a period, broken down by category. Negative amounts = expenses. Only expense transactions are included. Useful for budget review and identifying top spending categories.

| Parameter | Type | | Description |
|---|---|---|---|
| `since` | string | required | Start date (YYYY-MM-DD). |
| `until` | string | required | End date (YYYY-MM-DD). |
| `account_id` | UUID | optional | Filter to a specific account. |
| `top_n` | integer | optional, default 20 | Return top N categories by spending. Default 20. |

**`get_income_summary`** — Get Income Summary. Get total income for a period, broken down by category/source. Only positive-amount transactions (deposits/income) are included.

| Parameter | Type | | Description |
|---|---|---|---|
| `since` | string | required | Start date (YYYY-MM-DD). |
| `until` | string | required | End date (YYYY-MM-DD). |
| `account_id` | UUID | optional | Filter to a specific account. |

**`get_transaction_stats`** — Get Transaction Statistics. Get count, total, and average transaction value for a date range and optional filters. Useful for understanding data volume before running detailed queries.

| Parameter | Type | | Description |
|---|---|---|---|
| `since` | string | optional | Start date (YYYY-MM-DD). |
| `until` | string | optional | End date (YYYY-MM-DD). |
| `account_id` | UUID | optional | Filter to a specific account. |

### Session & meta (4)

**`get_user_profile`** — Get User Profile. Get your PennyBot profile: name, email, plan tier, number of connected bank accounts, and current MCP configuration (sync mode, etc.).

_No parameters._

**`get_mcp_config`** — Get MCP Config. Returns current MCP session configuration including sync mode, available tools, and rate limits. Sync mode: realtime (writes apply immediately) or batch (writes staged until commit_batch).

_No parameters._

**`create_session_note`** — Create Session Note. Save a note from this agent session to PennyBot. Notes persist across sessions — use this to record findings, analysis summaries, or decisions so future sessions can pick up where you left off.

| Parameter | Type | | Description |
|---|---|---|---|
| `title` | string | required | Short title for the note (e.g., "Q1 2024 Audit Findings"). |
| `content` | string | required | The note content. Supports markdown. Include findings, decisions, next steps. |
| `session_id` | string | optional | Optional session identifier for grouping notes. |

**`list_session_notes`** — List Session Notes. List all notes saved by previous agent sessions. Newest first.

| Parameter | Type | | Description |
|---|---|---|---|
| `limit` | integer | optional, default 20 | Max notes to return. Default 20. |

### Categories & transactions — write (7)

> Writes need a `read_write` key.

**`list_categories`** — List Categories. List all transaction categories available to you. Use category IDs when calling categorize_transaction or bulk_categorize. System categories are shared; user categories are yours only.

| Parameter | Type | | Description |
|---|---|---|---|
| `search` | string | optional | Filter categories by name (partial match). |

**`categorize_transaction`** — Categorize Transaction. Set or clear the category on a single transaction. Use list_categories to find valid category IDs. Pass category_id: null to clear the category. All changes are logged in category history for audit trail.

| Parameter | Type | | Description |
|---|---|---|---|
| `transaction_id` | UUID | required | UUID of the transaction to categorize. |
| `category_id` | undefined | required | Category UUID to assign, or null to clear the category. |
| `note` | string | optional | Optional note explaining this categorization. |

**`bulk_categorize`** — Bulk Categorize Transactions. Assign a category to multiple transactions at once. Max 1000 transactions per call. Use list_categories to find valid category IDs. Returns count of updated transactions. All changes are logged in category history.

| Parameter | Type | | Description |
|---|---|---|---|
| `transaction_ids` | string[] | required | Array of transaction UUIDs to categorize. |
| `category_id` | UUID | required | Category UUID to assign to all transactions. |
| `note` | string | optional | Optional note logged in category history for all updated transactions. |

**`update_transaction`** — Update Transaction. Update a transaction's notes, tag, or description. Amount and date edits are only allowed on manual (non-bank-synced) transactions. Tag: "personal", "business" or "both" (the only values the database accepts). Use categorize_transaction to change the category.

| Parameter | Type | | Description |
|---|---|---|---|
| `transaction_id` | UUID | required | UUID of the transaction to update. |
| `notes` | undefined | optional | User notes for this transaction. Pass null to clear. |
| `tag` | "personal" \| "business" \| "both" \| "" | optional | Tag for grouping/filtering. Empty string to clear the tag. |
| `description` | string | optional | Override the transaction description (manual transactions only). |
| `amount` | number | optional | Override the amount (manual transactions only). |
| `date` | string | optional | Override the date, YYYY-MM-DD (manual transactions only). |

**`bulk_flag`** — Bulk Flag Transactions. Flag or unflag multiple transactions for review. Max 1000 transactions per call. Flagged transactions come back with flagged: true from the transaction tools.

| Parameter | Type | | Description |
|---|---|---|---|
| `transaction_ids` | string[] | required | Array of transaction UUIDs to flag or unflag. |
| `flagged` | boolean | required | true = flag for review, false = clear flag. |

**`create_transaction`** — Record Manual Transaction. Record a transaction the user already made, in their ledger (not connected to a bank). This only records it; it never moves money or makes a payment. Useful for cash expenses, adjustments, or historical data entry. Manual transactions can be edited and deleted; bank-synced transactions cannot. Amount: positive = income, negative = expense.

| Parameter | Type | | Description |
|---|---|---|---|
| `account_id` | UUID | required | Account UUID to attach this transaction to. |
| `amount` | number | required | Transaction amount. Positive = income, negative = expense. |
| `date` | string | required | Transaction date, YYYY-MM-DD. |
| `description` | string | required | Transaction description or merchant name. |
| `category_id` | UUID | optional | Optional category UUID. |
| `notes` | string | optional | Optional notes. |
| `tag` | "personal" \| "business" \| "both" | optional | Optional tag. |

**`delete_transaction`** — Delete Manual Transaction. Soft-delete a manual transaction. Only manual transactions (source_type = "manual") can be deleted. Bank-synced transactions cannot be deleted — categorize or flag them instead. Deleted transactions are hidden from reports but not permanently removed.

| Parameter | Type | | Description |
|---|---|---|---|
| `transaction_id` | UUID | required | UUID of the manual transaction to delete. |
| `reason` | string | optional | Optional reason for deletion (saved in notes). |

### Record locking & batch mode (6)

> Writes need a `read_write` key.

**`lock_records`** — Lock Records. Lock one or more records to prevent the sync engine from overwriting them while you work. Locks expire after 30 minutes. Max 500 records per call. Use a consistent session_id across your work session to manage your locks together. Resource types: "transaction", "account".

| Parameter | Type | | Description |
|---|---|---|---|
| `session_id` | string | required | Your session identifier (e.g. "audit-q1-2024"). Used to group and release locks. |
| `resource_type` | "transaction" \| "account" | required | Type of records to lock. |
| `resource_ids` | string[] | required | Array of record UUIDs to lock. |

**`release_locks`** — Release Locks. Release locks held by your session. Call this when you finish editing to allow sync to proceed. Locks expire automatically after 30 minutes if not released.

| Parameter | Type | | Description |
|---|---|---|---|
| `session_id` | string | required | Your session identifier. |
| `resource_type` | "transaction" \| "account" | optional | Optional: release only locks of this type. Omit to release all locks for this session. |

**`get_active_locks`** — Get Active Locks. List all locks currently held by your session. Shows locked record IDs, type, and expiry time.

| Parameter | Type | | Description |
|---|---|---|---|
| `session_id` | string | optional | Optional: filter to a specific session. Omit to see all your active locks. |

**`get_pending_changes`** — Get Pending Changes. In realtime mode, writes apply immediately — there are no pending changes. Staged (batch) writes are not available to signed-in assistants.

| Parameter | Type | | Description |
|---|---|---|---|
| `session_id` | string | required | Your session identifier to preview. |

**`commit_batch`** — Commit Batch Changes. Only available in batch mode. Your current sync mode is realtime.

| Parameter | Type | | Description |
|---|---|---|---|
| `session_id` | string | required | Your session identifier to commit. |
| `confirm` | boolean | required | Must be true to confirm you want to apply all staged changes. |

**`discard_batch`** — Discard Batch Changes. Only useful in batch mode. Your current sync mode is realtime: writes apply immediately, so nothing is staged.

| Parameter | Type | | Description |
|---|---|---|---|
| `session_id` | string | required | Your session identifier to discard. |
| `confirm` | boolean | required | Must be true to confirm discarding all staged changes. |

### Historical import (4)

**`preview_import`** — Preview Import. Dry-run analysis of transactions you want to import. Bank and card transactions only: never send credit report data (scores, tradelines, inquiries), account or card numbers, or provider account/item/institution IDs. Shows exactly what would be created, what would be skipped (duplicates), and any validation errors. ALWAYS call this before import_transactions; pass its preview_token along to the import. Max 5000 transactions per batch.

| Parameter | Type | | Description |
|---|---|---|---|
| `transactions` | object[] | required | Array of transactions to preview importing. |
| `source_name` | string | optional | Human-readable name for this import (e.g. "Chase Bank 2018-2023"). |

**`import_transactions`** — Import Transactions. Record a batch of past transactions (ones the user already made at their bank) in their PennyBot ledger. This only records them; it never moves money or makes a payment. Imported transactions cannot be deleted afterwards (only manual ones can): import only what the user asked for. Bank and card transactions only: never send credit report data (scores, tradelines, inquiries), account or card numbers, or provider account/item/institution IDs. ALWAYS call preview_import first to review what will be recorded. Pass the preview_token from preview_import and confirmed: true to record them. Deduplication is automatic — re-importing the same external_ids is safe (duplicates skipped). Max 5000 transactions per batch.

| Parameter | Type | | Description |
|---|---|---|---|
| `transactions` | object[] | required |  |
| `source_name` | string | optional | Human-readable label for this import batch (saved in audit log). |
| `preview_token` | string | optional | The preview_token preview_import returned. |
| `confirmed` | boolean | required | Must be true. Call preview_import first, review the diff, then pass confirmed: true. |

**`find_duplicates`** — Find Duplicate Transactions. Find potential duplicate transactions in PennyBot. Matches by date (±N days), amount (±$X), and description similarity. Use this before importing historical data to identify existing records that may overlap.

| Parameter | Type | | Description |
|---|---|---|---|
| `date` | string | required | Reference date (YYYY-MM-DD). |
| `amount` | number | required | Reference amount. |
| `description` | string | required | Reference description. |
| `date_tolerance_days` | integer | optional, default 1 | Days to search around the date. Default 1. |
| `amount_tolerance` | number | optional, default 0.01 | Amount tolerance in dollars. Default $0.01. |

**`get_import_history`** — Get Import History. List past MCP import batches with transaction counts and date ranges. Useful to understand what historical data has already been imported.

| Parameter | Type | | Description |
|---|---|---|---|
| `limit` | integer | optional, default 20 | Number of import batches to return. Default 20. |

### Categorization rules (3)

> Creating or deleting rules needs write access.

**`list_category_rules`** — List Categorization Rules. List the categorization rules PennyBot applies to new transactions before any AI categorization.

_No parameters._

**`create_category_rule`** — Create Categorization Rule. Save a rule PennyBot applies to every NEW transaction from now on: when match_text appears in the merchant or description (and the optional type and amount range fit), the transaction gets this category, with no AI involved. Use the exact category name from list_categories. To fix existing transactions too, use bulk_categorize. The most specific rule wins (an amount range first, then longer match text).

| Parameter | Type | | Description |
|---|---|---|---|
| `match_text` | string | required | Text to look for in the merchant or description, e.g. "uber". |
| `category` | string | required | Exact category name from list_categories. |
| `transaction_type` | "expense" \| "income" | optional | Only expenses or only income. |
| `min_amount` | number | optional | Smallest absolute amount the rule applies to. |
| `max_amount` | number | optional | Largest absolute amount the rule applies to. |

**`delete_category_rule`** — Delete Categorization Rule. Delete one categorization rule by id (from list_category_rules). Past transactions keep their categories.

| Parameter | Type | | Description |
|---|---|---|---|
| `rule_id` | UUID | required | Rule id from list_category_rules. |

<!-- TOOLS:END -->

---

## Rate Limits

| Limit | Value |
|-------|-------|
| Requests per minute | 120 |
| Bulk operations max | 1,000 items |
| Import batch max | 5,000 rows |

If you hit a rate limit, pause 60 seconds before retrying.

---

## Common Workflows

### 1. Monthly Reconciliation

```
1. list_accounts → identify accounts to reconcile
2. get_uncategorized(since: "2026-04-01") → transactions with no category
3. list_categories → build a reference map (name → id)
4. For each uncategorized transaction, infer a category from its description
5. bulk_categorize(transaction_ids, category_id) per category batch
6. get_spending_summary(since, until) → confirm totals make sense
7. create_session_note(title: "Reconciliation Apr 2026", content: summary)
```

### 2. Historical CSV Import

```
1. Parse CSV into import rows (date, amount, description, account_name, account_type, external_id)
2. Generate external_id: MD5(filename + row_number + date + amount + description)
3. preview_import(transactions, source_name: "Chase 2020-2023")
   → review what will be created, which accounts are new, and validation errors
4. If there are validation errors: fix those rows and preview again
5. import_transactions(transactions, confirmed: true)
   → a row the database rejects is reported in error_details; the other rows are kept
6. get_import_history() → confirm the import landed
7. list_transactions(since, until) → spot-check a few rows
```

### 3. Tax Prep Pull

```
1. get_transaction_stats(since: "2025-01-01", until: "2025-12-31") → counts and totals
2. get_income_summary(since, until) → income by source
3. get_spending_summary(since, until, top_n: 50) → expenses by category
4. list_transactions(since, until, min_amount: 0) → income rows (positive)
5. list_transactions(since, until, max_amount: 0) → expense rows (negative)
6. get_uncategorized(since) → anything still needing a category
7. create_session_note("Tax Prep 2025", detailed markdown summary)
```

### 4. Net Worth Snapshot

```
1. get_net_worth() → assets, liabilities, net worth
2. list_accounts(type: "loan") → drill into liabilities
3. list_accounts(type: "investment") → drill into assets
4. create_session_note("Net Worth Snapshot", summary with key numbers)
```

### 5. Safe Bulk Edit with Locking

```
1. list_transactions(...) → collect transaction IDs to edit
2. lock_records(session_id, "transaction", transaction_ids)
3. bulk_categorize(transaction_ids, category_id)
4. release_locks(session_id, "transaction")
```

---

## Error Handling

| Situation | What to do |
|-----------|-----------|
| `isError: true` response | Read the text field — it contains a human-readable explanation |
| `"read-only"` error | The connection has read access only; the user reconnects the assistant with write access |
| `"transaction not found"` | Transaction doesn't exist or belongs to a different user |
| `"account not found"` | Account ID is wrong; call `list_accounts` to refresh |
| `"category not found"` | Category was deleted; call `list_categories` to refresh |
| HTTP 401 | The sign-in expired or was disconnected: the user signs in again from the assistant |
| HTTP 500 | PennyBot server error — retry once after 10 seconds |

Never silently swallow errors. Always surface them to the user with context.

---

## Batch Mode vs. Realtime Mode

**Every connection is realtime:** every write tool applies immediately.
`get_pending_changes`, `commit_batch` and `discard_batch` exist, but nothing is staged for them to act on,
so they report an empty queue. Until staging ships, review before writing: use `preview_import` before
`import_transactions`, `lock_records` around multi-step edits, and `get_transaction` to confirm a row
before changing it.

---

## Session Notes Best Practices

Session notes persist across your MCP sessions. Use them to:
- Record what you've done (so next session picks up where you left off)
- Save summaries the user asked for (monthly reports, tax summaries)
- Flag items that need human review

```
create_session_note(
  title: "April 2026 Reconciliation — Complete",
  content: "Categorized 142 transactions. 3 flagged for user review (large cash withdrawals).\nNet expenses: $8,420. Top category: Office Supplies ($1,240).",
  session_id: "my-agent-session-001"
)
```

---

## Project Instructions Setup

Add this to your assistant's project instructions:

```markdown
## PennyBot MCP

Connected to PennyBot financial data via MCP.

Access: [read-only / read and write]

Rules:
- Always call get_mcp_config at session start to verify connection
- Never delete bank-synced transactions (source_type = 'teller')
- Confirm with user before bulk_categorize on > 50 transactions
- Use lock_records before large bulk edits in realtime mode
- Save session notes after completing any significant task
- Amount convention: positive = income, negative = expense
- Dates: always YYYY-MM-DD format

Reference: https://pennybot.io/mcp/agent-context
```

---

## Support

- Docs: https://pennybot.io/for-agents
- Settings: https://pennybot.io/settings.html#assistants (AI Assistants)
- Support: support@pennybot.io
