PandaPayLens

An open-source, local-first toolkit that decodes payment provider errors, webhooks and event logs, rebuilds the transaction timeline and explains what looks wrong, without sending the data anywhere.

Status
Active
Type
Developer tool
Published
Case study written
License
Apache-2.0
Context
6 documented items
Decision
5 documented items
Implementation
11 documented items
Evidence
21 documented items
PandaPayLens: one mark per documented item on each track.
Provider adapters
40 of 42 roadmap providers
Fully supported
Stripe and Adyen; 38 others limited, with documented gaps
Diagnostic rules
15 deterministic rules
Tests at 0.14.0
4,900 reported passing
Synthetic fixtures
769 files
License
Apache-2.0

Problem

When a payment integration misbehaves, the evidence is a pile of provider-specific JSON, XML and webhook deliveries. Reading it means knowing each provider's formats, error codes and delivery habits, and the data is often too sensitive to paste into an online tool.

Context

Payment providers each describe a payment differently. A failed charge can arrive as a JSON error from an API, an XML document from an older gateway, a form-encoded postback, a raw HTTP response with a plain-text body, or a series of webhook deliveries that may be duplicated or out of order. Debugging means translating all of that into one picture: which events belong to the same transaction, what the provider actually reported, and what is merely missing.

PandaPayLens takes pasted input from 40 providers, detects the provider and format, normalizes the records, rebuilds the timeline and runs a catalog of explainable rules over it. It runs in three places from the same engine: a web analyzer, a static demo on GitHub Pages, and an offline CLI published to npm as pandapaylens.

The design constraint that shapes most decisions is privacy. Payment payloads contain card data, names, emails and credentials, so the analysis must run where the data already is: in the browser or on the developer's machine.

Constraints

  • No payment data may leave the deviceAnalysis runs in a Web Worker in the browser or in the local CLI. The web app sets connect-src 'self' in its Content Security Policy, so the browser itself blocks requests to other origins.
  • No AI or network calls in the diagnostic pathFindings come from deterministic rules with evidence, confidence and stated limitations. The engine package has no runtime dependencies.
  • Provider documentation is the only source of truthTwo roadmap providers stay blocked because they publish no developer documentation. Adapters are not built from guesses.
  • Untrusted, possibly hostile inputPasted text can be enormous, deeply nested or crafted. Parsing must fail with typed errors rather than stack overflows, quadratic regexes or entity expansion.

My role

Creator and maintainer. All 58 commits on main and all 39 merged pull requests up to release 0.14.0 are authored under my GitHub account. The repository was created on 2026-10-25 and shipped 14 releases in its first day.

Architecture

The repository is a pnpm workspace. packages/core holds the engine with no dependencies, packages/adapters/* holds one package per provider, packages/rules holds the diagnostic catalog, packages/providers is the registry, and packages/cli bundles everything into the npm package. Two apps reuse the same pages: apps/web for Vercel and apps/pages as a static export for GitHub Pages.

The analysis is a pipeline. parseInput decides the format, every adapter scores how likely the record is its provider, the best candidate above a threshold of 0.5 normalizes it, and the engine sorts, correlates and evaluates rules.

From pasted text to findings
  1. ParseparseInputJSON, JSONL, form, HTTP, XML, name: value text
  2. DetectdetectEvery adapter scores; threshold 0.5, ambiguity margin 0.15
  3. NormalizenormalizeAllowlisted fields, amounts as integer minor units
  4. OrdersortEventsZoned timestamps only; untimed events keep their place
  5. CorrelatecorrelateUnion-find over explicit identifiers
  6. DiagnosedefaultRules15 rules with evidence, confidence and limitations

Pasted text is parsed by parseInput into JSON, JSONL, form, HTTP, XML or name-value text. Each adapter scores the record in detect; the best match above 0.5 is normalized and its errors decoded. A time guard drops timestamps without a zone, sortEvents orders events, correlate groups them by shared identifiers with union-find, and 15 rules produce findings that the report masks and exports.

Technology stack
TechnologyVersionUsed for
TypeScript6.0Engine, adapters, rules, CLI and web app
Next.js16.4Web analyzer (Vercel) and static GitHub Pages export
React19.3Analyzer interface
Web Workernot statedRuns the analysis off the main thread, in the browser
esbuild0.28Single-file CLI bundle with no runtime dependencies
Vitest + fast-check5.0 / 4.10Unit, property and scenario tests
Playwright1.64Browser tests, including privacy regression tests
pnpm workspaces10.29Monorepo with one package per provider adapter

Implementation

Each adapter implements the same interface: detect, parse, normalize, decodeErrors and correlationKeys, plus a capability manifest that states what is supported, limited or experimental. A manifest capability is marked supported only when it is implemented and tested.

Normalization copies only allowlisted fields. Amounts are kept as integer strings in minor units, never floats, and an UNSAFE_INTEGER warning is raised when a value is not an exact integer. Every normalized event carries a coarse lifecycle stage, but the type documentation is explicit that this is a display aid: authorization is not settlement, and a capture is not necessarily settlement either.

Rules are pure functions of the normalized context. A rule that throws becomes a RULE_FAILED warning instead of crashing the analysis, and finding text never quotes payment identifiers, so masking in reports stays effective.

Technical decisions

Recognize repeated deliveries even when the provider sends no delivery id

Problem
Duplicate detection relied on each adapter's delivery key. Providers such as dLocal, Paystack and PayU India send notifications without one, so a refund notification delivered twice could make the amount rule report refunds larger than the payment.
Decision
The engine adds a content fingerprint to every event: a 64-bit FNV-1a pair over the provider, format, event path and the record serialized with sorted keys. Repeats keyed by a delivery id are reported as confirmed; repeats found by identical content are reported as possible.
Tradeoff or limitation
Content repeats remain a possibility, not proof, and the data cannot show whether the application processed the event twice.

Only trust timestamps that carry a time zone

Problem
Date.parse reads a date-time without a zone in the local zone of the machine running it, so the same input could produce a different order on different machines.
Decision
Event and observed timestamps are kept only when they are ISO 8601 with Z or an offset. Others are dropped and counted in a TIMESTAMP_IGNORED warning. Adapters whose providers send zone-less times (CCBill, Trust Payments) set eventTimeZoneAssumed, shown as "(UTC assumed)".
Tradeoff or limitation
The pull request states that all adapters already passed this check; the guard is defense in depth for future adapters. Ordering against other providers may be off by hours when UTC is assumed.

Parse XML with a small reader that refuses DOCTYPE

Problem
Several gateways answer in XML. A general XML parser brings external entities and entity expansion into a tool that reads untrusted input.
Decision
A dependency-free reader rejects any DOCTYPE or markup declaration, decodes only the five predefined entities and numeric references, and enforces depth 64, 50,000 elements and the text length limit in one linear pass with an explicit stack.
Tradeoff or limitation
Documents with a DOCTYPE line, such as some Worldpay WPG samples, must be pasted without it.

Accept common JSON mistakes, but only two of them

Problem
Developers paste JSON copied from logs and editors, often with comments or trailing commas.
Decision
When strict JSON.parse fails, relaxJson removes comments outside strings and commas directly before a closing bracket, then reports a LENIENT_JSON warning naming what was removed. Single quotes, unquoted keys and missing brackets are never repaired.
Tradeoff or limitation
A property test checks that 300 random JSON.stringify outputs pass through unchanged.

Publish one npm package instead of forty

Problem
The workspace has more than 40 packages; publishing each would multiply release work and supply-chain surface.
Decision
The CLI and engine ship as a single unscoped package bundled with esbuild, with no runtime dependencies. A smoke test packs the tarball, installs it offline into an empty project, runs the binary and type-checks a consumer in strict mode. Publishing uses npm trusted publishing with provenance and no stored token.
Tradeoff or limitation
Trusted publishing can only be configured for a package that already exists, so the first version was published by hand and the workflow now exits early when the version is already on npm.

Alternatives considered

  • Recognize repeated deliveries even when the provider sends no delivery idTreating identical content as a confirmed duplicate was rejected: two separate events with exactly the same fields would look the same. Adapters can set contentMayRepeat where a provider documents that identical records are separate events (Paymentwall partial refunds, EBANX refund and chargeback notifications).
  • Parse XML with a small reader that refuses DOCTYPENo adapter claimed XML at first; such records were reported as unsupported, which the pull request describes as clearer than a parse error. Adapter support followed in later releases.

Failures and difficulties

Speed of delivery created its own failure modes, and the issue tracker records them. Two regular expressions used for redaction turned out to run in quadratic time on long text without whitespace; one was flagged by CodeQL, and the companion fix measured 18 to 26 seconds for 80,000 characters before it was rewritten (see the problem record). Timeline ordering pushed events without timestamps to the end, which put a Paystack webhook before the PIN and OTP steps that caused it. A shared pay_ id prefix made Checkout.com appear as an alternative for every Razorpay payment.

The documentation link checker failed on its first scheduled run with 14 Razorpay links that load from Europe but answer 404 to GitHub-hosted runners. Each of these is documented as a problem record with the fix and its validation.

Security considerations

  • The web app sends a per-request nonce Content Security Policy with connect-src 'self', object-src 'none', base-uri 'none' and frame-ancestors 'none'. The static GitHub Pages build cannot send headers, so it uses a meta-tag policy and a build check fails if any page lacks it.
  • Redaction is defense in depth, and the README says so: normalization copies only allowlisted fields, then about 330 blocked key names, 11 text patterns and Luhn-checked card numbers are redacted from everything shown or exported. Identifiers are masked in reports by default.
  • Pasted signature headers are never treated as proof; the tool does not verify webhook signatures and does not confirm settlement.
  • Keys named __proto__, constructor and prototype are removed during parsing and reported.
  • The analyzer stores no payment data. The interface does store the theme preference in the browser.
  • SECURITY.md states that the project is not PCI DSS certified and has not had an independent penetration test.

Testing approach

Tests run against source through workspace aliases. Coverage thresholds in vitest.config.ts are 90% statements, 85% branches, 90% functions and 90% lines. Rule tests include scenario, property (fast-check) and lifecycle suites, and every adapter has its own test file driven by synthetic fixtures whose identifiers contain PPLsynth.

Playwright covers the analyzer and a privacy suite marked as a release blocker: it plants marker values in the input and fails if any of them appears in a network request or if any request goes to another origin. CI runs type checks, formatting, coverage, the README provider table drift check, bundle size budgets, a CLI smoke test, the npm pack smoke test and both Playwright suites. CodeQL runs on every push and weekly.

Results with evidence

  • 40 provider adapters registered, 2 more blocked for lack of public documentation. Evidence
  • 4,900 tests passing at release 0.14.0, as reported in the release pull request. Evidence
  • Documentation link check: 937 links, 0 broken on the first CI run after the regional-host fix. Evidence
  • Published to npm as pandapaylens with provenance. Evidence
  • Static demo deployed to GitHub Pages with a post-deploy live smoke test. Evidence

Limitations

  • Only Stripe and Adyen are fully supported. The other 38 providers are limited, each with documented gaps.
  • Redaction is best effort and does not guarantee anonymization.
  • The tool does not verify webhook signatures and cannot confirm settlement or fund movement.
  • A duplicate found by identical content is reported as possible, not confirmed.
  • Stripe timestamps have one-second resolution, so events created in the same second cannot be ordered.
  • No star count, download figures or user feedback exist yet to measure adoption; the project is one day old at the time of writing.

Future work

Source code and references

Found something inaccurate? Tell me and it will be corrected with a note.

More

Theme