Configuring plaid2text for Multiple Accounts - Tips and Gotchas

After reading @finance_fred’s excellent introduction to plaid2text, I wanted to share some more advanced configuration tips. In my role as a financial controller, I’ve set up plaid2text to handle transaction imports from multiple bank accounts across our company, and there are some gotchas worth knowing about.

Multiple Account Configuration

The basic config file structure works fine for a few accounts, but when you’re managing 5+ accounts, organization becomes important.

Organizing Your Config

Here’s how I structure my config for a company with multiple bank accounts and credit cards:

[DEFAULT]
encoding = utf-8
currency = USD
mongo_db = plaid2text
mongo_db_uri = mongodb://localhost:27017
output_format = beancount
default_expense = Expenses:Uncategorized

[PLAID]
client_id = your_client_id
secret = your_secret

# ==== OPERATING ACCOUNTS ====

[main_checking]
posting_account = Assets:Bank:MainChecking
mapping_file = ~/.config/plaid2text/main_checking/mapping
template_file = ~/.config/plaid2text/main_checking/template
journal_file = ~/accounting/imports/main_checking.beancount

[payroll_account]
posting_account = Assets:Bank:Payroll
mapping_file = ~/.config/plaid2text/payroll/mapping
template_file = ~/.config/plaid2text/payroll/template
journal_file = ~/accounting/imports/payroll.beancount

# ==== CREDIT CARDS ====

[amex_corporate]
posting_account = Liabilities:CreditCard:AmexCorporate
mapping_file = ~/.config/plaid2text/amex_corp/mapping
template_file = ~/.config/plaid2text/amex_corp/template
journal_file = ~/accounting/imports/amex_corp.beancount

[visa_purchasing]
posting_account = Liabilities:CreditCard:Visa
mapping_file = ~/.config/plaid2text/visa/mapping
template_file = ~/.config/plaid2text/visa/template
journal_file = ~/accounting/imports/visa.beancount

Per-Account Template Files

Here’s a template that works well for business accounts. Save this as your template file:

{date} * "{payee}"
  plaid_id: "{plaid_id}"
  plaid_category: "{category}"
  {account}  {amount} {currency}
  {posting_account}

Key points:

  • Always include plaid_id as metadata - it helps with deduplication
  • plaid_category is useful for reviewing AI categorization suggestions
  • Make sure metadata values are quoted strings (learned this the hard way)

Mapping File Best Practices

Your mapping file handles payee-to-account mappings. Structure it by expense category:

# === PAYROLL ===
ADP PAYROLL.*|Expenses:Payroll:Processing
GUSTO.*|Expenses:Payroll:Processing

# === UTILITIES ===
APS ELECTRIC.*|Expenses:Utilities:Electric
SOUTHWEST GAS.*|Expenses:Utilities:Gas
COX COMMUNICATIONS.*|Expenses:Utilities:Internet

# === OFFICE SUPPLIES ===
STAPLES.*|Expenses:Office:Supplies
OFFICE DEPOT.*|Expenses:Office:Supplies
AMAZON.*OFFICE.*|Expenses:Office:Supplies

# === PROFESSIONAL SERVICES ===
QUICKBOOKS.*|Expenses:Software:Accounting
ZOOM.*|Expenses:Software:Communications

Common Gotchas

1. Template Metadata Syntax

This will fail:

_plaid_id: {plaid_id}

This works:

plaid_id: "{plaid_id}"

Beancount metadata keys can’t start with underscore, and values should be quoted strings.

2. Duplicate Transaction Detection

plaid2text stores transactions in MongoDB to avoid duplicates, but this only works per-account. If you’re pulling the same transaction from multiple linked accounts (like a transfer between accounts), you’ll get it twice.

My workaround: use a post-processing script that identifies transfers by amount/date matching:

# Simple transfer detection
def find_transfers(transactions):
    potential_transfers = []
    for t1 in transactions:
        for t2 in transactions:
            if (t1.account \!= t2.account and 
                abs(t1.amount) == abs(t2.amount) and
                t1.date == t2.date):
                potential_transfers.append((t1, t2))
    return potential_transfers

3. OAuth Token Expiration

Plaid access tokens don’t expire by default, but some banks require periodic re-authentication. Chase, in my experience, needs re-linking every 90 days or so. Set up calendar reminders for accounts that need this.

4. Rate Limiting

If you’re syncing many accounts at once, add delays between API calls:

#\!/bin/bash
for account in main_checking payroll amex_corporate visa_purchasing; do
    plaid2text $account ~/accounting/imports/$account.beancount --since 7d
    sleep 5  # Be nice to the API
done

Questions for the Community

  1. Has anyone found a better approach to transfer detection?
  2. How do you handle account nicknames vs. official names in Plaid?
  3. Any tips for managing configs across multiple machines (version control?)?

Happy to share more details if anyone’s working on similar setups!

This is incredibly helpful! The config organization tips alone are worth their weight in gold.

For transfer detection, I’ve been using a slightly different approach that works well for my client bookkeeping:

Instead of detecting transfers after import, I create explicit transfer accounts in my mapping file:

# Internal transfers - mark with special payee
TRANSFER FROM.*CHECKING|Assets:Bank:Savings
TRANSFER TO.*SAVINGS|Assets:Bank:Checking
ONLINE TRANSFER.*|Assets:Internal:Clearing

Then I have a simple Beancount plugin that matches entries in the clearing account and links them:

def find_clearing_entries(entries, options_map):
    """Match clearing account entries by amount and date."""
    clearing = []
    for entry in entries:
        for posting in entry.postings:
            if "Clearing" in posting.account:
                clearing.append(entry)
    # Match logic here...

It’s not perfect, but it handles most cases.

Version control question: I keep all my plaid2text configs in a private Git repo:

~/dotfiles/
  plaid2text/
    config
    accounts/
      main_checking/
        mapping
        template
      payroll/
        ...

I symlink from ~/.config/plaid2text to the repo. Works great for keeping configs in sync across my laptop and desktop. Just be careful not to commit your Plaid API secrets - I use environment variables instead:

[PLAID]
client_id = ${PLAID_CLIENT_ID}
secret = ${PLAID_SECRET}

Does plaid2text support env var interpolation? I had to patch it locally to get this working.

Great deep dive on configuration! A few best practices I’d add from the professional accounting side:

Account Naming Conventions

For clients who may eventually migrate to other systems or need to share data with CPAs (like me!), I recommend following standard chart of accounts patterns:

# Good - follows standard COA structure
posting_account = Assets:Current:BankAccounts:Chase:Operating

# Less good - harder to map to financial statements
posting_account = Assets:Bank:Chase:Main

The extra hierarchy helps when generating financial statements:

  • Assets:Current vs Assets:Fixed
  • Liabilities:Current vs Liabilities:LongTerm
  • Expenses by department/function

Segregation of Duties Consideration

If you’re using this for a business with multiple people accessing finances, think about:

  1. Who controls the Plaid credentials? These effectively grant read access to all linked accounts
  2. Who reviews imported transactions? Automated imports shouldn’t mean unreviewed transactions
  3. Audit trail: Your MongoDB should be backed up and the backups secured

Mapping File Documentation

I’d strongly suggest adding comments to explain why mappings exist:

# Rent - main office lease, 123 Main St
# Started Jan 2024, landlord: ABC Properties
MAIN STREET PROP.*|Expenses:Occupancy:Rent:MainOffice

# IT consulting - approved vendor, SOW dated 2024-03-15
ACME CONSULTING.*|Expenses:Professional:IT

This documentation is invaluable during audits or when you need to explain a categorization choice.

Adding to @accountant_alice’s point about documentation - this becomes critical during tax season.

Tax-Relevant Mapping Categories

When setting up your mappings, think about how transactions will need to be reported on tax forms. Here’s a tax-aware structure I recommend:

# === MEALS - different treatment depending on context ===
# 100% deductible client meals (with documentation)
RESTAURANT.*CLIENT.*|Expenses:Meals:ClientEntertainment
# 50% deductible business meals
RESTAURANT.*|Expenses:Meals:BusinessMeals

# === VEHICLE - track mileage vs actual separately ===
GAS STATION.*|Expenses:Vehicle:Fuel
AUTO REPAIR.*|Expenses:Vehicle:Maintenance

# === HOME OFFICE ===
INTERNET.*|Expenses:HomeOffice:Internet
UTILITIES.*|Expenses:HomeOffice:Utilities

Why This Matters

At tax time, you’ll need to:

  1. Separate deductible from non-deductible expenses
  2. Calculate percentage deductions (meals, home office)
  3. Substantiate business purpose for certain categories

If your mapping is granular enough, you can generate tax-ready reports directly from Fava rather than manually reviewing every transaction.

A Cautionary Note

Don’t let automation create a false sense of accuracy. I’ve seen clients get into trouble because:

  • A personal expense got auto-categorized as business
  • A mapping rule was too broad and caught wrong transactions
  • Nobody reviewed the imports before filing

Automated categorization should be a starting point, not the final word. Monthly review of categorized transactions is still essential.