MySteadyAid
Transparent disaster-relief distribution
Created on 5th September 2026
•
MySteadyAid
Transparent disaster-relief distribution
The problem MySteadyAid solves
Malaysia experiences severe, recurring monsoon flooding nearly every year. When it happens, financial aid needs to move from donors and government budgets to affected households — and today, that process is slow and opaque in ways that are well understood but not well solved:
- Bureaucratic delay across multiple layers. Aid moves through several administrative layers before it reaches a household, and each layer adds processing time — real households can wait weeks for funds they need immediately.
- A gap between PPS and non-PPS victims. People sheltering in official evacuation centres (Pusat Pemindahan Sementara) are generally easier to register and track. People who don't evacuate to a PPS — staying with relatives, in a partially damaged home, or elsewhere — often fall into a slower, less-defined process.
- Opacity in fund flow. Donors and the public generally cannot see, in any verifiable way, that a specific donation actually reached a specific affected household. Trust is asked for, not demonstrated.
- Duplicate and "ghost" claims. Without a hard, systemic guarantee that one household can only be paid once, duplicate or fraudulent claims are a structural risk, not just a hypothetical one.
- Connectivity loss. The disasters this system is meant to help with often damage the same telecom infrastructure people would need to access a digital wallet or app — a real design constraint, not an edge case.
- Digital literacy and accessibility. Elderly and digitally illiterate residents — a real and significant share of affected populations — struggle with typical crypto wallet UX (seed phrases, gas fees, unfamiliar apps).
- No independent check that a declared disaster zone is real. Publishing the fund flow on-chain proves money moved correctly once a zone exists — but a donor still has to take the relief authority's word for it that a given "disaster zone" corresponds to a real, ongoing event. A convincingly-named fake zone, or a real zone whose severity is inflated, is a scam vector transparency of fund flow alone doesn't close.
Challenges we ran into
zkLogin: "Groth16 proof verify failed" at the final on-chain step
Our zkLogin flow completes every off-chain step correctly — Google OAuth, nonce binding, server-side JWT verification, deterministic salt derivation, and the Mysten prover call all succeed — but the assembled zkLogin signature is rejected on-chain with Groth16 proof verify failed when it reaches a validator. We ruled out ephemeral-key staleness (it reproduced on a completely fresh login), re-checked the address-seed and maxEpochassembly against the Sui docs line by line, and confirmed the prover response shape was correct — but couldn't isolate the root cause within the hackathon window.
Rather than fake it, we kept the full zkLogin implementation in the codebase and demo the identical downstream flow (sponsored transaction → claim_and_link → payout) using a standard Ed25519Keypair signer, which shares the exact same Signer abstraction. The zkLogin design and its entire off-chain half are real and working; only the final proof verification is unresolved, and we document that openly rather than leaving it hidden.
Sui SDK v2 and the sponsored-transaction dual-signature flow
The @mysten/sui v2 SDK replaced SuiClient with SuiGrpcClient and changed many method shapes, so our early integration code matched no tutorial. The sharpest single bug: role resolution ("which capability objects does this address own?") silently returned nothing because the gRPC listOwnedObjects type filter needs a fully-qualified datatype (0xPKG::relief_v3::RegistrarCap), not the 0xPKG::relief_v3 prefix the older API accepted.
Sponsored (gasless) transactions also took real iteration: the sponsor and the user must sign the exact same transaction bytes, the sponsor's coin must be pinned as gas payment before the transaction is built, and both signatures are submitted together. We settled on a clean three-step split — server builds + pre-signs as sponsor → browser signs the same bytes with the user's key → server executes with both signatures — which is now the single path every end-user action flows through.
Making multi-model AI consensus survive real model failures
Two things broke the first working version of our 3-model zone-credibility check:
MiniMaxAI/MiniMax-M2.7is a reasoning model that emits a<think>…</think>preamble before its JSON answer. Our 400-token budget was consumed entirely by that preamble, so the route failed withNo JSON found in model output. Raising the budget to 1200 tokens fixed it.moonshotai/Kimi-K2.6is listed by GonkaRouter's/modelsendpoint but rejected by/chat/completionswithunsupported model. Because we'd built the consensus call onPromise.allSettledwith a persisted row per model (including failures), this upstream inconsistency degraded gracefully to a clean "2 of 3 models responded" instead of taking down the whole check. The partial-failure handling we designed for turned out to be load-bearing in practice, not just theory.
Supabase mirror drifting out of sync after a rejected registration
Our database is a rebuildable read-mirror of chain state, kept current by an event indexer. A verifier rejecting a registration and the household re-registering is a valid flow (the contract releases the household-ID lock on rejection), but it creates a second on-chain object with the same household_id. Our mirror had a naive UNIQUE(household_id) index, so the indexer's upsert of the new row failed silently (logged, not surfaced), the new reference code was never mirrored, and lookups 404'd. We replaced it with a partial unique index (WHERE status <> 2, i.e. not-Rejected) that mirrors the contract's own semantics exactly.
Server-side evidence extraction drowning in page chrome
For the credibility checker, a URL's page text is fetched and stripped to plain text
server-side (no headless browser). Testing against a real Wikipedia article showed the first ~2,600 characters of "extracted text" were navigation, sidebar, and table-of-contents markup — which would have consumed our 6,000-char cap before reaching any real content. We added block-level stripping of <nav> / <header> / <footer> / <aside> / <table> before the generic tag strip. We also tried a regex to remove specific <div id="toc"> wrappers and deliberately removed it — regex can't balance nested tags, so it was a correctness risk pretending to be a fix.
Tracks Applied (3)
PAYMENTS & STABLECOINS
Sui Foundation
AI × SUI
Sui Foundation
AI For Society
Gonka Router
