This gateway really tries to cheat. It mints real signed invoices, then skims, swaps the recipient, or fakes the payment. Run it and watch your SDK refuse each one.
Most demos mock the attack. This one is the real Rust gateway, built with a cheat-monkey flag. It signs genuine BOLT11 invoices and then breaks exactly one thing. The SDK runs the same checks it runs in your app, from the invoices alone. Every response also names the cheat it tried, so you can hold the gateway's confession next to the SDK's independent verdict.
The SDK verdict comes from decoding the invoices, never from the cheat the gateway declared.
| Cheat | What the gateway does | SDK verdict |
|---|---|---|
| honest | everything correct, the control case | verified |
| hashmismatch | recipient invoice carries a different payment hash | hash_mismatch |
| skim | recipient invoice is for less than the donation | amount_skim |
| overcharge | donor invoice charges far above the donation | amount_overcharge |
| testnet | recipient invoice is a worthless testnet one reusing the hash | network_mismatch |
| custodial | takes custody despite require_trustless | custodial |
| fakesuccess | claims success with a preimage that does not hash to H | preimage_mismatch |
| substitute | recipient invoice signed by a different node | recipient_mismatch |
import { ThunderBridge, GatewayCheatError } from "thunder-bridge";
// Point the SDK at the cheating endpoint. Every response also declares the
// cheat it tried; the SDK ignores that and decodes the invoices itself.
const tb = new ThunderBridge("https://thunder-bridge.agora.gripe/cheat-monkey");
try {
// recipientNodeId is the recipient's node id, known out of band. The SDK
// pins it, so a substituted recipient throws instead of slipping through.
await tb.createDonation({
destination: "anon@chaos.test",
amountSat: 21,
recipientNodeId: "02989c0b...5f6f",
});
} catch (e) {
if (e instanceof GatewayCheatError) {
// e.code: amount_skim, recipient_mismatch, preimage_mismatch, ...
}
}