A self-custodial, multi-chain web wallet built on Tether's
Wallet Development Kit. It is the
production-grade web counterpart to the official
wdk-starter-react-native,
which Tether ships only for React Native.
Bitcoin and USD₮ both work, on the web, client-side. Create or import a seed, unlock with a passkey or passphrase, then send and receive real BTC and USD₮ (plus XAUT). Key material never leaves the browser, the WDK signer runs in a dedicated Web Worker, and there is nothing custodial in between.
Recorded end-to-end against the real built app. The BTC row is served by a local, offline Electrum-WS fixture (no endpoint, no secret); the addresses and keys are real client-side derivation. Regenerate locally:
corepack pnpm demo(one-timecorepack pnpm exec playwright install chromium, plusffmpegon PATH).
Live demo: https://wdk-wallet-web-production.up.railway.app — the real built app, served under the same strict per-request-nonce CSP and security headers as production. It boots with zero config, so it runs on its five keyless default chains (Ethereum, Polygon, Arbitrum, Plasma + Solana); BTC surfaces the honest "unsupported chain" notice until an Electrum-WS endpoint is configured, exactly as described below. Your seed is generated and encrypted in your own browser — the deploy holds no keys and nothing custodial.
pnpm install
pnpm --filter next dev # → http://localhost:3000
That boots the full wallet on its five keyless default chains with zero config (ETH + USD₮ / XAUT on Ethereum, USD₮ on Polygon / Arbitrum / Plasma / Solana). To enable Bitcoin, point it at an Electrum-over-WebSocket endpoint:
cp apps/next/.env.example apps/next/.env.local
# set NEXT_PUBLIC_BTC_ELECTRUM_WS_URL=wss://<your-electrum-host>:50004
pnpm --filter next dev
No endpoint set → the wallet runs on its five keyless default chains (Ethereum, Polygon, Arbitrum, Plasma + Solana) and BTC surfaces a typed, honest "unsupported chain" error instead of failing silently.
| Bounty ask | Status |
|---|---|
| Send / receive USD₮ on web | ✅ shipped — USDT on Ethereum, Polygon, Arbitrum & Plasma (WDK EVM manager) + Solana (WDK Solana manager) + XAU₮ on Ethereum |
| Send / receive BTC on web | ✅ shipped — pure-JS WDK BTC manager + injected Electrum-WS client, in the worker |
| Self-custodial, keys client-side | ✅ WebCrypto vault + Web Worker signer (ADR-004) |
| Unlock | ✅ WebAuthn passkey (PRF) with a PBKDF2 passphrase fallback (ADR-005) |
| Multi-wallet / multi-account | ✅ N independent BIP-39 seeds, each with HD accounts; zero-migration back-compat |
| QR | ✅ scan a BIP-21/EIP-681 payment URI into the recipient field; render the receive address as a QR |
| Reusable across hosts | ✅ headless core consumed byte-unchanged by a second app (Svelte) |
Beyond the BTC + USD₮ ask, the wallet ships four EVM networks
(Ethereum, Polygon, Arbitrum, Plasma — via the WDK EVM manager) plus
Solana (USD₮ as an SPL token, via @tetherto/wdk-wallet-solana through
the same adapter seam) plus BTC — all five non-BTC chains are
default-on with keyless public RPC. Honest CI bound: only the Ethereum
and BTC (fixture) flows are exercised end-to-end in the demo / CI; the
Solana, Polygon, Arbitrum and Plasma managers are wired, typed, built and
config + portfolio covered, but their live-RPC send/receive is not part of
CI. Lightning / Spark are not shipped — reachable on the same adapter
shape, deliberately left as documented extension points, not claimed as
done.
The one honest operational dependency: a browser cannot open a raw Electrum TCP
socket, so BTC needs a public Electrum-WS endpoint to point at (env-driven,
failover via @tetherto/wdk-failover-provider). That is a real deployment
input, not a missing feature — see docs/RN-TO-WEB-MAP.md →
"Bitcoin on web (shipped)".
This is not "create-next-app + paste the WDK quickstart". It mirrors the architecture of Tether's own RN starter, which cleanly separates platform-agnostic wallet logic from platform-specific UI / storage:
packages/wallet-core — a headless, fully-typed, tested WDK wallet engine
(orchestration, encrypted key vault, chains / failover config, balances, send,
receive, activity). Zero UI. Zero framework lock-in.apps/next — the reference Next.js app: full screen parity with the RN
starter (onboarding → wallet-setup → unlock → portfolio → token detail → send
→ receive → activity → settings).apps/svelte (package svelte-proof) — a Svelte 5 + Vite app that runs
the core's state machine against the byte-unchanged engine, proving
wallet-core is genuinely framework-agnostic, not Next-coupled. Ships with a
headless portability test (test/portability.test.ts).The headless core is reusable verbatim for a browser-extension wallet and an eCommerce checkout (the other two Tether WDK bounties).
packages/wallet-core/ headless WDK engine (the spine)
apps/next/ reference web wallet (the deliverable)
apps/svelte/ portability proof (Svelte 5 + Vite; pkg svelte-proof)
docs/
ARCHITECTURE.md module boundaries, data flow, ADRs
BOUNTY-CHECKLIST.md reviewer map for the Tether WDK bounty
BOUNTY-IMPLEMENTATION-PLAN.md
ordered roadmap for future bounty polish agents
SECURITY.md threat model & honest limits
SECURITY-REVIEW.md structured review: CSP rationale, secrets lifecycle, audit advisory
RN-TO-WEB-MAP.md every RN platform API → its web replacement
.github/workflows/ci.yml lint · typecheck · test · build
pnpm install
corepack pnpm verify # lint, typecheck, test, build (all 3 packages)
corepack pnpm smoke # E2E: create → seed quiz → portfolio → receive a11y → Recovery Check
corepack pnpm demo # records docs/demo.gif (one-time: playwright install chromium)
corepack pnpm audit --audit-level moderate # one accepted low advisory (docs/SECURITY-REVIEW.md §7)
corepack pnpm --filter next dev
corepack pnpm smoke builds the production app, serves it on a free port, and
drives a real browser through the reviewer walkthrough under the live strict
CSP — a passing run is also proof of zero CSP violations.
CI (.github/workflows/ci.yml) runs the same bar on every push and PR —
lint · typecheck · test · build across both apps on a Node 20 + 22
matrix, plus a committed-secret scan. The same quartet runs locally via
corepack pnpm verify; the caveat at the top of ci.yml explains why a local green
and a CI green mean the same thing. WDK is alpha; package versions are pinned
(see docs/ARCHITECTURE.md → Alpha-churn containment). Never commit .env*.
apps/next; apps/svelte runs that same
byte-unchanged core at full parity (the one delta is passphrase-only unlock)
as the portability proof. Built in phases. Honest CI bound: only the Ethereum
and BTC-fixture flows are exercised end-to-end; the other managers (Solana,
Polygon, Arbitrum, Plasma) are wired, typed, built and config + portfolio
unit-covered, but their live-RPC paths are not in CI.docs/RN-TO-WEB-MAP.md.@tetherto/* versions are pinned exact and quarantined
behind packages/wallet-core/src/wdk/ (ESLint-enforced), so an upstream break
is one-file localized.docs/SECURITY.md. Honest limits over false
parity.