Oracle3
Open-source trading engine and MCP server for prediction markets. Fee-aware no-arbitrage trading across related contracts, live on Kalshi, Polymarket and Solana or on paper.
A self-custody wallet with no seed phrase and no network fees, one tap from your Swissquote account.
FinanceTypeScript · JavaScriptGitHub @JitterKillerOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
SQure — Your keys. Swiss security.
SQure by Swissquote (formerly "Swissquote Wallet", PDR-026): self-custody, bank-grade.
Prototype for the Swissquote Wallet Challenge (BSA Hackathon): a self-custody wallet Swissquote could launch for its clients. The client holds the keys, Swissquote can never move the funds on its own, and the experience stays one tap from the Swissquote account.
Why things are built this way: see the decision records in docs/adr/ (product decisions and architecture decisions).Read the full README ↗SQure by Swissquote (formerly "Swissquote Wallet", PDR-026): self-custody, bank-grade.
Prototype for the Swissquote Wallet Challenge (BSA Hackathon): a self-custody wallet Swissquote could launch for its clients. The client holds the keys, Swissquote can never move the funds on its own, and the experience stays one tap from the Swissquote account.
Why things are built this way: see the decision records in docs/adr/ (product decisions and architecture decisions).
For the jury: threat model · business case (model: python3 docs/business/model.py) · pitch and demo script.
| Step | Status |
|---|---|
| Create wallet with a passkey (Face ID / Touch ID), no seed phrase | ✅ |
| Safe smart account (ERC-4337) owned by the passkey, deterministic address | ✅ |
| Gasless USDC transfers (paymaster), user never holds ETH | ✅ |
| Mock Swissquote backend: guardian (HSM), KYC, relayer, notifications | ✅ (mock) |
| Web UI: banking-style app shell, review-before-sign transfers, QR receive, on-chain activity with the factors that approved each payment (ADR-029), light/dark | ✅ |
| Security page for the two-Safe Account: owners read live onchain, "Swissquote · not an owner" checkable on Basescan, worst case as an amount, who can do what (ADR-034, #26) | ✅ |
Daily limit change, up or down, with Face ID + the Kit: worst case before → after, sealed review, one sponsored transaction (PDR-020, ADR-035, #13, npm run e2e:limit) | ✅ |
| Compliance before Face ID: sanctions screening, simulation with a readable summary, look-alike and large-payment checks (PDR-023, ADR-022), on the Send review (#12) | ✅ |
Approved dApps, built natively: Aave v3 and Compound III with USDC, 2 factors, sponsored only when approved (PDR-025, ADR-028); Earn screen with positions, risk acknowledgement and the unlisted-dApp demo (ADR-031, npm run e2e:earn) | ✅ |
Swaps USDC ↔ EURC and USDC ↔ ETH (WETH) at Uniswap v3 on Base Sepolia, from the Account with Face ID + Kit: best fee tier, exact approval, 0.5% minimum, readable preview; EURC and ETH balances on the Overview, swaps in Activity (PDR-028, ADR-032, npm run e2e:swap) | ✅ (real testnet pools: prices far from real markets, labelled) |
@handle directory: verified payee before Face ID, visibility, contacts, payment request link + QR (PDR-010, ADR-024), in the Send recipient field (#12) | ✅ |
| Transfers between the Swissquote account and the wallet, both directions, travel rule filled in (PDR-024, ADR-025); demo treasury to fund once | ✅ |
| Third guardian (trusted contact) | ⏳ |
| Separation of powers: Account Safe (user factors, threshold 2) + Key Safe with a user-chosen daily limit (PDR-014, PDR-020, ADR-017) | ✅ (Account created at onboarding #10; Overview with today's Face ID budget #11; Send: Face ID alone within the limit, Kit passphrase + Face ID above it #12) |
| One flow for a lost or stolen phone: time-locked eviction the thief cannot cancel (PDR-018, PDR-021, ADR-019, ADR-030) | ✅ ("Phone lost or stolen?" on the new phone #15, alert and 2-factor stop on the old phone #16, npm run e2e:eviction) |
Recovery Kit as a passphrase-encrypted cloud backup (PDR-016, ADR-018): passphrase, download, test step; "Test your Kit" every 90 days from the Security page, with an Overview reminder (the cloud file + passphrase, keys checked onchain; ADR-041, #9, npm run e2e:kit) | ✅ |
Open wallet for everyone + Swissquote link as the upgrade (PDR-015): level 0 for everyone, level 1 for linked clients (3-day lock-out with a Swissquote identity check), from the Security page or right after linking (ADR-036, #21, npm run e2e:level) | ✅ (KYC and the Swissquote sign-in are MOCK; no way back to level 0 yet) |
| Offers page: Free, Client and Premium "Theft protection" compared for your own Account (worst case from your daily limit, what your own key does in every offer), your offer and its one next step; one line on Send's receipt after the first payment that needed the Kit (PDR-019, ADR-039, #22) | ✅ (Premium subscription and key order are MOCK; Kit replacement "Soon") |
Security key as a third owner, at every level: added from the Security page with Face ID + the Kit (the phone itself is refused); with the Kit, it locks a lost phone out at once in "Phone lost or stolen?", even while a lock-out runs (PDR-017, PDR-019, ADR-038, #17, npm run e2e:key) | ✅ (any authenticator in the demo) |
| Face ID + a key tap instead of the Kit passphrase above the limit: Send, Earn, Swap and the limit change, the key first when the Account has one; Activity marks "Face ID + key" (ADR-043) | ✅ |
Demo security key: another phone plays the key on /wallet/key (its own Face ID, a real third owner), paired with a code or QR, requests relayed and shown on its "Demo screen" (ADR-043, npm run e2e:demo-key) | ✅ (the relay standing in for USB/NFC is MOCK) |
┌──────────────── Web app (Next.js) ──────────────────┐ ┌──── Mock Swissquote backend (/api) ────┐
│ Passkey (WebAuthn P-256, Secure Enclave) │ │ KYC session (video ident. — MOCK) │
│ └─ signs UserOperations (biometric prompt) │────►│ Swissquote Guardian key (HSM — MOCK) │
│ Recovery Kit v3 (2 keys, passphrase-encrypted) │ │ Relayer: ops Safe, sponsored UserOps │
└───────────────┬──────────────────────────────────────┘ └───────────────────┬────────────────────┘
│ UserOps │ UserOps
▼ ▼
Candide bundler + paymaster (gas sponsored) ───────────────────────────────
│
▼
EntryPoint v0.9 ──► Key Safe (the phone) Base Sepolia
└─ owner: passkey (WebAuthn verifier proxy)
Account Safe (holds the funds), threshold 2
├─ owners: Key Safe, Kit owner key, optional security key (ADR-017, ADR-038)
├─ Allowance Module: the Key Safe spends up to the daily limit with Face ID alone
└─ Candide recovery module: the lock-out of a lost phone (guardian = the Kit's
guardian key, + Swissquote at level 1), 7 / 3 days (3 min in the demo),
stopped only by 2 owners (ADR-019)
Where the keys live
| Key | Where | Can move funds? |
|---|---|---|
| Passkey private key | Device secure hardware (or synced in iCloud Keychain / Google Password Manager), never extractable | Up to the daily limit alone; above it, with a second factor |
| Passkey public key | On-chain, in the Safe | — |
| Recovery Kit keys (Account owner + guardian) | Encrypted with the client's passphrase (keystore V3): file in iCloud Drive / Google Drive + encrypted copy in the app. The passphrase is never stored or sent | Not alone: the owner key is 1 of the 2 signatures the Account needs; the guardian key starts a delayed eviction |
| Security key (optional) | A FIDO2 key kept at home; its Safe WebAuthn verifier is an owner of the Account | Not alone: with the Kit it locks a lost phone out at once (ADR-038) |
| Swissquote Guardian key | Swissquote HSM (mocked: env var or web/.data) | No: never an owner. At level 1, one guardian vote on a lock-out, after an identity check, during a delay that 2 owners can stop |
/wallet/lost): open the Recovery Kit (file + passphrase) and create a new passkey.With a security key, the Kit and the key replace the old phone at once, also while a lock-out runs (ADR-038).
0x036CbD53842c5426634e7929541eC2318f3dCF7eNo custom cryptography: every primitive comes from audited contracts (Safe, Safe passkey module, Candide recovery module) and established libraries.
cd web
npm install
npm run dev # http://localhost:3000 (app + mock backend)
localhost or HTTPS, and a passkey is bound to the domain it was created on.SWISSQUOTE_GUARDIAN_PRIVATE_KEY in web/.env.local (see web/.env.example), otherwise each machine has its own Swissquote Guardian.web → Deploy.SWISSQUOTE_GUARDIAN_PRIVATE_KEY (Production + Preview) = the team's shared key./api/health: expect "ok": true, "store": "upstash-redis", "guardianFromEnv": true.relayer address shown by /api/health.Passkeys are bound to the domain: create demo wallets on the production URL. Wallets made on localhost or a preview URL do not work there.
npm run smoke # simulated passkey → Safe → sponsored UserOps (no browser)
npm run e2e # real Chrome + virtual authenticator: create wallet in the UI, sign UserOps
npm run e2e:account-setup # protect flow in the UI (passphrase, Kit test, daily limit) → Account checked on-chain, levels 0 and 1 (~1 min)
npm run screenshots # captures every screen (desktop light/dark + mobile) into web/screenshots/
npm run spike:two-safe # on-chain spike of the two-Safe account + time-locked eviction (ADR-017, ADR-019), ~5 min
npm run spike:two-safe-variants # 2-owner eviction + immediate eviction with Kit + key, through lib/account.ts (~5 min)
npm run check:account # lib/account.ts: setup + C1–C4 on Base Sepolia (~1 min)
npm run check:kit # Kit v3 encryption, local (~5 s)
npm run check:eviction-api # /api/eviction endpoints over HTTP, needs npm run dev (~4 min)
npm run check:target-flows # client logic: spend within/above the limit, new limit, eviction cancel + finalize + handover, level upgrade (needs npm run dev, ~5 min)
npm run check:dapps # approved dApps with real USDC: Aave and Compound deposit/withdraw with 2 factors, paymaster allowlist (needs npm run dev + funded treasury, ~2 min)
npm run check:registry # wallet registry: valid proof, replay, other passkey, other origin, foreign Key Safe (needs npm run dev)
npm run check:sponsorship # sponsorship policy: quota, security ops, mixed batches, sanctions, linked client, stolen report (needs npm run dev)
npm run check:handles # @handles: uniqueness, reserved names, verified name, hidden = missing, contacts, rate limit (needs npm run dev)
npm run check:swissquote # Swissquote transfers: balance, unlinked refused, overdraft, treasury, deposit verification (needs npm run dev)
npm run e2e:link # real Chrome + virtual passkey: protect, sign in (MOCK), "Link with Face ID", claim @handle, /wallet/swissquote (~2 min)
npm run e2e:eviction # "Phone lost or stolen?" on two phones: lock-out, thief refused, 2-factor stop, takeover after the delay (~6 min)
npm run e2e:earn # Earn: Aave deposit with Kit + Face ID, withdraw, unlisted-dApp demo (~2 min, funded treasury)
npm run e2e:swap # Swap: USDC ↔ EURC, USDC ↔ ETH, Activity (~2 min, funded treasury)
npm run e2e:limit # daily limit raised and lowered with Kit + Face ID (~1.5 min, 0.10 USDC from the treasury)
npm run e2e:level # level 1 upgrade: link with Face ID, upgrade with Kit + Face ID (~30 s)
npm run e2e:kit # "Test your Kit": Demo skip, Overview reminder, wrong file and passphrase refused, test passes, Kit saved again (~40 s)
npm run e2e:key # security key: the phone refused as its own key, a USB key added, lost phone locked out at once with Kit + key (~1 min)
npm run e2e:demo-key # demo key on another phone: paired, added, limit with Face ID + key (refused, then approved), payment with the key (funded treasury), lock-out now with Kit + key (~2 min)
The e2e scripts need Google Chrome and npm run dev running.
| Endpoint | Role |
|---|---|
GET /api/health | Deployment check: store kind, write/read round trip, guardian + relayer (ADR-020) |
GET /api/guardian | Swissquote Guardian + relayer addresses |
POST /api/kyc | Identity verification session (MOCK outcome) |
POST /api/eviction | Starts a time-locked eviction of the phone (Key Safe) on the Account: Kit guardian signature; at level 1 also a verified KYC session, then Swissquote co-signs. Refuses anything but replacing the Key Safe (ADR-019, PDR-021) |
POST /api/eviction/finalize | Relays finalizeRecovery on the Account after the delay |
POST /api/wallets/challenge | Registry: single-use challenge for the ownership statement (ADR-021, MOCK auth) |
POST /api/wallets | Registry: verifies the passkey-signed EIP-712 statement + on-chain owners, links the Account to the client |
GET / DELETE /api/wallets/me?clientId= | Registry: the caller's entry / unlink (MOCK auth: client id in the query) |
POST /api/paymaster | Swissquote sponsorship policy in front of the ERC-7677 paymaster: level 0 quota, security operations always sponsored, sanctions, reported-stolen devices (ADR-023) |
POST / PATCH / DELETE /api/handles | Claim, change visibility, release the caller's @handle (linked clients, MOCK auth) |
GET /api/handles/[handle]?clientId= | Exact-match resolution → Account + masked verified name (rate-limited, no enumeration) |
POST /api/handles/contacts | Allow a client (by handle) to resolve my handle when visibility is "contacts" |
POST /api/compliance/screen | Sanctions screening of one address: MOCK list + optional Chainalysis (ADR-022) |
GET /api/swissquote/account?clientId= | MOCK Swissquote account balance, deposit address (relayer Safe), treasury balance |
POST /api/swissquote/withdraw | Swissquote account → wallet: on-chain USDC from the treasury to the linked Account |
| Real (on-chain / standard) | Mocked (documented) |
|---|---|
| Passkeys (WebAuthn), Safe smart account, ERC-4337 bundler + paymaster, USDC transfers, Account Safe (Allowance Module, eviction module), approved dApps (Aave v3, Compound III on Base Sepolia), Recovery Kit encryption (keystore V3), the Candide recovery module behind the lock-out (guardians, delay, stop, finalize), EIP-712 guardian signatures, registry ownership proof (WebAuthn + on-chain owner checks), security key (a WebAuthn credential as the Account's third owner through Safe's WebAuthn verifier) and the immediate lock-out with Kit + key | Swissquote banking account, KYC / video identification, guardian HSM, notifications (push / email / SMS / e-banking), postal mail, client authentication of the registry endpoints (client id passed by the caller), sanctions list (test addresses 0x…bad1, 0x…bad2, see ADR-022), Swissquote sign-in (lib/session.ts), Swissquote account balances and the demo treasury (relayer Safe, funded once with test USDC), Premium "Theft protection" subscription and security key order (/api/offers, server/offers.ts: no payment, nothing ships), the demo security key's channel (/api/key-relay, server/keyRelay.ts: another phone plays the FIDO2 key, its requests relayed instead of USB/NFC; the key and its signatures are real) |
docs/adr/ decision records: product/ (PDR-*) and architecture/ (ADR-*), index in README.md
CLAUDE.md context for AI assistants (imports docs/adr/README.md)
web/
src/app/(marketing)/ / (landing page)
src/app/wallet/(wallet)/ /wallet, /wallet/send, /wallet/receive, /wallet/activity, /wallet/security, … (AppShell: sidebar + guards)
src/app/wallet/(onboarding)/ /wallet/welcome, /wallet/welcome/protect (OnboardingShell)
src/app/wallet/(recovery)/ /wallet/lost (OnboardingShell, quiet)
src/app/api/ mock backend route handlers: guardian, kyc, recovery, recovery/finalize
src/components/views/ one view per page (Overview, Send, Receive, Activity, Security, Create, Protect, Recover)
src/components/shell/ AppShell, OnboardingShell, security banners, logo
src/components/ui/ design-system kit (button, card, feedback, field, stepper, address, page)
src/components/providers/WalletProvider.tsx wallet + on-chain state shared by all pages
src/lib/ config, passkey, wallet (Safe + UserOps), recovery, recoveryKit, activity, storage, format
src/server/ mock Swissquote backend: guardian key + relayer, JSON store, notifications
scripts/ smoke + e2e tests, UI screenshots
POST /api/swissquote/deposit |
| Wallet → Swissquote account: credits a verified on-chain transfer from the linked wallet (once per tx) |
POST /api/report-stolen | Report a stolen phone: its Key Safe loses sponsorship (identity session or Recovery Kit signature) |
GET / POST /api/security-keys | Security key directory (public data): lists a key only when its proxy matches its public key and is an owner on-chain; the first key listed stays while it is an owner (ADR-038) |
GET / POST /api/offers | MOCK Premium "Theft protection": the client's subscription and key order (subscribe, order-key, end), keyed by Swissquote client id; subscribing needs a linked wallet (ADR-039) |
POST /api/evict-now | Immediate lock-out with the Kit and the security key: rebuilds the swap of the Key Safe from the chain (and cancels a running lock-out), dry-runs it, relays it (ADR-038) |
GET / POST /api/key-relay | MOCK channel to the demo security key on another phone (instead of USB/NFC): pairing codes, one signing request per key, its answer (pair, register, request, seen, answer); public data only, it can sign nothing (ADR-043) |