Activity feed
Mock payloads for the social Activity card. Backed by GET /api/posts/feed (FYP).
Activity feed
The "Activity" tab in the mobile app (the trader-card stream — whale_mike.eth bought Yes: BTC above $70,800 for $2400) is not backed by /api/events/discover. That endpoint is a slim browse index by design. The activity feed is the social posts firehose:
| Surface | Endpoint | Auth | Notes |
|---|---|---|---|
| Activity tab (default) | GET /api/posts/feed?scope=global | optional | Public FYP. One row per auto_trade post (created on FILLED ≥ $5 USD), manual post, or auto_position_close (PnL highlight). |
| Activity tab (filter "Following") | GET /api/posts/feed?scope=following | required | Same shape, scoped to the people you follow. Falls back to coldStart: true when you don't follow anyone. |
| Home screen "Activity" strip | GET /api/home → activityFeed.items[] | required | Thinner trade-row shape today; will be brought in line with the post-feed contract. |
Pagination is a cursor on createdAt (ISO timestamp) — pass the previous response's nextCursor back as ?cursor=.... Default limit=25, max 100.
Where each pixel comes from
Mapping the activity card from the design to fields in the response:
| Card element | Field |
|---|---|
| Avatar, handle, display name | author.{avatarUrl, handle, displayName} |
Top 50 rank chip | author.leaderboardRank (proposed — see §gaps) |
82% win rate · 2 trades this week | author.{winRatePct, tradesLast7d} (proposed) |
Verb (Bought / Sold half of / Won) | verb (proposed — derived from trade.side + position delta) |
Outcome chip (Yes) | trade.outcome |
Market label (BTC above $70,800) | market.title |
for $2400 | trade.sizeUsd |
Event title (Bitcoin price today at 5pm…) | event.title |
| Event description (subtitle in card) | event.description (proposed) |
| Event image (right-edge thumbnail) | event.imageUrl |
Category chips (Crypto, Bitcoin) | event.category (proposed) |
Outcome ladder ($71,050 above 4.00x 46% …) | event.topMarkets[] (proposed — child-market roll-up) |
$2.4M Volume · 80 Markets | event.{totalVolume24hUsd, marketsCount} (proposed) |
Venue chips (Polymarket, Kalshi) | event.venues[] (proposed — already on the row) |
❤ 5 · 💬 12 · ↗ 2 | engagement.{likeCount, commentCount, copyCount} (today: top-level) |
2m ago | createdAt |
Today's response shape — /api/posts/feed?scope=global
This is what the API returns right now (see pear/apps/api/src/routes/posts.ts::shapePost). It carries the trade USD notional and the anchored event/market titles, but doesn't yet carry the trader stats, event metadata, or the outcome ladder. Use this for the v1 of the card:
{
"items": [
{
"id": "post_01HX9WQK4D2A7N6F4W5T3KZQXQ",
"source": "auto_trade",
"body": null,
"trade": {
"id": "ord_8c1f4a30",
"venue": "polymarket",
"venueMarketId": "0x5f7c2e9a8b1d4e6f3a2c8d9e7b1f4a3c6d2e8b9f1a4c7e3d2b6a9f8c1e4d7b3a",
"outcome": "yes",
"sizeUsd": 2400.0
},
"event": {
"pearEventId": "pear_btc_5pm_edt",
"slug": "btc-price-today-5pm-edt",
"title": "Bitcoin price today at 5pm EDT",
"imageUrl": "https://cdn.pear.trade/events/btc.png"
},
"market": {
"venue": "polymarket",
"venueMarketId": "0x5f7c2e9a8b1d4e6f3a2c8d9e7b1f4a3c6d2e8b9f1a4c7e3d2b6a9f8c1e4d7b3a",
"title": "BTC above $70,800"
},
"author": {
"userId": "did:privy:cmmthssoe00rd0cjsllawl7m9",
"handle": "whale_mike.eth",
"displayName": "Whale Mike",
"avatarUrl": "https://cdn.pear.trade/avatars/whale_mike.png"
},
"imageUrl": null,
"likeCount": 5,
"commentCount": 12,
"copyCount": 2,
"liked": false,
"visibility": "public",
"createdAt": "2026-04-24T16:09:30.000Z"
},
{
"id": "post_01HX9WPS3M8N1V2K7H6L9DQXR2",
"source": "auto_trade",
"body": null,
"trade": {
"id": "ord_b3a91d6c",
"venue": "polymarket",
"venueMarketId": "0xa1b2c3d4e5f60718293a4b5c6d7e8f9012345678abcdef0123456789abcdef01",
"outcome": "yes",
"sizeUsd": 850.0
},
"event": {
"pearEventId": "pear_az_senate_2026",
"slug": "az-senate-2026",
"title": "Will Dem candidate win Arizona Senate?",
"imageUrl": "https://cdn.pear.trade/events/az-senate.png"
},
"market": {
"venue": "polymarket",
"venueMarketId": "0xa1b2c3d4e5f60718293a4b5c6d7e8f9012345678abcdef0123456789abcdef01",
"title": "Democrat wins Arizona Senate 2026"
},
"author": {
"userId": "did:privy:cmm2x1n7w00aa0cjs5g2ftkpd",
"handle": "kw_trades",
"displayName": "KW Trades",
"avatarUrl": "https://cdn.pear.trade/avatars/kw_trades.png"
},
"imageUrl": null,
"likeCount": 8,
"commentCount": 4,
"copyCount": 1,
"liked": false,
"visibility": "public",
"createdAt": "2026-04-24T16:09:00.000Z"
},
{
"id": "post_01HX9WP3Y6F1Q2W8E7R4T9YQXS3",
"source": "auto_position_close",
"body": null,
"trade": {
"id": "ord_4d8c7b1e",
"venue": "kalshi",
"venueMarketId": "KXNBASPREAD-26APR-LAL",
"outcome": "yes",
"sizeUsd": 1200.0
},
"event": {
"pearEventId": "pear_lakers_spread_apr",
"slug": "lakers-cover-spread-apr",
"title": "Will Lakers cover -4.5 spread?",
"imageUrl": "https://cdn.pear.trade/events/lakers-warriors.png"
},
"market": {
"venue": "kalshi",
"venueMarketId": "KXNBASPREAD-26APR-LAL",
"title": "Lakers to cover -4.5"
},
"author": {
"userId": "did:privy:cmm9p4lz500jx0cjslckrnp7v",
"handle": "alex_l",
"displayName": "Alex L.",
"avatarUrl": "https://cdn.pear.trade/avatars/alex_l.png"
},
"imageUrl": null,
"likeCount": 14,
"commentCount": 3,
"copyCount": 0,
"liked": false,
"visibility": "public",
"createdAt": "2026-04-24T16:08:00.000Z"
}
],
"nextCursor": "2026-04-24T16:08:00.000Z",
"coldStart": false
}Designer-grade enriched shape (target — what the card needs end-to-end)
This is the composite shape we recommend the FE codes against. Everything marked (proposed) in the table above is folded in below — the designer can build the full card off a single mock without stitching three endpoints together.
{
"items": [
{
"id": "post_01HX9WQK4D2A7N6F4W5T3KZQXQ",
"source": "auto_trade",
"verb": "bought",
"createdAt": "2026-04-24T16:09:30.000Z",
"body": null,
"imageUrl": null,
"author": {
"userId": "did:privy:cmmthssoe00rd0cjsllawl7m9",
"handle": "whale_mike.eth",
"displayName": "Whale Mike",
"avatarUrl": "https://cdn.pear.trade/avatars/whale_mike.png",
"winRatePct": 82,
"tradesLast7d": 2,
"leaderboardRank": 47,
"leaderboardTier": "top_50"
},
"trade": {
"id": "ord_8c1f4a30",
"venue": "polymarket",
"venueMarketId": "0x5f7c2e9a8b1d4e6f3a2c8d9e7b1f4a3c6d2e8b9f1a4c7e3d2b6a9f8c1e4d7b3a",
"side": "buy",
"outcome": "yes",
"sizeShares": 12000,
"sizeUsd": 2400.0,
"avgPrice": 0.20,
"filledAt": "2026-04-24T16:09:28.000Z"
},
"market": {
"venue": "polymarket",
"venueMarketId": "0x5f7c2e9a8b1d4e6f3a2c8d9e7b1f4a3c6d2e8b9f1a4c7e3d2b6a9f8c1e4d7b3a",
"title": "BTC above $70,800",
"yesPrice": 0.20,
"noPrice": 0.80
},
"event": {
"pearEventId": "pear_btc_5pm_edt",
"slug": "btc-price-today-5pm-edt",
"title": "Bitcoin price today at 5pm EDT",
"description": "Bitcoin price today at 5pm EDT today",
"imageUrl": "https://cdn.pear.trade/events/btc.png",
"category": "Crypto",
"subCategory": "Bitcoin",
"venues": ["polymarket", "kalshi"],
"totalVolume24hUsd": 2400000,
"marketsCount": 80,
"closesAt": "2026-04-24T21:00:00.000Z",
"topMarkets": [
{
"venue": "polymarket",
"venueMarketId": "0x5f7c2e9a8b1d4e6f3a2c8d9e7b1f4a3c6d2e8b9f1a4c7e3d2b6a9f8c1e4d7b3a",
"title": "$71,050 above",
"yesPrice": 0.46,
"multiplier": 4.0,
"highlighted": false
},
{
"venue": "polymarket",
"venueMarketId": "0x5f7c2e9a8b1d4e6f3a2c8d9e7b1f4a3c6d2e8b9f1a4c7e3d2b6a9f8c1e4d7b3a",
"title": "$70,800 above",
"yesPrice": 0.50,
"multiplier": 200.0,
"highlighted": true
}
]
},
"engagement": {
"likeCount": 5,
"commentCount": 12,
"copyCount": 2,
"liked": false
},
"visibility": "public"
},
{
"id": "post_01HX9WPS3M8N1V2K7H6L9DQXR2",
"source": "auto_trade",
"verb": "sold_half",
"createdAt": "2026-04-24T16:09:00.000Z",
"body": null,
"imageUrl": null,
"author": {
"userId": "did:privy:cmm2x1n7w00aa0cjs5g2ftkpd",
"handle": "kw_trades",
"displayName": "KW Trades",
"avatarUrl": "https://cdn.pear.trade/avatars/kw_trades.png",
"winRatePct": 61,
"tradesLast7d": 5,
"leaderboardRank": 92,
"leaderboardTier": "top_100"
},
"trade": {
"id": "ord_b3a91d6c",
"venue": "polymarket",
"venueMarketId": "0xa1b2c3d4e5f60718293a4b5c6d7e8f9012345678abcdef0123456789abcdef01",
"side": "sell",
"outcome": "yes",
"sizeShares": 1545,
"sizeUsd": 850.0,
"avgPrice": 0.55,
"filledAt": "2026-04-24T16:08:58.000Z"
},
"market": {
"venue": "polymarket",
"venueMarketId": "0xa1b2c3d4e5f60718293a4b5c6d7e8f9012345678abcdef0123456789abcdef01",
"title": "Democrat wins Arizona Senate 2026",
"yesPrice": 0.55,
"noPrice": 0.45
},
"event": {
"pearEventId": "pear_az_senate_2026",
"slug": "az-senate-2026",
"title": "Will Dem candidate win Arizona Senate?",
"description": "Resolves to the certified winner of the 2026 Arizona Senate race.",
"imageUrl": "https://cdn.pear.trade/events/az-senate.png",
"category": "Politics",
"subCategory": "US Midterms 2026",
"venues": ["polymarket"],
"totalVolume24hUsd": 380000,
"marketsCount": 1,
"closesAt": "2026-11-04T05:00:00.000Z",
"topMarkets": [
{
"venue": "polymarket",
"venueMarketId": "0xa1b2c3d4e5f60718293a4b5c6d7e8f9012345678abcdef0123456789abcdef01",
"title": "Yes",
"yesPrice": 0.55,
"multiplier": 1.82,
"highlighted": true
}
]
},
"engagement": {
"likeCount": 8,
"commentCount": 4,
"copyCount": 1,
"liked": false
},
"visibility": "public"
},
{
"id": "post_01HX9WP3Y6F1Q2W8E7R4T9YQXS3",
"source": "auto_position_close",
"verb": "won",
"createdAt": "2026-04-24T16:08:00.000Z",
"body": null,
"imageUrl": null,
"author": {
"userId": "did:privy:cmm9p4lz500jx0cjslckrnp7v",
"handle": "alex_l",
"displayName": "Alex L.",
"avatarUrl": "https://cdn.pear.trade/avatars/alex_l.png",
"winRatePct": 74,
"tradesLast7d": 8,
"leaderboardRank": 318,
"leaderboardTier": null
},
"trade": {
"id": "ord_4d8c7b1e",
"venue": "kalshi",
"venueMarketId": "KXNBASPREAD-26APR-LAL",
"side": "sell",
"outcome": "yes",
"sizeShares": 1200,
"sizeUsd": 1200.0,
"avgPrice": 1.00,
"filledAt": "2026-04-24T16:07:55.000Z",
"realizedPnlUsd": 540.0
},
"market": {
"venue": "kalshi",
"venueMarketId": "KXNBASPREAD-26APR-LAL",
"title": "Lakers to cover -4.5",
"yesPrice": 1.00,
"noPrice": 0.00,
"status": "resolved"
},
"event": {
"pearEventId": "pear_lakers_spread_apr",
"slug": "lakers-cover-spread-apr",
"title": "Will Lakers cover -4.5 spread?",
"description": "NBA · Lakers vs Warriors · Resolved",
"imageUrl": "https://cdn.pear.trade/events/lakers-warriors.png",
"category": "Sports",
"subCategory": "NBA",
"venues": ["kalshi"],
"totalVolume24hUsd": 380000,
"marketsCount": 1,
"closesAt": "2026-04-24T03:00:00.000Z",
"topMarkets": [
{
"venue": "kalshi",
"venueMarketId": "KXNBASPREAD-26APR-LAL",
"title": "Lakers cover -4.5",
"yesPrice": 1.00,
"multiplier": 2.20,
"highlighted": true
}
]
},
"engagement": {
"likeCount": 14,
"commentCount": 3,
"copyCount": 0,
"liked": false
},
"visibility": "public"
}
],
"nextCursor": "2026-04-24T16:08:00.000Z",
"coldStart": false
}Field reference (enriched shape)
author
| Field | Type | Source | Notes |
|---|---|---|---|
userId | string | identity.posts.user_id | Privy DID. |
handle | string | null | identity.user_profiles.handle | null until the user sets it. |
displayName | string | null | identity.user_profiles.display_name | |
avatarUrl | string | null | identity.user_profiles.avatar_url | |
winRatePct | integer 0–100 | null | identity.user_stats.win_count / total_trades | Rounded to nearest int. null if total_trades < 5. |
tradesLast7d | integer | null | identity.user_stats.trades_7d | Rolling 7-day window from the stats roll-up worker. |
leaderboardRank | integer | null | identity.leaderboard.rank | Global rank by total_volume. null if outside top 1000. |
leaderboardTier | "top_50" | "top_100" | "top_500" | null | derived from leaderboardRank | Drives the chip badge. |
trade
| Field | Type | Notes |
|---|---|---|
id | string | The originating order id. |
venue | "polymarket" | "kalshi" | "dflow" | |
venueMarketId | string | Polymarket token id or Kalshi market ticker. |
side | "buy" | "sell" | |
outcome | "yes" | "no" | |
sizeShares | number | |
sizeUsd | number | The "for $X" in the card. sizeShares × avgPrice. |
avgPrice | number 0..1 | |
filledAt | ISO timestamp | |
realizedPnlUsd | number | null | Only present on auto_position_close posts (drives the "Won" / "Lost" chip). |
event.topMarkets[]
The outcome-ladder strip in the card (e.g. $71,050 above 4.00x 46%). Server-side we cap to 2–3 child markets, sorted by volume_24h desc, with the trader's own outcome marked highlighted: true.
| Field | Type | Notes |
|---|---|---|
venue | "polymarket" | "kalshi" | |
venueMarketId | string | |
title | string | The bucket label, e.g. $71,050 above. |
yesPrice | number 0..1 | Drives the 46% chip. |
multiplier | number | 1 / yesPrice, rounded to one decimal. Drives the 4.00x chip. |
highlighted | boolean | true if this is the market the trader actually traded. |
verb
| Value | Triggered when |
|---|---|
bought | auto_trade, trade.side = "buy". |
sold_half | auto_trade, trade.side = "sell", position size shrinks to [1%, 99%) of pre-fill size. |
sold_all | auto_trade, trade.side = "sell", position closes (post-fill size ≤ 1%). |
won | auto_position_close, realizedPnlUsd > 0. |
lost | auto_position_close, realizedPnlUsd ≤ 0. |
manual | source = "manual". The card renders body instead of a verb chip. |
Empty / cold-start states
{ "items": [], "nextCursor": null, "coldStart": true }coldStart: true is only returned on scope=following when the caller follows nobody — the FE should swap in a "Find traders to follow" recommendations strip (use GET /api/social/overview.recommendedTraders).
Realtime hook
Subscribe to the social:activity WebSocket topic to prepend new cards as they're created. Frame shape:
{
"topic": "social:activity",
"data": {
"kind": "auto_trade",
"userId": "did:privy:cmmthssoe00rd0cjsllawl7m9",
"postId": "post_01HX9WQK4D2A7N6F4W5T3KZQXQ",
"eventSlug": "btc-price-today-5pm-edt",
"marketVenue": "polymarket",
"marketId": "0x5f7c2e9a8b1d4e6f3a2c8d9e7b1f4a3c6d2e8b9f1a4c7e3d2b6a9f8c1e4d7b3a"
}
}The frame is a pointer, not the card payload — the FE should refetch GET /api/posts/{postId} (which returns the same item shape as the feed) before inserting the row, so the enriched event/author data is consistent with what's already on screen.