01
Why a demo and not a README
The starter kits next door explain how a PayFast integration should be built. They explain it well, and they explain it to somebody who has already decided to read Java. That is a narrow door for work whose whole argument is that this domain is more subtle than it looks.
So this is the same knowledge with the door taken off. Four showcases — the merchant application, what actually gets signed, what happens when the webhook does not arrive, and a dashboard over the payments that did — each one openable by a stranger with no signup, no sandbox account and no tunnel. It is a demo shell rather than a payment processor: it never touches card data and never embeds a provider page.
The interesting constraint follows from that. A demo anybody can open is a demo whose claims anybody can check, which rules out the comfortable option of describing behaviour the code does not have.
02
Money is integers, all the way down
Nothing in the ledger holds an amount as a floating-point number. Every value is integer cents from the moment it is generated to the moment it is formatted, and formatting happens once, at the edge.
This is not fastidiousness. A dashboard whose total is out by a cent because 0.1 + 0.2 does not equal 0.3 is the one domain error that a reviewer who actually works in payments spots immediately — and the place they spot it is the summary, several steps from the row that caused it. Choosing integers is cheap at the start and expensive to retrofit, which is roughly the shape of every decision worth writing a case study about.
It also makes the totals assertable. When the unit of account is an integer, the sum of a filtered set is a fact a test can pin exactly, rather than a value that has to be compared within a tolerance nobody chose deliberately.
03
The hash, written out
PayFast's ITN signature is defined as an MD5. MD5 has been collision-broken on ordinary hardware since 2004 and must not be chosen for anything needing collision resistance — but it was not chosen here. It is a protocol constant, and a tool that traces what a provider signs has to reproduce what the provider actually does.
The browser will not help. Web Crypto offers SHA and deliberately omits MD5, so the options were a dependency or writing the arithmetic out. Writing it out won, pinned to the published test vectors, and the reason is the same one that governs the rest of the site: a hashing dependency is a script this page does not control, sitting directly under the field where somebody has been invited to paste a passphrase.
The tracer's actual contribution is not the digest, though. Plenty of tools will tell you a signature is invalid. This one names which documented mistake the sender made — sorting the fields, encoding to RFC 3986 instead of PHP's older scheme, lowercasing the hex, signing without a passphrase the receiver expects — because "invalid" is where a debugging session starts rather than where it ends.
Two claims about that are worth separating, because they are not equally strong. A correct signature is checked against a captured ITN whose digest PayFast's own server produced, and the Java implementation next door is anchored to the same bytes, so the two agree with a recorded reality rather than with each other. The wrong ones are not: every deliberately-broken string in those tests is written out by hand and hashed, because no provider ever sends you a canonical example of a mistake. That is a real limit and it is written down as one rather than rounded up.
04
Charts as numbers rather than a library
The dashboard draws its own charts, and the module that does it produces no markup at all. It turns a series into coordinates; the component draws them. That split is the whole point of writing it rather than installing it.
What it buys is that the interesting claims about a chart become assertions over plain numbers. A zero sits on the baseline. The tallest point reaches the top. An empty series does not divide by zero. A missing value breaks the line rather than quietly dropping it to the floor and inventing a crash that never happened. None of those is a snapshot of a rendered tree, so none of them fails because a library changed a transform.
There is a discipline attached that is easy to skip. When the dashboard stopped showing bars, the bar-drawing function was deleted rather than kept in case it came back — in a codebase whose argument is that the geometry is small enough to own, a tested function with no caller is exactly as much of a liability as an untested one with a caller.
05
Determinism, which turns out to be a rendering concern
Nothing in the ledger reads the system clock or an unseeded random source. Every timestamp is an offset from a fixed epoch and every choice comes out of a seeded generator.
The obvious benefit is reproducible tests. The one I did not anticipate is that it is what makes the page render at all. These showcases are islands: the server renders the initial state and the browser then hydrates over that same markup. A single Date.now() inside the generator produces one answer on the server and a different one in the browser a moment later, and React reports a hydration mismatch on a page that looks completely fine in every screenshot anybody took of it.
So a decision filed under testing turned out to be load-bearing for correctness in the browser. That is worth saying plainly because the two concerns are usually discussed by different people.
06
What it is not
It is not the sandbox. A real transaction path against PayFast's sandbox exists and is linked from the starter kits' write-up; this site is the shell that explains it, not a second implementation of it.
The signature rule is written twice, though — once in Java in the kits and once in TypeScript here — and that is a decision rather than an oversight. A browser tool that traces what gets signed cannot call a Java library, and the alternative was to make the reader sign up for something. What keeps two copies honest is that neither is the reference: both assert against the same captured ITN bytes, and neither fixture may ever be regenerated from either implementation, which would turn an anchor into the two of them agreeing with themselves. A checker compares them.
What is deliberately not reimplemented is the other rule. PayFast signs the payment request and the ITN differently — field order, empty values and trimming all differ — and their documentation presents the two pages apart without ever saying so. Getting one right and reusing it for the other is the failure worth knowing about: checkout works, every ITN is rejected as forged, and the payments quietly never complete. Only the ITN rule is here, because the other one would be a copy with no caller.
It is not a payment processor, it holds no real money, and the ledger behind the dashboard is invented. That last one carries a rule worth keeping: the seeded data is permanently labelled as demo data and there is no way to dismiss the label into looking live. A demo that can be mistaken for production volume teaches the opposite of what it exists to teach.
