WDK Cashback Wallet

System Overview & Trust Model

Pay in USD₮, get 5 % back as an on-chain token.

A non-custodial multi-chain wallet plus a cashback pipeline: a customer pays a merchant in USD₮, the backend observes the payment, and 5 % of it comes back as a coupon the customer redeems for an on-chain utility token (UTL).

Backend

NestJS + TypeORM + PostgreSQL. One codebase, five processes: API, issuer, relayer, settlement watcher, monitor.

Smart contracts

Solidity / Foundry. UTL (LayerZero OFT) and CouponClaim (K-of-N issuer threshold claims) on Ethereum Sepolia.

Mobile

React Native 0.86 + WDK worklet, MobX. Google sign-in, biometrics, wallet create/restore, encrypted backup, QR scan-to-pay, real transfers, coupons and the ERC-1271 claim signature.

Integration

The contracts between the three repos: paymentRef, the EIP-712 Entitlement, shared fixtures, the REST surface.

Team

Who built what, how the work was split and reviewed, the AI tooling used, and what would come next.

Lessons learned

How we scoped what to build ourselves, and the conventions our app settled on while working against the WDK.

Known gaps

The defects we found in our own code and the hardening that did not fit the schedule — with severity, cause and the order we would fix them in.

Repositories

Directory Stack What it owns
backend/ NestJS, TypeScript (strict), TypeORM, PostgreSQL, Redis Payment detection, pricing, coupon accrual, claim state machine, attestation, relaying, reconciliation, REST API
contract/ Solidity ^0.8.24, Foundry, OpenZeppelin, LayerZero OFT UTL reward token, CouponClaim threshold claim contract, deploy script, invariant suite
mobile/ React Native 0.86.2, React 19, MobX, WDK React Native Core Non-custodial wallet UX: key generation inside the WDK worklet, biometric gate, session, encrypted backup, cashback screens

The whole path, end to end

One payment, from the till to a minted token. The dots are work in flight; step through it, or click any box.

pays USD₮ · mainnet poll · never trusted REST claim K sigs claim() · pays gas Claimed settlement recorded pause() WDK Indexer API third party · hosted Device → merchant Ethereum / Arbitrum One holds the user's keys API process poll · price · accrue 5 % no chain key Issuer × N re-verify · EIP-712 own RPC + key Relayer preflight · submit only writing key CouponClaim → UTL mint Ethereum Sepolia K-of-N signatures · per-claim & per-epoch caps Watcher reads Claimed Monitor guardian · reconciles

Click a box for what it holds and what it may write. Dashed lines are reads or out-of-band actions. Payments are watched on mainnet; rewards settle on Sepolia.

The same path as plain text
  device ──pays USD₮──▶ merchant address (mainnet: Ethereum / Arbitrum One)
                             │
   WDK Indexer API ◀─────────┘
        │  polled, never trusted
        ▼
  ┌──────────────────────────────────────────────────────────┐
  │ API process                                              │
  │  payment poller → pricing → accrual → coupon (5 %)       │
  │  REST for the app: auth, wallets, secrets, coupons,      │
  │  claims, transactions, balances                          │
  └──────────────────────────────────────────────────────────┘
        │ claim, signed once by the user's wallet (ERC-1271 / EOA)
        ▼
  ┌──────────────┐   K signatures   ┌──────────────┐  claim()  ┌─────────────┐
  │ issuer × N   │ ───────────────▶ │ relayer      │ ────────▶ │ CouponClaim │
  │ own RPC+key  │   EIP-712        │ own RPC+key  │  pays gas │  (Sepolia)  │
  └──────────────┘                  └──────────────┘           └─────────────┘
        ▲                                                            │
        │                                  Claimed event             ▼
  ┌──────────────┐                  ┌────────────────────┐     ┌──────────┐
  │ monitor      │◀── reconciles ───│ settlement watcher │     │ UTL mint │
  │ guardian key │    can pause()   └────────────────────┘     └──────────┘
  └──────────────┘

The same picture is available as an interactive diagram — click a process for what it holds and what it may write, or step through one payment from the till to a minted token. How the app's requests reach these endpoints is on the mobile page.

Two chains, on purpose. Payments are detected on mainnet (Ethereum, Arbitrum One) through the hosted WDK Indexer API. Rewards settle on Ethereum Sepolia, where CouponClaim mints UTL. Nearly every mistake on this path comes from confusing the two, so the split is spelled out everywhere it matters — including in the RPC configuration, which is a map keyed by srcChainId.

Trust model in one paragraph

Payment detection depends on a third party (the indexer), so no single process can turn an indexer answer into a mint. The issuers re-verify every payment against their own nodes and sign an EIP-712 Entitlement; the relayer re-verifies again before spending gas and is the only key allowed to write to the chain; the contract requires K distinct issuer signatures and enforces per-claim and per-epoch mint caps; the monitor reconciles minted supply against the payments that justify it and holds the pause key. Each process runs with its own key and its own RPC endpoint, and refuses to start if it shares an endpoint with another.

Process Holds May write Never
API no chain key users, wallets, coupons, claims attest, submit
Issuer (×N) one ISSUER_ROLE key attestations, claim rejection spend gas, call the indexer
Relayer the only chain-writing key claim status create attestations
Settlement watcher no key settlements, claim status anything on-chain
Monitor guardian (PAUSER_ROLE) nothing repair, attest, submit

Status at a glance

Area State Notes
Backend money path shipped 612 tests / 57 suites, 94 % statements, 95 % lines
Contracts deployed Sepolia, 136 tests across 6 suites, 100 % line/branch on src/, Slither clean
Transaction history, both directions shipped The device can only report what it broadcasts, so a wallet transfer poller ingests incoming transfers from the indexer and confirms outgoing ones, keyed UNIQUE (userId, srcChainId, txHash, outputIndex)
Mobile: auth, biometrics, wallet lifecycle, backup wired to backend /auth/google, /auth/refresh, /secrets/*. Two real backups — key in the device keychain or in Google Drive's appDataFolder, never next to the ciphertext
Mobile: balances, transfers, QR pay, coupons, claims end to end Balances and transfers from the WDK worklet; /wallets, /transactions, /coupons, /claims/*, /pricing/live consumed. 357 tests across 41 Jest suites, 90 % coverage gate on the logic layers