Penny Pincher is an agent-friendly CLI for connecting a bank account with Plaid and reading account data as JSON.
npx -p penny-pincher penny-pincher
npx -p penny-pincher penny-pincher auth
npx -p penny-pincher penny-pincher sync
npx -p penny-pincher penny-pincher chart create net-worth-over-time --preset net-worth-over-time
npx -p penny-pincher penny-pincher chart run --json
npx -p penny-pincher penny-pincher accounts
npx penny-pincher dashboard
npx -p penny-pincher penny-pincher-dashboard
npx -p penny-pincher penny-pincher balances
npx -p penny-pincher penny-pincher transactions --days 30
npx -p penny-pincher penny-pincher recurring
npx -p penny-pincher penny-pincher cache transactions --days 30Running penny-pincher with no command prints a JSON readiness report with the next command to run. It does not open an interactive menu by default.
The default CLI flow uses the hosted Penny Pincher backend:
npx -p penny-pincher penny-pincher authThe CLI prints any required Stripe Checkout or Plaid Link URLs so an agent can hand the URL to a human. Pass --open when a human is driving the terminal and you want the browser to open automatically.
The backend creates Plaid Link tokens, exchanges public tokens, and proxies Plaid data requests. The CLI stores encrypted token envelopes and a local signing key at ~/.penny-pincher/config.json.
Run auth again to link another institution. Data commands query every linked item, so Chase and Mercury can sit side by side in the same local config.
If you deploy your own backend, point the CLI at it:
export PENNY_PINCHER_API_URL=https://your-vercel-app.vercel.app
npx -p penny-pincher penny-pincher authProduction Plaid is the default for the hosted backend. For sandbox testing, pass --env sandbox:
npx -p penny-pincher penny-pincher auth --env sandboxpenny-pincher authopens Plaid Link, exchanges the public token through the backend, and adds or refreshes one linked institution. It requests 730 days of transaction history by default; use--history-days <days>to change that for new Items.penny-pincher accountsprints linked accounts.penny-pincher dashboardstarts a local dashboard server with linked accounts and account-level transactions.penny-pincher balancesprints accounts with balances.penny-pincher transactions --days 30prints recent transactions.penny-pincher recurringprints Plaid recurring inflow and outflow streams.penny-pincher synchydrates or updates the encrypted local SQLite cache at~/.penny-pincher/penny.db. This is the command that writes Plaid data into SQLite.penny-pincher cache summaryprints local cache counts and sync state.penny-pincher cache transactions --days 30reads transactions from the local cache without calling Plaid.penny-pincher cache accounts,cache holdings, andcache investment-transactionsread cached account and Investments data.penny-pincher chart create <id>creates a local dashboard chart backed by a JS processor.penny-pincher chart listprints local chart definitions.penny-pincher chart run [id]executes local chart processors against the SQLite cache.penny-pincher identityprints account owner identity data when the product is enabled.penny-pincher numbersprints ACH/routing data when the Plaidauthproduct is enabled.penny-pincher statusprints local connection metadata, readiness, and the next command without exposing secrets.penny-pincher doctorprints the same machine-readable readiness report asstatus.penny-pincher usageprints current billing-period usage and estimated costs.penny-pincher billingprints a Stripe Customer Portal URL.penny-pincher interactiveopens the human-oriented menu.penny-pincher logoutremoves all saved local tokens.penny-pincher logout --purge-dataalso deletes the local SQLite cache and removes its encryption key.
All data commands print JSON so another agent or script can parse them directly.
Commands accept --json where they have non-data output. Error responses also become machine-readable when --json is present.
Penny Pincher has two data paths:
- Live commands such as
accounts,balances,transactions,recurring,identity, andnumberscall the backend/Plaid read path directly. - Local cache commands such as
sync,cache, and the dashboard net worth chart use~/.penny-pincher/penny.db.
The cache is intentionally explicit. penny-pincher sync is the writer: it pulls data for every linked Item and stores encrypted rows in SQLite. Normal live API reads do not write through to SQLite, and the dashboard Refresh button currently re-reads the local dashboard API from the existing DB; it does not call Plaid or hydrate the cache.
The SQLite cache uses Node's built-in node:sqlite module, so it does not install or build a native npm SQLite addon. Penny Pincher requires Node >=22.5.0 for local cache commands.
Recommended agent flow:
npx -p penny-pincher penny-pincher auth
npx -p penny-pincher penny-pincher sync
npx penny-pincher dashboardUse penny-pincher sync again whenever you want to refresh the local warehouse for dashboards or agent analysis. Use penny-pincher sync --reset to delete the local DB first and rebuild it from Plaid. If ~/.penny-pincher/penny.db is deleted manually, only the cache is gone; run penny-pincher sync to recreate it as long as ~/.penny-pincher/config.json still contains the linked Item metadata and localDatabaseKey.
Charts live in ~/.penny-pincher/charts. The declarative registry is charts.json; each chart points at a local .mjs processor. Processors run inside the local dashboard server and receive decrypted cache rows:
export default function chart({ cache, helpers, options }) {
return {
yFormat: "currency",
series: [
{
name: "Net worth",
points: helpers
.netWorthSeries(cache.accounts, cache.transactions, "2026-01-01", helpers.today())
.map((point) => ({ x: point.date, y: point.netWorth }))
}
]
};
}Supported chart kinds are time-series, number, and delta. You can start from a preset:
penny-pincher chart create net-worth-over-time --preset net-worth-over-time
penny-pincher chart create weekly-income-spending --preset weekly-income-spending
penny-pincher chart create net-worth-today --preset net-worth-today
penny-pincher chart create monthly-trade-volume --preset monthly-trade-volumeAgents can iterate by editing the generated .mjs processor or replacing it with:
penny-pincher chart create custom-view --kind time-series --processor ./custom-view.mjs --replace
penny-pincher chart run custom-view --jsonThe dashboard runs saved processors when it loads and renders successful charts above accounts. Click a chart card to open its specific chart URL. The /charts route lists every saved chart, and focus-page chart URLs are available at /charts/<chart-id>, for example http://localhost:7778/charts/net-worth-today.
The dashboard net worth series is currently a balance reconstruction: it starts from current cached account balances and walks backward through cached transaction amounts. This is useful for the first dashboard graph, but true historical investment market value requires cached investment holdings/value snapshots over time.
The hosted backend stores your Plaid app credentials in Vercel environment variables. It does not need to store per-user Plaid access tokens. Instead, it returns encrypted token envelopes to the CLI. Data commands send each envelope back with a signed request; the backend decrypts the envelope just long enough to call Plaid.
Penny Pincher stores the encrypted envelope and a local private signing key in ~/.penny-pincher/config.json with 0600 file permissions. Treat that file like a password. If someone steals the full file, they can query data until you revoke the Plaid Item or rotate backend encryption keys.
The local cache stores account, transaction, holding, security, and investment transaction payloads as AES-256-GCM encrypted JSON inside ~/.penny-pincher/penny.db. The cache encryption key is a random 32-byte base64url value stored in ~/.penny-pincher/config.json as localDatabaseKey, by design, so future npx penny-pincher runs can read the cache without prompting. Treat config.json as the root local secret: someone who can copy both the config and database can decrypt the cached data. The SQLite file itself uses keyed hashes for record lookup and does not store raw Plaid IDs as primary keys.
Deploy this repository to Vercel and set:
PLAID_CLIENT_ID=your-client-id
PLAID_SECRET=your-secret
PLAID_SANDBOX_SECRET=your-sandbox-secret
PLAID_ENV=production
PLAID_REDIRECT_URI=https://penny-pincher-cli.vercel.app/oauth-return
PENNY_PINCHER_ENCRYPTION_KEY=at-least-32-random-bytes
PENNY_PINCHER_TOKEN_KEY_VERSION=v1Generate a strong encryption key with:
openssl rand -base64 32The Vercel API exposes:
POST /api/link-tokenPOST /api/exchangePOST /api/accountsPOST /api/balancesPOST /api/transactionsPOST /api/transactions-syncPOST /api/recurringPOST /api/investments-holdingsPOST /api/investments-transactionsPOST /api/identityPOST /api/numbers
You can still run the CLI without the hosted broker by using local Plaid credentials:
export PLAID_CLIENT_ID=your-client-id
export PLAID_SECRET=your-secret
export PLAID_ENV=sandbox
npx -p penny-pincher penny-pincher auth --direct-plaidnpm install
npm run typecheck
npm run build
npm run dev -- statusPublishing is intentionally left to the package owner:
npm login
npm publish