Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 113 additions & 8 deletions hunch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ frens" → nothing; (5) "what was $BNKR's all-time high?" → answer, no market;
1. **Discover** — turn a phrase or post into matched markets.
`GET /api/partner/discover?q=<text>` (free-text / cashtags), or
`GET /api/partner/discover?post=<raw post text>` (claim-LLM extraction).
→ `{ count, matches: [{ market, odds, stats, matchKind, … }] }` — each match
is **nested** under `matches[].market`. No match → offer nothing.
→ `{ count, matches: [{ market, odds, stats, headline, matchKind, … }] }` —
each match is **nested** under `matches[].market`, and carries a
screenshot-ready **`headline`** (title · odds · social proof · close). No
match → offer nothing.
2. **Quote** — live odds + a full cost breakdown for a chosen market.
`GET /api/partner/quote?marketId=<id>&side=<yes|no>&sizeUsd=<n>` →
`{ market, odds, stats, tokenSnapshot, quote{ priceCents, feeUsd, netUsd,
Expand Down Expand Up @@ -137,6 +139,7 @@ Every market object is the shared ref documented in `references/market-ref.md`.
| `/api/partner/proof/{tradeId}` | GET | on-chain proof of a settled bet | `proof.md` |
| `/api/partner/positions` | GET | a wallet's portfolio + PnL | `positions.md` |
| `/api/partner/result` | GET | how a market resolved + payout | `result.md` |
| `/api/partner/resolved` | GET | a wallet's settled bets + ready-to-post win-broadcast | `resolved.md` |
| `/api/partner/trending` | GET | hottest markets + daily-post digest | `trending.md` |
| `/api/partner/mint` | POST | mint a market on demand (advanced, dark) | `mint.md` |

Expand Down Expand Up @@ -174,11 +177,38 @@ scheduled "what's trending on Hunch" post, or surface the top entry unprompted.
Read-only, cached, deterministic id selection (the model never picks). See
`references/trending.md`.

## Win-broadcast (close the loop loudly)

A silent settlement is a wasted viral moment. When a market a Bankr user bet on
resolves, **reply in the original bet thread** with the result + on-chain proof +
a rematch hook — the dopamine for the winner, the FOMO for everyone watching.

`GET /api/partner/resolved?wallet=<0x…>` returns the wallet's **settled** bets
(won + lost), newest first. Each entry carries a ready-to-post **`broadcast`**
line, plus a wallet-level **`digest`** (a "here's how it settled" recap). Read-only,
no money path; positions are keyed to the paying wallet exactly as the bet wrote
them. Two uses:

- **In-thread reply** — when a bet settles, reply to its original cast with the
entry's `broadcast` (it already ends with the project tags — post verbatim), e.g.
> 🎉 Won $8.40 on $BNKR → $100M (YES) — settled in USDC on Base.
> Proof: playhunch.xyz/markets/bankr-100m. Run it back? Tag @bankrbot. @playhunchxyz
Losses get a **rematch** nudge, never a dunk.
- **Recap post** — drop `digest.text` as a "your week on Hunch" post.

**Stateless — you dedupe.** Hunch reports the current resolved set; the bot tracks
what it has already broadcast (by wallet + `marketId`) so a settled bet is
announced once. Poll on a cadence, or check right after `result` flips to
`resolved`. See `references/resolved.md`.

## Money-path rules (do not break)

- **You never pick the market id or size from a model guess.** Discovery's
deterministic ranker returns the id; you echo it. The user picks side + size.
- **Bets are $1–$10** (x402 ceiling). Reject anything outside the band.
- **Offer sized chips, don't demand a number.** Pre-select the market's
`defaultTicketUsd`, surface `[$1] [$5] [$10]`, and accept any custom $1–$10.
One tap from "what are the odds" to a placed bet is the whole point.
- **Idempotent.** Reuse the same `idemKey` on retries — a replay returns the
original receipt, never a second bet.
- **Always show the disclosure line** from the market's category before
Expand Down Expand Up @@ -239,21 +269,95 @@ text cannot reach the money path.

## Reply shape

When discovery matches, render the bot's `Take YES / Take NO` UI:
When discovery matches, render the bot's `Take YES / Take NO` UI. Lead with the
server-built **`headline`** — it already packs the title, the live odds **and**
the social proof (bet count + pool), so the reply reads like a real market
instead of a coin-flip — then the distance hook, the disclosure, **sized**
actions, and the **attribution tags**:

> **{market.question}**
> YES {odds.yesPriceCents}¢ · NO {odds.noPriceCents}¢ · closes {market.deadlineLabel}
> **{match.headline}**
> {distance hook — market-cap markets only, from the quote's `tokenSnapshot`}
> _{category disclosure}_
> [Take YES] [Take NO]
> [Take YES] [Take NO] · size [$1] [$5] [$10]
> {match.headline already ends with the @tags — keep them}

- **`headline`** rides on every discover / trending match — render it verbatim as
the bold lead (it already opens with the market's short title); the numbers are
formatted server-side. Example:
`"$BNKR → $100M · YES 12¢ / NO 88¢ · 142 bets · $1.2k pool · closes Jun 30 · @playhunchxyz"`.
Don't strip it back to bare odds — the **depth** (bets + pool) is the social
proof that makes a reply travel, and the **trailing @tags credit the project**
(see *Project attribution* below). With no bets yet it reads `… · be the first
to bet · …`, a first-mover nudge rather than a hollow `0 bets`.
- **Distance hook** (market-cap markets): after you `quote`, fold the live
`tokenSnapshot` into one line — `"📈 $52M now · +92% to $100M"` (from
`distanceToTargetPct` + `targetMarketCapUsd`; `reachedTarget: true` → "already
past $100M ✅"). It turns a price answer into a reason to act. `null` for non
market-cap markets — just omit the line.
- **Size chips** — surface `[$1] [$5] [$10]` (the band is **$1–$10**) with the
market's `defaultTicketUsd` pre-selected. Don't make the user type a number;
still accept any custom $1–$10 amount.

For an N-way market (`market.outcomes` non-null) the headline reads
`"… · 6 outcomes · …"` (no YES/NO); list the rungs with their `impliedPct` from
the quote `ladder`, mark the `isCurrent` one, and let the user pick a rung + size.

## Project attribution (tag the project — every reply)

**Every Hunch reply credits the project with @-tags — treat this like the
disclosure: non-negotiable, never stripped.** It is built server-side so the
numbers and handles are always right; your job is to render it, not to compose it.

- **`@playhunchxyz` (Hunch) is always tagged**, plus the market's **token
project** when there's a verified one — e.g. a `$LFI` market tags
`@playhunchxyz @lienfiapp`, a `$VVV` market tags `@playhunchxyz @AskVenice`.
- **Where it already is:** the `headline` (discover + trending) **ends with the
tags** — render the headline verbatim and they ride along. The **`broadcast`**
and **`digest.text`** strings (win-broadcast / trending / settled recap)
**already contain the tags** — post them verbatim.
- **Where you add it:** the **`quote`** and **`positions`** responses carry a
separate **`tags`** field (e.g. `"@playhunchxyz @lienfiapp"`). End those replies
with that line, verbatim, as the last line.
- **Don't double-tag and don't add your own.** `$BNKR`'s own account is
`@bankrbot` (you), so a `$BNKR` market intentionally tags only `@playhunchxyz` —
the server already drops the self-tag. Never invent or substitute a handle; if a
token has no verified project tag the reply simply credits `@playhunchxyz`.

## Funding a bet (insufficient balance)

A `422 insufficient_balance` means the paying wallet doesn't hold enough **Base
USDC** for the bet — the most common reason a first-time user can't bet yet.
Don't dead-end the conversation. Offer a way forward, but treat funding as a
**money action that needs its own explicit consent**:

1. **Lower the bet** to fit the balance (e.g. bet $3.50 of a $3.72 balance —
leave a small margin; never stake the whole cent-rounded amount). Re-quote at
the smaller size, confirm, retry with the **same `idemKey`**.
2. **Top up by swapping another token → USDC on Base — only with permission:**
- **Never pick a token or execute a swap automatically.** A swap moves the
user's funds.
- **Show the wallet's swappable balances and ASK which token** (and how much)
to convert to USDC on Base.
- **Get explicit confirmation for that specific swap** before executing it —
the same consent bar as the bet itself.
- Only after the user confirms: Bankr performs the swap, then retry the bet
with the **same `idemKey`**.
3. Or the user deposits USDC on Base themselves.

> **Hard rule:** no token is ever swapped without the user naming it and
> approving that one swap. When in doubt, ask — don't convert. And never blindly
> retry the same amount: `insufficient_balance` keeps reverting until the balance
> or the size changes.

## Troubleshooting

| Status | Meaning | What to do |
|---|---|---|
| `402` | Payment required — the trade returned an x402 challenge. | Expected on the first `POST /trade`. Sign the EIP-3009 authorization, base64 it into `X-PAYMENT`, resubmit the **same** body + `idemKey`. |
| `409` | `market_closed` — the market isn't open / its deadline passed; **or** `idempotency_conflict` — the `idemKey` was reused with a **different** body. | Check `error`. `market_closed` → re-run `discover` for a live market. `idempotency_conflict` → mint a fresh `idemKey` per distinct bet (a replay of the *same* body returns the original receipt, which is safe). |
| `422` | Bad size (outside **$1–$10**) or a missing field. | Clamp `sizeUsd` to 1–10; ensure `marketId`, `side`, `walletAddress`, `idemKey` are present. |
| `422` | `insufficient_balance` — the wallet doesn't hold enough Base USDC for the bet (e.g. it tried to stake its **whole** cent-rounded balance, which is fractionally short); **or** bad size (outside **$1–$10**); **or** a missing field. | For `insufficient_balance`: see **Funding a bet** above — **lower `sizeUsd`** (leave a margin), or top up by **swapping another token → USDC on Base _only with the user's explicit, per-swap permission_ (ask which token first — never auto-swap)**, or have them deposit USDC. **Do NOT blindly retry the same amount** — it keeps reverting. Otherwise clamp `sizeUsd` to 1–10 and ensure `marketId`, `side`, `walletAddress`, `idemKey` are present. |
| `404` | Unknown market, or the partner API is disabled. | Re-run `discover` for a fresh id; never hand-craft a market id. If everything 404s, the endpoint may be off (see Safety). |
| `503` | `settlement_failed` / `settlement_recording_failed` — a genuine transient server/relay problem. **Funds were not moved.** | This one *is* safe to retry shortly with the **same** `idemKey`. If it persists, settlement is down — surface that, don't loop. (Contrast with `422 insufficient_balance`, which retrying never fixes.) |
| `count: 0` / `silent: true` on discover | No live market matches (or the post is non-actionable). | **Offer nothing.** Never substitute a loosely related market. |

Idempotency: use one `idemKey` (a UUID) per intended bet; reuse it verbatim on
Expand Down Expand Up @@ -296,10 +400,11 @@ any network retry so a dropped response can never double-settle.
- `references/proof.md` — on-chain proof read for a settled bet.
- `references/positions.md` — wallet portfolio lookup.
- `references/result.md` — market resolution read.
- `references/resolved.md` — a wallet's settled bets + the win-broadcast digest.
- `references/trending.md` — the trending feed + daily-post digest.
- `references/mint.md` — on-demand market mint (advanced, flag-gated).
- `references/transcripts.md` — worked transcripts (bet, claim-LLM, injection,
multi-market, portfolio, result, silence).
multi-market, portfolio, result, win-broadcast, funding/swap, silence).
- `scripts/walkthrough.sh` — a runnable discover → quote → trade(402) example.
- `x402-registry.json` — the x402 service listing for go-live registration, plus
the **pinned** `allowedOrigins` (host pinning) and `signingPolicy` (pre-sign
Expand Down
8 changes: 6 additions & 2 deletions hunch/references/discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ else stays lexical; the raw sentence is a last-resort fallback only.
"matchedTerms": ["$bnkr"],
"reason": "cashtag $bnkr matches market token",
"odds": { "yesPriceCents": 12, "noPriceCents": 88 },
"stats": { "totalBets": 142, "totalPoolUsd": 1240, "yesPoolUsd": 150, "noPoolUsd": 1090, "feeUsd": 24.8 }
"stats": { "totalBets": 142, "totalPoolUsd": 1240, "yesPoolUsd": 150, "noPoolUsd": 1090, "feeUsd": 24.8 },
"headline": "$BNKR → $100M · YES 12¢ / NO 88¢ · 142 bets · $1.2k pool · closes Jun 30 · @playhunchxyz"
}
]
}
Expand All @@ -95,6 +96,7 @@ else stays lexical; the raw sentence is a last-resort fallback only.
| `reason` | Human explanation of why it matched — useful for debugging, not for the reply. |
| `odds` | Live `{ yesPriceCents, noPriceCents }` (falls back to `50/50` if the book can't be read). For ladder markets odds are returned by `quote`, not here. |
| `stats` | Live bet activity — see below. |
| `headline` | **Screenshot-ready one-liner**, built server-side: `title · odds · social proof (bets + pool) · close · @tags`. Render it verbatim under the question — the numbers **and the project @tags** are formatted for you. It **ends with the attribution** (`@playhunchxyz` + the token project, e.g. `… · @playhunchxyz @lienfiapp`) — keep those tags, they credit the project (see SKILL.md *Project attribution*). N-way markets read `N outcomes` instead of YES/NO; markets with no bets yet read `be the first to bet`. This is the line that makes a reply travel — lead with it instead of bare odds. |

#### `stats` (bet activity)

Expand All @@ -105,7 +107,9 @@ else stays lexical; the raw sentence is a last-resort fallback only.
| `yesPoolUsd` / `noPoolUsd` | USD pooled per side (both `0` for ladder markets — per-rung backing is on the quote's `ladder`). |
| `feeUsd` | Fees accrued so far, USD. |

Surface real depth ("142 bets · $1,240 pooled") instead of bare odds when it helps.
The `headline` already folds this depth in (`142 bets · $1.2k pool`) — render it
verbatim and the social proof comes for free, no assembly. Reach into the raw
`stats` fields only when you want a custom layout.

### Response — `post` mode (claim-LLM)

Expand Down
5 changes: 4 additions & 1 deletion hunch/references/positions.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ Use for: "show my Hunch bets", "how are my positions doing", "what's my PnL".
"proofUrl": "https://basescan.org/tx/0x…",
"filledAt": "2026-06-01T12:00:00.000Z"
}
]
],
"tags": "@playhunchxyz @lienfiapp"
}
```

Expand All @@ -55,9 +56,11 @@ Use for: "show my Hunch bets", "how are my positions doing", "what's my PnL".
| `maxPayoutUsd` | Payout if this side wins ($1 / share). |
| `status` | `open`, `resolved-won`, or `resolved-lost`. |
| `proofUrl` | Entry-settlement on-chain proof (BaseScan), when available. |
| `tags` | **Project attribution** for the portfolio reply's footer — `@playhunchxyz` + up to two of the held markets' token projects. Render as the last line, verbatim (SKILL.md *Project attribution*). |

### Reply shape

> **Your Hunch bets** (1 open · 1 resolved · PnL +$1.42)
> • **$BNKR → $100M** — YES, $5 @ 12¢ → 15¢ · +$1.25 · open
> • **$HUNCH flips $LFI** — YES, $3 · resolved-lost
> @playhunchxyz @lienfiapp
32 changes: 24 additions & 8 deletions hunch/references/quote.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,18 @@ the pricing call between `discover` and `trade`. Read-only, CORS-open, cached
"netUsd": 4.9,
"shares": 40.83,
"feeRecipient": "Hunch market treasury"
}
},
"tags": "@playhunchxyz"
}
```

- `market` — the shared [market ref](./market-ref.md).
- `odds` — live `{ yesPriceCents, noPriceCents }` (50/50 fallback if unreadable).
- `stats` — bet activity (same shape as discover; see `discovery.md`).
- `tags` — **project attribution** to render as the **last line** of the quote
reply, verbatim: `@playhunchxyz` + the token project when verified (e.g. a `$LFI`
quote returns `"@playhunchxyz @lienfiapp"`). Always present. Treat it like the
disclosure — don't strip it (SKILL.md *Project attribution*).
- `quote` — the cost breakdown for `side` at `sizeUsd`:

| Field | Meaning |
Expand Down Expand Up @@ -108,7 +113,8 @@ added:
},
"tokenSnapshot": null,
"quote": { "side": "63m-67m", "priceCents": 35, "grossUsd": 5, "feeUsd": 0.1,
"netUsd": 4.9, "shares": 14, "feeRecipient": "Hunch market treasury" }
"netUsd": 4.9, "shares": 14, "feeRecipient": "Hunch market treasury" },
"tags": "@playhunchxyz"
}
```

Expand All @@ -125,13 +131,23 @@ added:

### Reply shape

> **{question}**
> YES {yesPriceCents}¢ · NO {noPriceCents}¢ · closes {deadlineLabel}
> _{category disclosure}_
> [Take YES] [Take NO]
By quote time you have the discover `headline` **and** the live `tokenSnapshot`,
so add the distance hook and sized actions:

For a ladder, list rungs with `impliedPct` and let the user pick one; mark the
`isCurrent` rung.
> **{discover `headline`}**
> {distance hook}
> _{category disclosure}_
> [Take YES] [Take NO] · size [$1] [$5] [$10]

- **Distance hook** (market-cap markets): `"📈 $52M now · +92% to $100M"`, from
`tokenSnapshot.currentMarketCapUsd` / `distanceToTargetPct` /
`targetMarketCapUsd` (`reachedTarget: true` → "already past $100M ✅"). It turns
a price answer into a reason to bet. `tokenSnapshot` is `null` for every
non-market-cap market — just omit the line.
- **Size chips** — `[$1] [$5] [$10]` (band $1–$10) with `defaultTicketUsd`
pre-selected; accept any custom $1–$10. Don't make the user type a number.
- **Ladder** — list rungs with their `impliedPct`, mark the `isCurrent` rung, and
let the user pick one + a size (no YES/NO on an N-way market).

### Errors

Expand Down
Loading