12 / 12Prism Systems · FinTech & Banking
Payment routing that learns, fails over and never charges twice
A smart payment router for UPI, cards, netbanking and wallets. It scores five PSPs with decayed Bayesian success estimates and Thompson sampling, weighs fees and latency, trips per-PSP circuit breakers, detects bank-wide outages, fails over only on soft declines, and runs a status inquiry before retrying a timeout. Around it sit a compliance gate, an FX desk with DCC, and a chargeback state machine.
Five PSPs, live traffic, and outages you can cause
About 14 payments a second flow from UPI, cards, netbanking and wallets through the smart router to five fictional PSPs. Each one is also replayed through four baseline routers against the same simulated network, so the comparison is fair. Take a PSP down or slow it, knock out a bank, or launch a card-testing attack, then watch the breakers, the failovers and the auth-rate gap. You can also answer chargebacks yourself.
What's inside the engine
Real algorithms, not mock-ups. The simulator above runs the exact same code you import.
Learns success rates
Success and failure counts per PSP × method × issuer decay with a 2-minute half-life and borrow a prior from the PSP × method level, so a new combination starts sensible and an old outage is forgotten.
Thompson sampling
The router ranks PSPs on a sample from each posterior rather than the mean. Uncertain PSPs get some traffic, and well-known bad ones get almost none.
Cost and latency aware
Score = sampled success − fee ratio − latency penalty, with tunable weights. Fees include amex surcharges and cross-border uplift, all in exact integer paise.
Circuit breakers
A rolling 30 s window per PSP × method. When failures pass 50% over at least 12 calls the breaker opens. After the cooldown, a few probes decide whether it closes or reopens with a doubled cooldown.
Issuer outage detection
If the failure code says the bank is down, the PSP isn't blamed. A separate issuer breaker limits attempts to one and suggests another payment method instead of retrying.
Safe failover
Only soft declines are retried, within an attempt limit and a 15 s budget. On a timeout the router asks the PSP for the payment status first, and if it was captured it stops. That is why the smart router never double-charges.
Compliance gate
Checks for sanctions, Luhn and BIN, card-on-file tokenisation, 3DS/AFA step-up, e-mandate and UPI limits, data localisation for domestic payments, and per-instrument velocity. Each block returns a code and a reason.
FX, DCC and disputes
Firm FX quotes with markup and expiry, a DCC versus card-network comparison, and a chargeback state machine with deadlines, evidence scoring, pre-arbitration and 3DS liability shift.
From request to result
Normalise
Validate the method, currency and amount (exact minor units), look up the BIN, and redact the card number so it is never stored.
Gate
Compliance blocks, asks for 3DS/AFA, or restricts the payment to PSPs that store data in India.
Rank
Eligible PSPs are scored from a Thompson sample minus fee and latency. Breaker state and merchant rules are applied, and every row records why it was ranked or excluded.
Attempt
Try the best PSP. Success ends it, a hard decline ends it, and a soft decline fails over. A timeout triggers a status inquiry first.
Learn
Only technical outcomes update the estimates and breakers. Issuer-wide failures feed the issuer breaker, and hard declines teach nothing about the PSP.
System design
Every box is a module in src/. Data flows left to right.
Ship it in your own stack
Zero dependencies. Works in Node.js 18+ and modern browsers.
import { Gateway, Router, Network, Platform } from "prism-payroute";
const gw = new Gateway({ router: new Router({ strategy: "smart", costWeight: 1, latencyWeight: 0.3 }) });
// Dry run: compliance + every PSP's score, or why it was excluded
gw.route({ method: "card", amount: "2499.00", threeDS: true, card: { pan: "4111111111111111" } });
// Real attempt with idempotency: same key + same body replays, a different body is a 409
const r = gw.pay({ method: "upi", amount: "499", vpa: "asha@oksahy" }, "order-1042");
r.status; // "captured" | "declined" | "failed" | "blocked" | "requires_action"
r.attempts; // [{ psp, n, code, latency, inquiry? }]
// Compare routers on identical traffic and incidents
const P = new Platform({ seed: 12 });
P.run(60); P.outage("neorail", { secs: 60 }); P.run(60);
P.report().strategies.smart.authRate; // vs .cheapest, .static, .round-robin, .naive-retry
gw.fx.quote(12000, "USD", "INR"); // firm quote with markup and expiry
gw.disputes.act(id, "represent", { evidence: ["delivery", "comms"] });$ npx github:krishyaa06/prism-payroute demo
$ npx github:krishyaa06/prism-payroute simulate --scenario mixed --seconds 180
$ npx github:krishyaa06/prism-payroute route --method card --issuer DECN --amount 4500
$ npx github:krishyaa06/prism-payroute fx --from USD --to INR --amount 120
$ npx github:krishyaa06/prism-payroute dispute
$ npx github:krishyaa06/prism-payroute serve --port 8790# use the CLI without installing
$ npx github:krishyaa06/prism-payroute --help
# or add the engine to your project
$ npm install github:krishyaa06/prism-payroute
# run the simulator locally
$ git clone https://github.com/krishyaa06/prism-payroute.git && cd prism-payroute
$ npx serve . # then open http://localhost:3000