Paste a transaction hash or a Solana signature. The console pulls the transaction, the receipt, the block and the current head through a provider-failover proxy, decodes every stablecoin movement, and applies a written per-rail confirmation policy. It answers what the chain can actually answer on the first reply to a client: did the money move, and is it final. Whether it went to the right place needs a payment record, which is what tab 05 does. Nine rails, including the two that carry most real stablecoin payment volume and that EVM-only tooling misses.
Investigation input
A trace on its own says what happened on a chain. It cannot say whether that was the right thing to happen, because the chain has never heard of your payment record. This tab does the comparison a support engineer actually needs: expected chain, asset, amount and beneficiary from the payment record, against what settled on chain. The payment records below are fixtures I wrote; every transaction they point at is real, and the comparison runs live.
Payment records synthetic records, real transactions
| Payment | Client | Rail | Expected | Ledger status |
|---|
Why fixtures. No public source exists for another company's payment ledger, so the expected side is written by me. The cases are the ones a payments desk actually meets: a clean match, a short payment, a wrong beneficiary, a ledger that says settled while the chain says failed, a hash recorded against the wrong rail, and a payout that was never broadcast. The chain side of every comparison is fetched live.
The trace result plus the reported symptom produces a severity, an owner, and a next-action list. The point of writing this down is that severity stops being a mood. Two people triaging the same ticket at 03:00 land on the same call, and the client hears the same thing either way.
The severity matrix I would propose on day one draft, to be argued with engineering
| Sev | Trigger | First response | Client comms | Status page |
|---|---|---|---|---|
| S1 | Funds at risk, data integrity loss, or settlement stopped for all clients on a corridor. Includes any suspected duplicate debit. | Immediate page, no batching | Proactive within 15 min, then every 30 min | Post before the first client asks |
| S2 | One client or one provider route is blocked with no workaround. Payouts queued past the corridor cutoff. | Escalate within 30 min with full context | Named-channel update within 30 min, then hourly | Post if more than one client is exposed |
| S3 | Degraded but working: retries succeeding, webhook lag, slow confirmations, a workaround exists. | Support-owned, escalate if unresolved in 4h | Reply same business day | Only if sustained past one hour |
| S4 | Question, clarification, documentation gap, expected behaviour misread as a bug. | Support resolves, no ticket unless it recurs | Answer, then close the gap in the docs | No |
Per-rail confirmation policy used by the trace
| Rail | Policy | Why, and what I would tell a client |
|---|
The honest caveat. On an optimistic rollup, "confirmed" on the L2 and "final" on Ethereum are different events, hours apart. A support answer that says "final" when it means "sequencer-confirmed" is the kind of answer that becomes an incident later. The thresholds above are a defensible operating default; the real numbers belong to whoever owns treasury risk, and I would want them written down and versioned rather than living in one engineer's head.
Live health across every supported rail, measured now. Head, head age, response latency, block occupancy and fee level, plus which provider answered and whether the proxy had to fail over to reach it. This is the "stay aware of system health" half of the job, and it is the check I would run at the start of every shift rather than waiting for a client to tell me a corridor is slow.
Rail status
How to read this
| head age | Seconds since the newest block's timestamp. A rail whose head age climbs past several block times is either genuinely stalled or the provider answering us is lagging. Both matter, and they have different fixes, which is why the provider is named. |
| latency | Round trip from this Worker, including any failover attempts. Elevated latency without staleness usually means one provider is degraded and the failover is doing its job. |
| failovers | How many calls had to walk past a broken provider to get an answer. Zero is normal. Anything else is a provider to watch, and the trail underneath names it. |
| block fill | Gas used against gas limit. Sustained near 100% means congestion, which is the honest explanation for slow confirmations before anyone blames the platform. |
| down | Every configured provider for that rail refused or failed. This says nothing about the chain itself and nothing about any client's money. It is a tooling outage, and saying so precisely is most of what a good status update does. |
Real SQLite compiled to WebAssembly, running in your browser, over two datasets. Live flow pulls actual stablecoin transfers out of recent blocks on a rail you choose and loads them into a table you can query. Platform ledger is synthetic, because the payment-platform tables that matter for support (idempotency keys, webhook delivery attempts, FX quote expiry) do not exist on chain and no public source for another company's ledger exists. Both are labelled everywhere they appear.
Dataset
Pulls every block's receipts, decodes the ERC-20 Transfer events, and keeps the ones issued by a known stablecoin on that rail. Tron and Solana are excluded here because neither exposes block receipts in this shape; their trace support is on tab 01.
Synthetic, and shaped on purpose. A payment, the legs it fanned out to across providers, the webhooks owed to the client, and the FX quote it was priced against, with faults planted so the runbook queries have something to find. Provider names are neutral codes: I know the layers of the stablecoin stack, I do not know anyone's actual vendor list, and inventing one would be a lie dressed as domain knowledge.
Runbook queries
Schema
The output of triage is three pieces of writing, aimed at three different readers. Most support time is lost regenerating these from scratch and leaking internals into the client-facing one. Generated from the live trace, so the evidence is real.
Every investigation is kept, in this browser only, so the console behaves like a desk rather than a scratchpad. A real support function needs to answer "has this happened before" without asking anyone, and the second time a shape recurs is the moment it stops being a ticket and starts being a documentation gap or a monitoring gap.
Case log
Recurrence
Where this stops
This is localStorage, so it is private to this browser and it does not survive a cleared cache. That is the correct scope for a prototype and the wrong scope for a support desk, where case history has to be shared, searchable and retained to a policy. Naming that boundary matters more than pretending it is not there: the value of the pattern is what it shows about how the function should work, and the storage is the easy part to replace.
Why I built this instead of writing about it
The job description asks for someone who reads logs, constructs REST calls and reads their error codes, writes basic SQL for data investigation, stays aware of system health through dashboards and alerting, keeps Jira tickets complete enough that engineering never chases context, and owns status-page comms during incidents. Those are all verifiable. So I made them verifiable.
| Requirement | Where | What is actually happening |
|---|---|---|
| Read and interpret REST/RPC responses and error codes | 01 Trace | A Worker with a rail allowlist, ordered provider failover and edge caching: JSON-RPC to seven EVM chains and Solana, HTTP to Tron. A provider error moves on; a null result gets confirmed against a second independent provider before the console will say "not found". The provider trail prints on every result. |
| Inspect logs, trace request flows, extract context | 01 Trace | Decodes every Transfer event in an EVM or Tron receipt, and computes net per-owner balance deltas on Solana, which is closer to what a client means by "did they receive it" than an instruction list. |
| Decide whether an issue is ours or the client's | 02 Reconcile | Compares the live trace against a payment record field by field: rail, asset, amount, beneficiary, ledger status. Catches a ledger that says settled against a transaction that failed, a payout to the wrong address, a short amount, and a hash recorded against the wrong rail, which it confirms by probing the other rails. |
| Know what genuinely needs escalation | 03 Triage | A written severity matrix and a per-rail confirmation policy, applied deterministically to the live result rather than typed by hand. |
| Stay aware of system health through dashboards and alerting | 04 Rail health | Live head, head age, latency, block occupancy, fee level and failover count for all nine rails, refreshable on a timer. Not a mock: rails do go down while you watch it. |
| Read and write basic SQL | 05 Ledger | SQLite in WebAssembly over real stablecoin transfers pulled from recent blocks, plus a synthetic platform ledger for the questions that only exist inside a payments platform. |
| Client comms, status page, complete Jira context | 06 Handoff | Three drafts for three readers, every number lifted from the trace. The status-page block also shows when the correct decision is not to post, and why. |
| Verify AI output before it reaches a client | throughout | Nothing in the client-facing draft is generated prose. Every number in it comes from the trace, and anything unknown prints as unknown rather than being filled in. |
What review changed
I had a second model review the source cold, told to be blunt and skip praise. It found a real design lie and several defects. Every one below is fixed in what you are looking at.
| What was wrong | What it does now |
|---|---|
| It claimed to answer "is this ours or theirs" from a trace alone | A chain can prove a transfer happened and is final. It can never know who was owed what. That claim moved to 02 Reconcile, which compares the trace against a payment record, and the trace tab now only claims what a chain can support. |
One provider's null was treated as proof of absence | A provider that has not indexed a transaction yet returns the same null as one that never existed. Saying "not found" on that would tell a client their money was gone. A null is now confirmed against a second independent provider, and if that one returns data, the answer flips. |
| Solana deltas were keyed by token account, not owner | One owner can hold several token accounts for the same mint, so a single net movement could render as separate debits and credits. Now keyed by owner, which is what the label always claimed. |
| The Solana client draft could quote the debit | Every Solana movement appears twice. Taking the first stablecoin entry could tell a client the money went to the sender. Client-facing amounts and counterparties now come only from the credit side. |
| Confirmations were computed from an independently cached head | A head older than the block produced a wrong, sometimes negative, count. That is provider lag, so it now says so instead of printing a number. |
| "Read-only" SQL was a block-list | It missed REPLACE, VACUUM, REINDEX and chained statements, so read-only was a claim rather than a guarantee. Now an allow-list: one SELECT or WITH, nothing after a semicolon. |
| Live amounts overflowed a double | An 18-decimal BSC USDT transfer exceeds 2^53 in base units, so Number(BigInt(raw)) lost precision. Scaling now happens in string space. |
| Tron had one provider while the console sold failover | Three now. Tron carries the most USDT volume, so it was the worst rail to have no second route on. |
Why this table is on the page. Anyone can ship a demo that works on the paths they chose to test. Showing what an adversarial reader broke, and what changed, is a better signal about how someone handles a bug report than a clean screenshot is.
What this is not, and what I would build next
Not. There is no ticketing backend, no auth, no server-side persistence and no alerting. The case log is this browser's localStorage. The platform ledger is synthetic and says so everywhere it appears. It is an independent prototype built for one application and it touches nothing belonging to anyone else.
Next, in order. Address-level history, because the question after "did this transaction land" is always "then where did the money go". Flow loading for Tron and Solana, which need a different shape from EVM block receipts. Then replacing the synthetic ledger with a read replica, and turning the rail checks into actual alerts with a threshold and a destination rather than a page someone has to look at. The gap between this and a real support desk is integration and ownership, not concept, and both are things I would rather do against the real systems than guess at.
Built by
- who
- Edward Tay, Malaysia. Technical support and customer success on crypto infrastructure since 2021: Aztec, BOB (a Bitcoin L2), deBridge, FrodoBots.
- stack
- Cloudflare Worker (static assets, a rail-allowlisted RPC proxy with failover and edge caching, and a normalising trace endpoint across three chain families), vanilla JS, sql.js. No framework, no build step.
- data
- Public RPC endpoints only: publicnode, drpc, meowrpc, 1rpc, Blast, official chain endpoints, TronGrid. No API keys, so anyone can verify what it does.
- links
- edwardtay.com · github.com/edwardtay · dune.com/edwardtay