Skip to content

Commit eeab4cc

Browse files
committed
Document the prediction markets prototype
Technical design document covering the data module, scenes, sample fallback, and the decisions behind the read-only scope, plus the CHANGELOG entry.
1 parent 4e7710b commit eeab4cc

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased (develop)
44

55
- added: App/device attestation for gated info-server requests
6+
- added: Prediction Markets side menu entry with market browsing scenes (prototype): category tabs, per-venue price comparison, and a merged order book view, backed by the Slipstream Connect API with a bundled sample dataset when no API key is configured
67
- added: "-m" tag on the version number in the Help scene for Maestro test builds
78
- added: Sign Message option in the wallet list menu for Bitcoin-family wallets, letting users prove self-hosted wallet ownership to exchanges by signing an exchange-provided message.
89
- added: `edge://buy` and `edge://sell` deep links (and their `https://deep.edge.app` equivalents) that open the buy/sell flow, optionally pinning a provider and payment method to the top of the quote options for that visit.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Prediction markets prototype: browse cross-venue markets from the side menu
2+
3+
| | |
4+
|---|---|
5+
| Status | Implemented (read-only prototype) |
6+
| Author | Jon Tzeng (agent run) |
7+
| Reviewer | - |
8+
| Last updated | 2026-08-14 |
9+
| Repos | [edge-react-gui](https://github.com/EdgeApp/edge-react-gui) |
10+
| Implementation | [edge-react-gui#6158](https://github.com/EdgeApp/edge-react-gui/pull/6158) |
11+
| Supersedes | - |
12+
| Related | [slipstream-example](https://github.com/tylerthebuildor/slipstream-example) |
13+
14+
File references point at the `jon/prediction-markets-prototype` branch. Direction came from Asana task 1217498026846463: add a prediction markets side menu entry with a new set of scenes, design at the implementer's discretion, preferring UI component reuse.
15+
16+
## Contents
17+
18+
1. [Problem](#1-problem)
19+
2. [Prior art](#2-prior-art)
20+
3. [Goals and non-goals](#3-goals-and-non-goals)
21+
4. [Design overview](#4-design-overview)
22+
5. [Detailed design: edge-react-gui](#5-detailed-design-edge-react-gui)
23+
6. [Testing](#6-testing)
24+
7. [Phase history](#7-phase-history)
25+
8. [Decisions](#8-decisions)
26+
9. [Glossary](#9-glossary)
27+
10. [References](#10-references)
28+
29+
## 1. Problem
30+
31+
Edge has no surface for prediction markets. The [Slipstream Connect](#slipstream-connect) API aggregates the same real-world event across Polymarket, Hyperliquid, and Kalshi into one [cluster](#cluster) with per-[venue](#venue) prices and a merged order book, and its non-custodial trade flow (the client signs, the API never sees a key) fits Edge's custody model. Before committing to a trading integration, the app needs a prototype that proves out navigation, scene design, and the data shapes.
32+
33+
## 2. Prior art
34+
35+
Two in-app features already solve the "browse an external REST dataset" problem and set the conventions this prototype reuses:
36+
37+
- CoinRanking (`src/components/scenes/CoinRankingScene.tsx`): list plus details scenes over the rates server, cleaner-typed responses in `src/types/coinrankTypes.ts`.
38+
- Gift cards (`src/plugins/gift-cards/`, `src/components/scenes/GiftCardMarketScene.tsx`): a plugin directory holding the API client and [cleaners](#cleaners), TanStack Query for fetching, category chips over an `Animated.FlatList`, an optional API key under `ENV.PLUGIN_API_KEYS` gating provider behavior.
39+
40+
Neither talks to a prediction markets [venue](#venue), and neither pattern needed changes; the prototype is an application of both.
41+
42+
## 3. Goals and non-goals
43+
44+
Goals:
45+
46+
- A Prediction Markets side menu row visible to every logged-in user.
47+
- A list scene: category tabs (sports, crypto, macro, politics), one card per market [cluster](#cluster) comparing each [venue](#venue)'s best ask, best-priced venue highlighted.
48+
- A details scene: per-venue bid/ask, merged order book top levels, per-venue metadata (24h volume, resolution date, link out to the venue page).
49+
- A typed client for `GET /connect/markets/{category}` with cleaner-validated responses.
50+
- Fully browsable without an API key, via a clearly labeled bundled sample dataset.
51+
52+
Non-goals (each deferred, see [decision 1](#d1-read-only-scope-no-trade-flow)):
53+
54+
- Trading (quote, order build, signing, submit), balances, positions, and venue setup.
55+
- A server-side key proxy. The prototype reads the key from `env.json` on the client; production hardening is deferred with the trade flow.
56+
- New icon assets. The row reuses an existing FontAwesome5 glyph.
57+
58+
## 4. Design overview
59+
60+
One repo, three layers: a data module under `src/plugins/prediction-markets/`, two scenes, and wiring (router types, `Main.tsx` registration, side menu row, localized strings).
61+
62+
```mermaid
63+
flowchart TD
64+
menu[SideMenu row: Prediction Markets] --> list[PredictionMarketListScene]
65+
list -->|useQuery per category| api[fetchPredictionMarkets]
66+
api --> keyed{ENV.PLUGIN_API_KEYS.slipstream set?}
67+
keyed -->|yes| live[GET baseUrl/connect/markets/category with X-API-Key]
68+
keyed -->|no| sample[Bundled sample dataset, banner shown in UI]
69+
live --> clean[asPredictionMarkets cleaner]
70+
sample --> clean
71+
clean --> list
72+
list -->|tap market card| details[PredictionMarketDetailsScene]
73+
details -->|tap venue row| browser[openBrowserUri to venue page]
74+
```
75+
76+
## 5. Detailed design: edge-react-gui
77+
78+
### Data module
79+
80+
`src/plugins/prediction-markets/` follows the gift-card plugin layout:
81+
82+
- `slipstreamTypes.ts`: [cleaners](#cleaners) mirroring the Connect API's market [cluster](#cluster) shape (`asPredictionMarket`, `asMarketBook`, `asMarketLeg`, `asVenuePrice`), the category list, and `formatCentsPrice`, which renders the API's `0`-`1` decimal-string prices as whole cents using biggystring math.
83+
- `slipstreamApi.ts`: the fetch client, whose exported surface is:
84+
85+
[`src/plugins/prediction-markets/slipstreamApi.ts`](https://github.com/EdgeApp/edge-react-gui/blob/2f183298671fd77c28e49878981f71db77d2b181/src/plugins/prediction-markets/slipstreamApi.ts)
86+
```ts
87+
export const fetchPredictionMarkets = async (
88+
category: PredictionMarketCategory
89+
): Promise<PredictionMarketsResult> => {
90+
```
91+
92+
`PredictionMarketsResult` is `{ markets: PredictionMarket[], isSampleData: boolean }`. With no key configured it returns the sample dataset and `isSampleData: true`; with a key it fetches `GET {baseUrl}/connect/markets/{category}` with the `X-API-Key` header and throws on a non-OK status so the scene's error state renders. Responses parse through `asJSON(asPredictionMarkets)`.
93+
- `slipstreamSampleData.ts`: two markets per category shaped like live responses. The literals run through `asPredictionMarkets` at module load, so the sample path exercises the same cleaners as the live path.
94+
95+
The optional key lives at `ENV.PLUGIN_API_KEYS.slipstream` (`apiKey`, optional `baseUrl` defaulting to `https://api.papi.market`), following the phaze entry in `src/envConfig.ts`.
96+
97+
### Scenes
98+
99+
`PredictionMarketListScene` (route `predictionMarkets`): `SceneWrapper` and `SceneContainer` with the scene title, a horizontal category chip row (the GiftCardMarketScene pattern), and an `Animated.FlatList` of `EdgeCard` rows. Each card shows the league tag, title, and one bordered cell per [venue](#venue) with its best ask; the cell matching the merged book's best ask (falling back to the lowest venue ask) is highlighted. Data comes from `useQuery` keyed on the category. Sample mode renders an `AlertCardUi4` banner above the list; fetch errors render the same card with an error string; an empty category renders a centered empty message.
100+
101+
`PredictionMarketDetailsScene` (route `predictionMarketDetails`, params `{ market: PredictionMarket }`): the market travels in the route params, so the scene does no fetching. Three sections: venue prices (one row per venue with bid and ask, best ask tagged), order book (top three bid and ask levels side by side, each level showing price, size, and source venue), and market details (one `EdgeRow` per [leg](#leg) with 24h volume and resolution date, tapping opens the venue URL via `openBrowserUri`).
102+
103+
### Wiring
104+
105+
- `src/types/routerTypes.tsx`: `predictionMarkets: undefined` and `predictionMarketDetails: PredictionMarketDetailsParams` in `EdgeAppStackParamList`, params imported from the details scene per repo convention.
106+
- `src/components/Main.tsx`: both scenes wrapped in `ifLoggedIn` and registered on the app stack next to the CoinRanking screens.
107+
- `src/components/themed/SideMenu.tsx`: a row after Markets navigating to `predictionMarkets`, icon `iconNameFontAwesome: 'poll'` (see [decision 4](#d4-icon-reuse-fontawesome5-poll)).
108+
- `src/locales/en_US.ts`: a PredictionMarkets region; all scene text is localized.
109+
110+
## 6. Testing
111+
112+
1. `PredictionMarketListScene.test.tsx`: snapshot render under `FakeProviders` (which supplies the `QueryClientProvider`).
113+
2. `PredictionMarketDetailsScene.test.tsx`: snapshot render with the first sports sample market as params, covering the [venue](#venue) price, order book, and [leg](#leg) sections.
114+
3. Sim drive (run evidence in the task's run report): side menu shows the row; list scene renders sample data with the banner; category tabs switch datasets; tapping a card opens details with prices, book, and legs.
115+
4. Type and cleaner conformance: the sample dataset passes `asPredictionMarkets` at module load, so a shape drift fails every jest suite importing it.
116+
117+
## 7. Phase history
118+
119+
### Phase 1: read-only prototype (2026-08-14)
120+
121+
Sketch and shipped implementation match: data module, two scenes, side menu row, sample fallback. Nothing diverged mid-build except the sample dataset's typing, which moved from hand-written `PredictionMarket[]` literals to cleaner-validated literals when tsc rejected the optional-field shapes. Deferred: the trade flow and everything key-gated (see [goals and non-goals](#3-goals-and-non-goals)).
122+
123+
## 8. Decisions
124+
125+
### D1: read-only scope, no trade flow
126+
127+
Chosen: browse-only (markets list and details). The trade flow needs a `trade`-scope API key, funded [venue](#venue) wallets, and [EIP-712](#eip-712) signing wired through the wallet layer; none of those exist in this environment (live API probes returned 404/502 without a key, and `env.json` has no Slipstream entry). Rejected: full trade flow (unbuildable and unverifiable here); quote-only trading UI (a quote the user cannot execute is a dead-end control, worse than omitting it). Reopen when a key with `trade` scope and a signing design for `signing_request.kind` exist.
128+
129+
### D2: sample-data fallback instead of key-gating the feature
130+
131+
Chosen: the row always shows; with no key the scenes run on bundled sample data behind a visible banner. Rejected: hiding the row without a key like the phaze gift-card row (the task commissions a browsable prototype, and a hidden row demos nothing on any build without secrets); treating no-key as an error state (same problem, an error screen is not a prototype). The banner keeps the provenance honest. Reopen at productization, when the row should probably gate on a real key.
132+
133+
### D3: client-side key from env.json
134+
135+
Chosen: `ENV.PLUGIN_API_KEYS.slipstream`, fetched directly from the app. The reference integration holds the key server-side behind a proxy, and that remains the right production shape. Rejected for the prototype: standing up a proxy or info-server relay for a read-only demo that usually runs keyless. This mirrors how other `PLUGIN_API_KEYS` entries already work in the app. Reopen with D1.
136+
137+
### D4: icon, reuse FontAwesome5 'poll'
138+
139+
Chosen: `iconNameFontAwesome: 'poll'` on the side menu row, matching the gift-card row's use of the FontAwesome5 escape hatch. Rejected: a new [Fontello](#fontello) glyph (requires regenerating `src/assets/vector/config.json` and the font binary, churn a prototype does not justify); reusing the Fontello `chart` glyph (already the Markets row icon, and duplicate icons in adjacent rows read as a bug).
140+
141+
### D5: data module under src/plugins/prediction-markets/
142+
143+
Chosen: the gift-card plugin layout (`<feature>Api.ts`, `<feature>Types.ts` equivalents). Rejected: the older CoinRanking layout (types in `src/types/`, fetch helpers in `src/util/network.ts`), which scatters one feature across three directories; new features in the repo have moved to the plugin-directory shape.
144+
145+
## 9. Glossary
146+
147+
### Slipstream Connect
148+
149+
The aggregation API at `api.papi.market`. It matches the same real-world event across prediction market venues, returns per-venue prices and a merged book, and builds venue order payloads for the client to sign locally. Source: [slipstream-example README](https://github.com/tylerthebuildor/slipstream-example).
150+
151+
### Cluster
152+
153+
One real-world event matched across venues: a title, one leg per venue, per-venue prices, and a merged book. A cluster's `id` is the venue:market_id pairs joined over the sorted legs. Source: [slipstream-example README, markets endpoint](https://github.com/tylerthebuildor/slipstream-example#get-connectmarketscategory).
154+
155+
### Leg
156+
157+
A cluster's listing on one specific venue: the venue's market id, YES outcome id, page URL, 24h volume, and resolution date. Defined by `asMarketLeg` in [slipstreamTypes.ts](https://github.com/EdgeApp/edge-react-gui/blob/jon/prediction-markets-prototype/src/plugins/prediction-markets/slipstreamTypes.ts).
158+
159+
### YES frame
160+
161+
The API's price normalization: every leg is quoted as the probability of the YES outcome, priced `0`-`1`, so asks are always the cost to buy and bids the proceeds to sell regardless of a venue's native convention. Source: [slipstream-example README](https://github.com/tylerthebuildor/slipstream-example#get-connectmarketscategory).
162+
163+
### Venue
164+
165+
An underlying prediction market exchange reachable through Slipstream Connect: [Polymarket](https://polymarket.com), [Hyperliquid](https://hyperliquid.xyz), or [Kalshi](https://kalshi.com). Kalshi is discovery-only in the API.
166+
167+
### Cleaners
168+
169+
Edge's runtime validation library ([cleaners](https://www.npmjs.com/package/cleaners)): composable functions that assert a JSON shape and produce the matching TypeScript type. All Connect responses and the sample dataset pass through them.
170+
171+
### Fontello
172+
173+
The app's generated icon font (`src/assets/vector/`), built with the [Fontello](https://fontello.com) font generator, and the default icon source for side menu rows. Adding a glyph means regenerating the font, which is why this row uses the FontAwesome5 fallback instead.
174+
175+
### EIP-712
176+
177+
Ethereum's typed structured data signing standard ([EIP-712](https://eips.ethereum.org/EIPS/eip-712)). Slipstream Connect returns order payloads in this format for the client wallet to sign locally; nothing in this prototype signs, which is part of why trading is out of scope.
178+
179+
## 10. References
180+
181+
- [slipstream-example](https://github.com/tylerthebuildor/slipstream-example): API documentation and reference integration.
182+
- Asana task 1217498026846463 (Prediction Markets - Prototype).
183+
- In-repo precedents: `src/components/scenes/GiftCardMarketScene.tsx`, `src/components/scenes/CoinRankingScene.tsx`, `src/plugins/gift-cards/`.

0 commit comments

Comments
 (0)