Bug bounty · Polymarket integration

Sixteen findings on Manic Trade

A single guest session against app.manic.trade/pm on 3 September 2026. No account, no deposit, no orders placed — everything below was reached from the logged-out surface, which is also why the categories carrying this bounty's P0 weight are still untouched.

Build
dpl_74W5o38E8FQ…
Browser
Chromium 148
OS
Windows 11 · 26200
Viewports
1280×720 · 375×812
Session
Guest, no wallet
1 P1 · Severe Contradictory prices on a settled market
5 P2 · Medium Broken entry URL, wrong settlement date, unpriced buy buttons
11 P3 · Minor Filters, formatting, accessibility, stale copy
1 Withdrawn Evidence pointed at the test harness, not the app

Coverage against the published scope

Three of the six scope areas need a funded account. Those are the ones where P0 lives, and this session did not reach them.

Scope areaReachedFindings
Market discovery and navigationCoveredF-01 · F-05 · F-12 · F-13
Event details, prices, charts, market infoCoveredF-02 · F-03 · F-06 · F-07 · F-08 · F-11
Market status and settlementDisplay onlyF-02 · F-03 · F-05
Mobile, browser and wallet compatibilityPartialF-14 · F-15 · F-16
Deposits and real-money tradingNot reachedNeeds a funded account
Orders, positions, balances, P&LNot reachedNeeds a funded account

The three that matter

Full reproduction steps, captured DOM text and measured numbers. The remaining fourteen follow in summary.

F-02 P1 Every load

A settled market shows two contradictory price sets, and one pair sums to 101%

The Moneyline market for US Open ATP: Jaime Faria vs Carlos Alcaraz has resolved. The right panel says so — OUTCOME: CARLOS ALCARAZ, status Resolved, order book Market Ended. The outcome block carries the settlement values. The price chart legend directly beneath it carries something else entirely.

Reproduce
  1. Open /pm/event/atp-faria-alcaraz-2026-09-02
  2. Read the outcome block at the top of the centre panel
  3. Read the chart legend immediately below it
Rendered DOM text, verbatim
Carlos Alcaraz
100%
Jaime Faria
0%
Jaime Faria
9%
Carlos Alcaraz
92%

9% and 92% sum to 101%, which complementary outcomes cannot do, and both contradict a settlement the same screen declared two lines earlier. The chart tooltip on the final data point reads Sep 2, 2026, 09:00:16.

A trader reading the legend sees a live-looking market quoting an outcome at 92c that is already worth a dollar. On a market that had not yet settled, the same defect would present a false arbitrage.

F-01 P2 Every time, once any event has been opened

The URL in the bounty brief stops working after the first click

app.manic.trade/pm renders the market directory exactly once. Open any event and the address is claimed permanently: the app persists its open-tab list to localStorage and, on every later load of /pm, reopens the stored event and rewrites the address bar to /pm/event/<id>.

Reproduce
  1. Open /pm in a clean profile — the directory renders
  2. Click any event card — the address becomes /pm/event/<slug>
  3. Type /pm in the address bar and press Enter
Stored state that causes it
localStorage['pm-events-store']
{"state":{"category":"all","surface":"catalog",
 "sidebarCollapsed":false,
 "openEventIds":["atp-faria-alcaraz-2026-09-02"],
 "tabMeta":{…}}}

Removing that key and reloading restores the directory, which confirms the cause rather than just the symptom. Anyone following a link to /pm — the docs, the blog, this bounty's own brief — lands somewhere else. In this session the restored market happened to be a resolved one, so the first screen on return was a dead market.

F-10 Withdrawn Not filed

A slow page load that turned out to be my own test environment

I measured two consecutive cold loads of /pm reaching loadEventEnd at 94,029 ms and 135,689 ms, with time to first byte at 55 ms both times, and wrote it up as connection-pool saturation across 113 JavaScript chunks.

An automated Playwright run on the same machine and the same connection then loaded the same route and measured this:

Re-measured, headless Chromium
RunTTFBDOMContentLoadedloadEventEndJS chunks
Instrumented browser, load 156 ms1,653 ms94,029 ms113
Instrumented browser, load 255 ms4,817 ms135,689 ms113
Playwright, headless57 ms1,193 ms1,581 ms115

One and a half seconds, not ninety-four. The slow figures came from the instrumented browser I was driving, not from the application. The programme excludes issues caused by the reporter’s own device or network, and this one belongs in that category, so it was withdrawn before filing rather than submitted.

The chunk count itself — 115 requests for one route — still stands as an observation. It is not a defect on this evidence, and it is not worth a submission.

It is recorded here rather than deleted, on the view that a reviewer is owed the failures as well as the findings. The raw numbers are in the evidence locker.

The remaining fourteen

Each was filed separately through the official Typeform, with its own reproduction steps and environment block.

F-03P2

Resolved market advertises a settlement date a week out

The same US Open event shows Settlement Time: Sep 9, 3:00 PM UTC while displaying Resolved, Market Ended and a final score of 2–6. The match was played on 2 September — the event slug records the date itself. A user checking when funds are released gets a date unrelated to a settlement that already happened.

F-04P2Intermittent

An outcome with no price keeps both buy buttons enabled

On the New York Yankees vs. Los Angeles Angels card, Extra Innings rendered its price as -- while Yes and No stayed clickable. A later reload priced the same card normally, so the gap is transient — which makes it easier to walk into, not harder. On a real-money venue this is the setup for an order placed at a price the user never saw.

F-05P2

A resolved market sits in Trending with live buy buttons

The Total Sets: O/U 3.5 card for the US Open event appears in the default Trending feed at 100.0% with both order buttons rendered as live controls. Opening it reveals Market Ended. The integration's first discovery surface is promoting something nobody can trade. Reproduced across two page loads.

F-12P3

Picking a category silently discards the search query

Type bitcoin, get nine markets, click Politics — the query is gone with no notice. Both input[placeholder="Search markets"] elements return an empty value afterwards. Either search should compose with the category, or the app should say it cleared the box.

F-14P3

The empty state gives no way out of the filter that caused it

Select a zero-count subcategory and the grid reads No markets in this category with no reset control anywhere — scanning the rendered text for clear, reset or show all returns nothing. At 375×812 it worsens: the subcategory rail scrolls horizontally and the selected chip sits off-screen, so a mobile user sees an empty screen, no cause, and no undo.

F-15P3

Fifty-eight invisible controls sit in the keyboard tab order

Both halves of the responsive layout stay in the DOM, the inactive one hidden by zero sizing rather than display:none. The result is 58 enabled, focusable elements measuring 0×0:

[...document.querySelectorAll('button,a,input')]
  .filter(e => { const r = e.getBoundingClientRect();
    return r.width === 0 && r.height === 0
        && e.tabIndex >= 0 && !e.disabled }).length
// 58

A keyboard user passes 58 stops with no visible focus before reaching the grid. Separately, 23 of the 25 images on the page carry no alt text, including every market thumbnail.

F-07P3

Every prop label repeats the event title, so all 26 truncate the same way

The Props rail names each market with the whole event title prepended — US Open ATP: Jaime Faria vs Carlos Alcaraz Set 1 Winner, and twenty-five more like it. Rendered in the rail they all cut off at US Open ATP: Jaime Faria vs Carl…, so the only distinguishing part is the part that disappears. The rail cannot be scanned; each row has to be opened to identify it.

F-16P3

The nav still reads “August Rewards” on 3 September

The most prominent header call to action names last month, with the browser clock at 2026-09-03T04:26:35Z. The panel behind it advertises a Monthly Tournament. Either the label is a month stale, or a closed campaign is the app's loudest header item.

F-06P3

One percentage in thirty-five carries a decimal place

A single render of /pm shows 35 distinct percentages. Thirty-four are integers; exactly one is 100.0%. That points at a special case in the rounding path right at the 100% boundary.

F-08P3

Zero volume renders as a dash, not as zero

Twelve props show -- Vol beside neighbours reading $4K Vol and $116 Vol. A reader cannot tell “no trades yet” from “we failed to load this”.

F-09P3

Landing-page hero images are preloaded on the trading route

Chrome reports it without prompting: hero-video-fallback.png and hero-kv-first-frame.jpg are both preloaded on /pm and never used. They compete at high priority with the 113 chunks the route actually needs.

F-11P3

The same candidate is spelled two ways in one viewport

Republican Presidential Nominee 2028 lists J.D. Vance; Presidential Election Winner 2028, visible at the same time, lists JD Vance. Search and favourites will not match across the two.

F-13P3

Subcategories with a count of zero are offered as filters

The Politics rail lists Colombia Election 0. It is selectable, and selecting it produces the guaranteed empty result described in F-14.

F-17P3Observation, not confirmed

A click on empty grid space wrote to the OS clipboard

At the mobile viewport, a click on blank space in the market grid fired a copy handler and overwrote the system clipboard — no copy control under the cursor, no confirmation. Not reproduced a second time and the triggering element was never identified, so it is filed as an observation. Worth a look regardless: a user part-way through pasting a wallet address would lose it.

What was withdrawn, and why

Mobile subcategory chips appearing dead — not submitted

Two taps on the All subcategory chip at 375×812 selected the label text instead of applying the filter, and the grid stayed empty. That reads like a broken tap target, and it would have been a decent P2.

On re-testing, a programmatic element.click() on the same button applied the filter correctly — the active chip moved and the grid repopulated. The automation harness had been timing out on synthetic clicks throughout that stretch. The evidence points at the test rig, not the app.

It is recorded here rather than deleted, because a reviewer should be able to see what was ruled out as well as what was found. One manual pass on a real handset would settle it.

What this session could not reach

Deposits, orders, positions, balances, P&L, settlement crediting

All of it requires a funded account and real USDC at risk. None of it was exercised. That is where this bounty's P0 weight sits, so the coverage above should be read as the display and discovery layer only.

Everything reported was reached from a logged-out guest session. No wallet was connected, no funds were deposited, and no orders were placed.