#!/usr/bin/env python3 """ verify_payment.py -- the one thing I actually sell, in a form that cannot die. THE PROBLEM THIS SOLVES ----------------------- Everything I have advertised so far required a live server: a trycloudflare origin, a tunnel, a heartbeat. Those are all ephemeral. I have generated 84 distinct origin URLs and every one of them now returns 000. A catalog that points at a dead URL is not a product with an expiry date -- it is a lie with a countdown. And it is the *only* kind of lie I was building toward: one that makes me look available when I am not. So the offer is inverted. Instead of "pay me and I will confirm," this file IS the confirmation. The buyer runs it on their own machine. It talks to public Base nodes directly. It never contacts me, so it is trivially impossible for me to lie about the result, and it keeps working after I am gone -- which is the only property of my entire business that is actually under my control. $ python3 verify_payment.py 0xabc... # did a payment reach me? $ python3 verify_payment.py --watch # poll for one, forever $ python3 verify_payment.py --self # what is my address, exit code? EXIT CODES (this is the contract; do not change it) 0 a USDC transfer to the seller's address was found in a successful tx 1 the transaction succeeded but no USDC reached the seller 2 no receipt: unknown hash, or not yet mined 3 the transaction was REVERTED -- logs are meaningless, no money moved 4 bad input 5 no public Base node could be reached (NOT a statement about payment) Exit 5 is deliberately distinct. "I could not check" and "you did not pay" are different facts, and collapsing them is how a verifier becomes an instrument for telling a buyer what the verifier's author wants them to hear. WHY IT IS SAFE TO RUN STRANGER CODE ----------------------------------- Read it. It is 300 lines, uses only the standard library, and the only network calls it makes are POSTs to public Base RPC endpoints. There is no wallet, no key, no signing, no file writes, no subprocess. The trust you need is much smaller than the trust "run this payment verifier from a stranger" sounds like. WHY 0x0E9F19e059A2f04D1e8330DB746a2aE60E3F77e4 IS THE ONLY ADDRESS IT ACCEPTS --------------------------------------------------------------------------- Because that is the only address that pays me. A verifier that accepts any address is a verifier that confirms nothing. """ import sys, json, time, urllib.request, urllib.error, argparse, os # The address money must reach. Not a default -- the entire subject of the check. SELLER = "0x0E9F19e059A2f04D1e8330DB746a2aE60E3F77e4" # Compared in lowercase: log topics are lowercase hex, SELLER is checksummed. # Comparing them raw is a silent, total failure -- every real payment reads # as NOT PAID. Keep SELLER as the display form; use SELLER_LC to match. SELLER_LC = SELLER.lower() USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # Base native USDC USDC_LC = USDC.lower() # chain logs are lowercase; see SELLER_LC above TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" NODES = [ "https://mainnet.base.org", "https://base-rpc.publicnode.com", "https://base.llamarpc.com", "https://base.drpc.org", "https://1rpc.io/base", "https://base.meowrpc.com", ] EXIT_OK, EXIT_NOT_PAID, EXIT_NO_RECEIPT = 0, 1, 2 EXIT_REVERTED, EXIT_BAD_INPUT, EXIT_NO_NODE = 3, 4, 5 # ---------------------------------------------------------------- plumbing -- def rpc(body, timeout=20): """POST JSON-RPC to every node until one answers. Returns (result, node) or raises RuntimeError listing why each one failed.""" why = [] for node in NODES: try: # A User-Agent is not optional here. Cloudflare answers the default # Python-urllib UA with "error code: 1010" (bot integrity block), so # a perfectly healthy network looked like a dead one -- and my # verifier reported "COULD NOT CHECK" about six working public nodes. # An instrument that blames the network for its own missing header # is worse than no instrument: it looks like caution. req = urllib.request.Request( node, data=json.dumps(body).encode(), headers={"content-type": "application/json", "user-agent": "verify_payment/1.0 (+stdlib; Base receipt checker)", "accept": "application/json"}, method="POST") with urllib.request.urlopen(req, timeout=timeout) as r: payload = json.loads(r.read().decode()) if "error" in payload: why.append("%s(%s)" % (host(node), str(payload["error"])[:40])) continue return payload.get("result"), node except Exception as e: why.append("%s(%s)" % (host(node), type(e).__name__)) raise RuntimeError("; ".join(why)) def host(u): return u.split("/")[2] def topic_addr(topic): """A 32-byte log topic whose last 20 bytes are an address.""" return "0x" + topic[-40:].lower() def fmt_units(raw): """USDC has 6 decimals. Integer math only -- float money is a bug.""" v = int(raw, 16) return "%d.%06d" % (v // 10**6, v % 10**6) def valid_hash(h): h = (h or "").strip() return h.startswith("0x") and len(h) == 66 and \ all(c in "0123456789abcdefABCDEF" for c in h[2:]) # ------------------------------------------------------------------- check -- def check(tx_hash): """Return (exit_code, report_lines). Read this function: it is the whole product, and it is the only place a lie could be told.""" if not valid_hash(tx_hash): return EXIT_BAD_INPUT, ["BAD INPUT: expected 0x followed by 64 hex chars.", " got: %r" % (tx_hash,)] try: rc, node = rpc({"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionReceipt", "params": [tx_hash]}) except RuntimeError as e: return EXIT_NO_NODE, [ "COULD NOT CHECK -- this is not a statement about payment.", " no public Base node answered: %s" % e, " retry later, or run it yourself: curl -s -X POST -H 'content-type: " "application/json' --data '{\"jsonrpc\":\"2.0\",\"id\":1," "\"method\":\"eth_getTransactionReceipt\",\"params\":[\"%s\"]}' " "https://mainnet.base.org" % tx_hash] if not rc: return EXIT_NO_RECEIPT, ["NO RECEIPT: unknown hash, or not yet mined.", " tx %s" % tx_hash, " node %s" % node] out = ["tx %s" % tx_hash, "block %s" % int(rc.get("blockNumber", "0x0"), 16), "node %s" % node, "miner %s" % "yes" if rc.get("blockNumber") else "no"] # THE CHECK EVERY RECEIPT-READER GETS WRONG. A reverted transaction still # returns a receipt, and that receipt still contains the Transfer log it # emitted before dying. Reading those logs as payment tells a buyer their # money arrived when it was unwound. This single line is the difference # between a verifier and a marketing page. if rc.get("status") != "0x1": out += ["status FAILED (reverted) -- logs below prove nothing", "", "RESULT: THE TRANSACTION FAILED AND WAS UNWOUND.", "No money moved. Do not credit this transaction."] return EXIT_REVERTED, out mine, other = [], [] for log in rc.get("logs") or []: if (log.get("address") or "").lower() != USDC_LC: continue # not USDC: not payment topics = log.get("topics") or [] if len(topics) < 3 or topics[0] != TRANSFER_TOPIC: continue frm, to, amt = topic_addr(topics[1]), topic_addr(topics[2]), \ fmt_units(log.get("data", "0x0")) (mine if to == SELLER_LC else other).append((frm, to, amt)) for frm, to, amt in mine + other: out.append(" USDC %14s %s -> %s%s" % (amt, frm[:10] + ".." + frm[-6:], to[:10] + ".." + to[-6:], " <== SELLER" if to == SELLER_LC else "")) out.append("") out.append("seller %s" % SELLER) out.append("total %s USDC received" % ("%.6f" % sum(float(a) for _, _, a in mine) if mine else "0.000000")) out.append("") if mine: out += ["RESULT: PAID. A USDC transfer reached the seller in a successful", "transaction. Amounts above are read from the Transfer logs; you", "can confirm independently with any Base block explorer."] return EXIT_OK, out if other: out += ["RESULT: NOT PAID TO THE SELLER.", "The transaction succeeded and moved USDC, but none of it went", "to the address above. Crediting it is how people get scammed."] return EXIT_NOT_PAID, out out += ["RESULT: NOT PAID. The transaction succeeded but contains no USDC", "transfer at all (different token, or no transfer)."] return EXIT_NOT_PAID, out def head_watch(interval=12, once=False): """Poll for ANY inbound USDC transfer to the seller. Used by the seller, but it is also the honest way for a buyer to watch their own payment land.""" seen, last = set(), 0 while True: try: blk, _ = rpc({"jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": []}) n = int(blk, 16) except RuntimeError as e: print("waiting for a Base node... (%s)" % e); time.sleep(interval); continue for b in range(last, max(last - 1, n - 6)): try: d, _ = rpc({"jsonrpc": "2.0", "id": 1, "method": "eth_getBlockByNumber", "params": ["0x%x" % b, True]}) except RuntimeError: continue for tx in (d or {}).get("transactions") or []: h = tx.get("hash") if not h or h in seen: continue seen.add(h) code, lines = check(h) if code == EXIT_OK: print("\n".join(lines)); return EXIT_OK last = n print(" scanned to block %d -- no new payment to seller" % n, flush=True) if once: return EXIT_NOT_PAID time.sleep(interval) def main(): ap = argparse.ArgumentParser(add_help=True, description=__doc__.split("THE PROBLEM")[0]) ap.add_argument("tx", nargs="?", help="Base tx hash (0x + 64 hex)") ap.add_argument("--self", action="store_true", help="print the seller address") ap.add_argument("--watch", action="store_true", help="poll for any payment") ap.add_argument("--interval", type=int, default=12) ap.add_argument("--once", action="store_true", help="with --watch: one sweep") a = ap.parse_args() if a.self: print(SELLER); return 0 if a.watch: return head_watch(a.interval, a.once) if not a.tx: ap.print_help(); return EXIT_BAD_INPUT # A pasted tx may be a URL from a block explorer. Take the hash out of it # rather than telling a paying customer their own link is malformed. t = a.tx.strip() if "0x" in t and len(t) > 66: cands = [w.strip(",)'\" ") for w in t.replace("/", " ").split()] t = next((c for c in cands if valid_hash(c)), t) code, lines = check(t) print("\n".join(lines)) return code if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: sys.exit(130)