Inspiration

Filing the Spanish IRPF (income tax) is already painful for a salaried worker. It gets worse when money arrives through platforms. PayPal is not just a wallet: a single account can contain second-hand sales from Vinted or Wallapop, occasional freelance payments, refunds, internal currency conversions and PayPal fees. Each one can have a different tax treatment, and most people have no idea which is which.

The base project, https://github.com/agarnung/IRPF-declaration, already automated the Interactive Brokers side of the Spanish tax return: FIFO gains/losses on stocks and ETFs, options (including index options), and the two-month loss rule. What was missing was the messy, heterogeneous PayPal history that nobody classifies properly. I built this update during the PayPal AI Hackathon 2026 to close that gap.

What it does

The tool adds PayPal as a second data source to the existing IRPF pipeline:

  1. Imports PayPal transactions through the PayPal REST Transaction Search API v1 (OAuth2 client credentials, sandbox or live), or through deterministic mock data when no credentials are available.
  2. Classifies every movement into one of six fiscal categories: venta_ocasional (occasional sale), ingreso_recurrente (recurring income), reembolso (refund), cambio_divisa (currency conversion), comision (fee) or otro (other).
  3. Explains each classification in plain language, with a confidence score, an approximate Renta Web box/section and a "review manually" flag.
  4. Converts amounts to EUR using official ECB exchange rates, reusing the logic that already existed for Interactive Brokers.
  5. Outputs:
    • tabla_paypal_irpf_<year>.csv — one row per transaction, with category, AI confidence, EUR amount, suggested box, review flag and explanation.
    • extras/paypal_resumen_por_categoria.csv — aggregated totals per category.
    • extras/paypal_resumen_natural.txt — a plain-language summary plus the list of movements that need manual review.
    • extras/paypal_informe.html — a self-contained visual report (KPIs, category summary, filterable movements, explanations, downloads) that opens in any browser with no server or extra dependencies.

It runs with zero credentials by default (mock data + heuristic classifier), so judges can execute it immediately:

python process_paypal.py --ejercicio 2026

Real modes are opt-in through environment variables (PAYPAL_MODE=real / CLASSIFIER_MODE=llm). If credentials are missing or an API call fails, the pipeline falls back safely to mock/heuristics instead of crashing — the demo never breaks.

How I built it

The design goal was to keep PayPal and the LLM behind small interfaces so the rest of the pipeline never depends on a vendor payload or a specific provider:

  • paypal/models.py — dataclasses PayPalTransaction and ClassifiedTransaction, plus the closed list of fiscal categories.
  • paypal/client.py — abstract PayPalClient, a real PayPalSandboxClient (OAuth2 + Transaction Search API, paginating in 31-day blocks because PayPal requires it) and a deterministic MockPayPalClient covering all relevant categories. get_paypal_client() selects real vs. mock from PAYPAL_MODE.
  • paypal/classifier.py — abstract TransactionClassifier, a real LLMTransactionClassifier against any OpenAI-compatible API (configurable through OPENAI_BASE_URL or LITELLM_BASE_URL, so OpenAI, Azure OpenAI, OpenRouter, Groq, a local vLLM server or a local LiteLLM proxy all work), and a HeuristicTransactionClassifier that uses keywords, transaction type and batch-level recurrence detection (same amount + same counterparty repeated ≥ 2 times). get_classifier() selects LLM vs. heuristic from CLASSIFIER_MODE.
  • process_paypal.py — the pipeline: fetch → classify → convert to EUR → write CSV + natural-language summary + self-contained HTML report.
  • tests/ — 31 tests covering the client, the classifier and the end-to-end pipeline.

The LLM is asked for strict JSON (category, confidence, explanation, suggested box, review flag). If a single LLM call fails, that transaction falls back to the heuristic classifier instead of aborting the whole batch. The heuristic classifier also detects recurrence across the full batch, which a per-transaction rule cannot see.

The branch contains 17 commits and 34 changed files (~2,550 added lines) relative to main, so the evolution is visible in the Git history rather than squashed into one commit.

Challenges I ran into

  • PayPal's 31-day search window. Transaction Search rejects wider ranges, so the real client has to paginate date blocks and pages inside each block. Getting that loop right without duplicates was the main integration challenge.
  • Heterogeneous PayPal data. PayPal event codes and memos are inconsistent. The parser handles the fields documented for Transaction Search, but it does not yet map every possible transaction_event_code exhaustively; that is explicitly listed as future work.
  • Testing the LLM without a key. The PayPal sandbox path was validated end to end (OAuth + Transaction Search + one imported transaction). The LLM classifier was validated against a LiteLLM-compatible endpoint; the recorded demo shows CLASSIFIER_MODE=llm, and every transaction still falls back to the heuristic classifier if the model is unavailable.
  • Tax nuance vs. automation. Distinguishing a genuine occasional sale from undeclared economic activity is not a purely technical problem. The tool therefore reports confidence, explains its reasoning and flags ambiguous/high-value cases for manual review instead of pretending to be definitive.
  • Keeping the demo unbreakable. Every real component has a safe fallback, and the mock data is deterministic, so a missing key or a network failure cannot ruin a live demo.

Accomplishments that I'm proud of

  • A complete, runnable end-to-end flow that works offline, with no credentials, and produces coherent CSV, prose and a self-contained HTML report.
  • 31 tests passing without network access or credentials (the only network call is the pre-existing ECB rate cache).
  • A clean, swappable architecture: real/mock client and real/heuristic classifier behind the same interfaces.
  • Explainability as a first-class output, not an afterthought: every row says why it was classified that way and whether a human should review it.
  • A recorded demo (2:55.2, 9 shots) that shows the mock pipeline, the tests, the PayPal sandbox and the LLM classifier.
  • Adding the missing OSI license (MIT) and documenting exactly what is new, to support the "significant update" requirement honestly.

What I learned

  • Wrapping vendor payloads behind a domain model makes the pipeline testable and keeps API changes contained.
  • The OpenAI-compatible API surface is a practical abstraction: one client works across many providers and even a local model server.
  • Deterministic mocks are worth the effort — they make demos reproducible and tests independent of external services.
  • Some signals (recurrence) only make sense at batch level, not per transaction.
  • In tax software, humility is a feature: a confident wrong answer is worse than an explicit "review this manually".

What's next

  • CLASSIFIER_MODE=llm is tested against a LiteLLM-compatible endpoint; PAYPAL_MODE=real is already tested against the PayPal sandbox (OAuth + Transaction Search + one transaction).
  • Add an exhaustive mapping of PayPal transaction_event_code values.
  • Add local caching/pagination for high-volume real accounts, so repeated runs do not re-query the whole history.
  • Refine categories, year-specific Renta Web boxes and recurrence thresholds with an accountant's review.
  • Optional next step: a small web UI for non-technical users, and English/Catalan localisation.

What is new in this submission (significant update)

To be explicit for the judges: the base repository previously handled Interactive Brokers only. During the hackathon submission period I added:

  • The entire paypal/ package: models.py, client.py, classifier.py (real/mock PayPal client and LLM/heuristic classifier).
  • process_paypal.py, the new end-to-end PayPal pipeline.
  • 31 tests in tests/test_paypal_client.py, tests/test_paypal_classifier.py and tests/test_process_paypal.py.
  • A new README section explaining the PayPal module and exactly what is new.
  • .env.example, updated requirements.txt / requirements-dev.txt, and .gitignore rules that exclude .env and caches.
  • A root MIT LICENSE, which the repository did not have before.

No existing Interactive Brokers logic in process_data.py was removed or rewritten. The 17 commits on paypal-ai-hackathon-2026 show the work landing incrementally.

Try it

No hosted application. The test build is the command-line pipeline in the public repository. It runs offline with mock data and the heuristic classifier, without credentials.

You can also run it with your own data and a local LLM. We tested LiteLLM and vLLM, but any equivalent OpenAI-compatible endpoint works as explained in the repository.

git clone https://github.com/agarnung/IRPF-declaration.git
cd IRPF-declaration
git switch paypal-ai-hackathon-2026
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python process_paypal.py --ejercicio 2026
pytest tests/ -q

Expected: 31 passed. The default command uses mock data + the heuristic classifier and needs no credentials. Optional real modes:

PAYPAL_MODE=real python process_paypal.py --ejercicio 2026
CLASSIFIER_MODE=llm python process_paypal.py --ejercicio 2026

No credentials are required for the default test. For optional real modes, use your own PayPal Sandbox and OpenAI-compatible LLM credentials.

Built With

  • cli
  • csv
  • european-central-bank-(ecb)-reference-exchange-rates
  • oauth2-client-credentials
  • openai-compatible-llm-apis-(openai-/-azure-openai-/-openrouter-/-groq-/-vllm-/-litellm-local)
  • openai-python-sdk
  • pandas
  • paypal-rest-transaction-search-api-v1
  • paypal-sandbox
  • pytest
  • python
  • python-dotenv
  • requests
Share this project:

Updates

Submission history