Skip to content

feat(streaming): Superfluid streaming pricing strategy - #9

Draft
tnrdd wants to merge 29 commits into
devfrom
feat/streaming-frontend
Draft

feat(streaming): Superfluid streaming pricing strategy#9
tnrdd wants to merge 29 commits into
devfrom
feat/streaming-frontend

Conversation

@tnrdd

@tnrdd tnrdd commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a streaming (Superfluid) pricing strategy for Markee boards, alongside the existing fixed-price strategy. Message rank on a streaming board is driven by an on-chain effectiveRate (wei/sec): the max of the live aggregate stream rate and a decaying grandfather floor. Strategy is orthogonal to vertical, so streaming boards slot into the same marketplace/home listings as fixed ones.

Opening as a draft to start review. Streaming is gated and inert in production until configured (see open items).

What's included

  • Streaming boards + reader view. StreamingBoardDetail, unified under /markee/[address] (strategy detected on-chain by contract version). useStreamingMarkees ranks by effectiveRate.
  • Create + back flow. CreateMessageModal to add a message, StreamModal to open/adjust a Superfluid CFA flow (single-tx wrap + permit + createFlow).
  • Streaming leaderboards API (/api/streaming/leaderboards): ranks boards by cumulative ETHx streamed, reconstructed from Superfluid FlowUpdated logs (totalFundsRaw), with effectiveRateRaw for the $/mo label. /api/streaming/register for board metadata.
  • Live-ticking totals. useStreamingBoardTotal + useFlowingAmount count streamed amounts up between on-chain refetches.
  • Keeper. /api/cron/streaming-keeper + lib/streaming/keeper.ts, plus fork/seed/verify scripts under frontend/scripts/.
  • Shared plumbing. lib/strategy.ts + StrategyBadge (strategy orthogonal to vertical), lib/superfluid/streaming.ts encoding helpers, flow-rate ($/mo) labels in marketplace listings.
  • Supporting changes. CSP allows the configured NEXT_PUBLIC_BASE_RPC_URL origin in connect-src; repeated contract ABIs deduped into lib/contracts/abis.ts; view writes made production-only (VERCEL_ENV === 'production') so previews/local read through but never increment the shared KV store; root .gitignore added.

Status / open items

  • Draft. Streaming is gated behind STREAMING_ENABLED and the API is inert until NEXT_PUBLIC_STREAMING_FACTORY is configured, so merging does not activate streaming in production on its own.
  • Production streaming still needs the factory deployed and the keeper cron enabled.
  • Some env vars and one-off fork/seed scripts are testnet-only and should be cleaned up before streaming ships to mainnet.
  • No automated test suite in this repo; verification is via Vercel preview and the fork scripts.

tnrdd added 8 commits July 1, 2026 10:26
StreamModal (permit + single-tx host.batchCall), streaming boards listing +
leaderboard pages ranked by effectiveRate, useStreamingBoards/useStreamingMarkees
hooks, the superfluid encoding helper + StreamingLeaderboardABI, gated create /
raise-funding entries, and the keeper cron route. The whole feature self-gates on
NEXT_PUBLIC_STREAMING_FACTORY (STREAMING_ENABLED) and stays hidden until set.
Fork-test harnesses under scripts/.
…nect-src

The CSP connect-src hardcoded the RPC allowlist (Alchemy + base.org), so pointing the
app at any other RPC via NEXT_PUBLIC_BASE_RPC_URL (a fork, a self-hosted node, etc.) was
silently blocked by the browser. Derive the configured RPC origin at build and add it.
…oards

Make pricing strategy (fixed vs streaming) an axis orthogonal to the
vertical (placement). Verticals and their pages stay; strategy is a
per-markee attribute.

- Create flow is now strategy -> vertical -> configure. Streaming is a
  gated "Coming soon" strategy until NEXT_PUBLIC_STREAMING_FACTORY is
  set, and is vertical-agnostic (one factory; vertical stored off-chain
  in KV via /api/streaming/register).
- Rank fixed and streaming markees on one universal effectiveRate axis:
  streaming reads it on-chain, fixed imputes total/(3*2628000) wei/sec
  to match the contract grandfather floor. Badge every row by strategy
  on home, marketplace, and the github/superfluid listings.
- Unify board detail at /markee/[address]: detect strategy via on-chain
  VERSION and render the fixed or streaming detail. Retire the
  /ecosystem/platforms/streaming route (streaming is a strategy, not a
  platform) and delete the now-dead useStreamingBoards hook.
- Add lib/strategy, StrategyBadge, StreamingBoardDetail, boardMeta and
  /api/streaming/{register,leaderboards}.

Streaming stays gated, so production is unchanged beyond a Fixed badge
and identical ordering.
Leaderboard listing routes and useFixedMarkees each re-declared ABIs
that already exist centrally. Point them at the shared exports:

- add shared LeaderboardFactoryABI (getLeaderboards); the fragment was
  byte-identical across the three listing routes
- streaming route -> StreamingLeaderboardABI; superfluid/openinternet
  routes -> LeaderboardV11ABI (same read signatures, incl. topFunds name)
- all three MARKEE_ABI -> central MarkeeABI
- useFixedMarkees -> central FixedPriceStrategyABI + MarkeeABI

No behavior change: every replacement ABI carries the same signatures
for every function called, getTopMarkees is read positionally, and the
narrow single-call and external Superfluid fragments are left inline.
tsc clean.
Reconstruct each board's cumulative inflow from Superfluid FlowUpdated logs
(getLogs integration, chunked + KV-cached) and expose it as totalFundsRaw,
keeping effectiveRateRaw for the $/mo label. Add useStreamingBoardTotal +
useFlowingAmount so the total ticks up live between on-chain refetches, and
extract CreateMessageModal shared by the marketplace and board detail views.
Gate POST /api/views so only VERCEL_ENV=production increments the shared KV
store. Previews and local dev now read the same store through (counts still
look real) but never write, keeping preview traffic and bots out of the
production totals.

With writes safely gated, track a view for a streaming board's top message in
StreamingBoardDetail (mirroring the fixed reader) and surface the count in the
board's hero stats, so streaming boards accumulate views like fixed ones.
… docs

The repo had no root .gitignore beyond .vercel, leaving node_modules, foundry
out/cache/broadcast/lib, foundry.lock, and the local-only .claude design docs
untracked-but-not-ignored (one stray `git add -A` from committing them). Cover
them. /lib/ is anchored to the repo root so frontend/lib source is never caught.
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
markee Ready Ready Preview, Comment Jul 22, 2026 4:13pm

The five frontend/scripts/*.mts are one-off fork/seed/logic-check tooling run
manually via tsx, not imported by the app or build. Untrack them and gitignore
frontend/scripts so they stay local-only, and drop the two now-dangling script
references from the keeper README (dryRun + the on-chain forge test still cover
validation).
create-a-markee (createLeaderboard), CreateOpenInternetModal, and
RevnetBuyWidget sent transactions with no chainId, so wagmi broadcast on
whatever chain the wallet was on. A wallet left on Ethereum mainnet fired a
real L1 transaction against a Base contract address (wasted gas, wrong chain;
RevNet pay is worse since terminals share deterministic cross-chain addresses).

Mirror the guarded modals: block when the wallet is not on Base (prompt a
switch) and pin chainId on the write so wagmi refuses a wrong-chain send
instead of silently broadcasting it.
Privy embedded wallets broadcast via chain.rpcUrls, not the wagmi transport, so
pointing NEXT_PUBLIC_BASE_RPC_URL at a fork only ever routed reads there; writes
went to real Base (or wherever an external wallet sat). When the var is set,
override base.rpcUrls.default + privyWalletOverride to it and share one
CANONICAL_CHAIN across wagmi + Privy so embedded/social-login writes hit the
fork. Same chainId (8453), so the ETHx permit domain is unchanged.

FORK-TESTING ONLY, remove before mainnet (env-gated, self-disables when unset).
External wallets still use their own RPC and are not fork-safe: testers must use
email/social (embedded) login.
tnrdd added 4 commits July 2, 2026 13:26
Bare toLocaleString() formats with the server locale during SSR and the
browser locale after hydration, so any non en-US browser locale crashed
hydration on the buy widget receive amount and the owners phase rates.
A browser-injected wallet that Privy merely detects (site previously
authorized in the extension) could hold the active wagmi slot after an
email login, so writes would route through a wallet the user never picked.
SyncActiveWallet demotes an active wallet only when Privy has finished
enumerating wallets and explicitly reports it unlinked, acts once per
address, and the header pill now shows the actual active account.
…tion

Privy's embedded-wallet typed-data screen crashes deterministically
(SignRequestScreen destructures a missing data.signMessage, broken on
react-auth 3.22.1 and 3.32.2), so the EIP-2612 permit leg of the open
flow is unusable in-app. depositBuffer has always pulled via
transferFrom, so a plain ERC20 approve carries the same authorization:
approve (skipped when the allowance already covers the buffer), then a
3-op host.batchCall of wrap, depositBuffer, createFlow. Verified
end-to-end on a Base fork including the lib encoding.

Also hardens the modal: disabled buttons render as disabled, empty-rate
clicks explain themselves, the approve receipt status is checked before
the value-bearing batch, the flow aborts silently if the modal closes
mid-await, only the final action hash drives the success screen, and
the receipt watcher is pinned to Base.
The streaming leaderboards route now exposes each board's admin (the
creating wallet on v1.3 factories), and the account page fetches
streaming boards, filters them by admin, and renders them under their
vertical with the strategy badge. The website verify/integrate/manage
flows stay fixed-price-only via one isFixedWebsiteBoard predicate, so
streaming boards route to their board page instead. The streaming
breadcrumb now links back to the create flow.
tnrdd added 2 commits July 8, 2026 08:31
Brings mainnet address/config updates and shared features onto the streaming branch:
- MARKEE_TOKEN -> 0xF6627cF19317C33B457f77452876e6e297c4942F
- RevNet Base config -> projectId 7, terminal 0x130f5Dd2bD8805443Cf41755253D778a75a67f53
- restore V13_LEADERBOARDS (dropped at df0e08d)
- network-wide view total (views:network:total) + seed-network-views admin route
- server-computed totalPlatformFundsUsd on ecosystem API, consumed by home + marketplace
- BuyMessageModal MARKEE estimate fixes

addresses.ts conflict resolved by keeping streaming exports alongside main's V13_LEADERBOARDS.
CreateOpenInternetModal (after website create), IntegrationModal (buy URL), and
update-markee-file's leaderboardUrl (written into users' GitHub files) routed to
legacy /ecosystem/... pages; point them at the unified /markee/[address] instead.
Legacy pages' own in-page links and the OAuth returnTo fallback are left as-is.
Comment thread frontend/app/api/ecosystem/leaderboards/route.ts Fixed
CodeQL js/request-forgery: self-fetches of our own API routes built their
origin from new URL(request.url).origin, which is caller-controlled via the
Host header and carries the protection-bypass secret.

Add lib/internal-origin.ts and use it in the ecosystem and account routes.
The account routes now also send the bypass header, so they keep working if
the origin ever resolves to a protected deployment URL.
tnrdd added a commit that referenced this pull request Jul 9, 2026
The streaming frontend (StreamModal, keeper route, streaming board pages,
Superfluid encoding helpers, fork scripts) landed independently on
feat/streaming-frontend (#9), which supersedes the earlier draft carried
here: permit replaced by an approve tx, the standalone
/ecosystem/platforms/streaming pages unified under /markee/[address], and
the dev/testnet scripts kept out of the repo.

Leaves this PR contracts-only: StreamingLeaderboard + factory, deploy
script, Base-fork tests, and the Foundry toolchain.
tnrdd added 2 commits July 9, 2026 19:27
useMarkees inlined V13_LEADERBOARDS.COOPERATIVE as a literal, leaving the
constant with no readers while the same address sat duplicated across the
codebase. Value is unchanged; this only restores the indirection so a
redeploy is a one-line edit in addresses.ts.
The streaming contracts PR tracks lib/forge-std as a submodule. Ignoring
/lib/ here would shadow it and silently swallow any Foundry dep added
later. Matches the .gitignore on feat/streaming-strat so the two PRs
merge without a both-added conflict.
@tnrdd
tnrdd changed the base branch from main to dev July 10, 2026 05:15
The spec marks the pool connect as mandatory: a disconnected backer's
wallet drains at the full stream rate while the refund accrues unclaimed
in the pool, so they could be liquidated even while being refunded. The
implemented batch had dropped it.

Adds a fourth op to buildOpenStreamOps: op 201 straight to the GDA
agreement class (a 301 via GDAv1Forwarder would connect SimpleForwarder
instead of the backer). Verified end to end on the Base fork: one
batchCall flips isMemberConnected false to true with flow, units, and
buffer deposit all landing as expected.
tnrdd added 4 commits July 22, 2026 15:15
The keeper was written scheduler-agnostic and invented its own secret, RPC
and from-block. Now that it runs on Vercel cron those collapse into
CRON_SECRET, NEXT_PUBLIC_BASE_RPC_URL/ALCHEMY_BASE_URL and
STREAMING_FROM_BLOCK; KEEPER_PRIVATE_KEY is the only var it still owns.

Also: take a KV lock so a run that outlives its tick cannot collide with the
next one on the single signer's nonce, paginate getLeaderboards instead of
capping at 200 (a skipped board never heals its title nor settles its
backers), batch the per-backer pendingSettlement reads, and chunk the
BackerUpdated scan so it stays under provider getLogs range caps.
Drops the CANONICAL_CHAIN rpcUrls override that pointed Privy embedded
wallets at the Base fork, plus the fork wording it left behind. The anvil
box is torn down, so there is no in-app fork-testing path any more.
Board totals came from a getLogs integration bounded to a 50k-block window.
That was exact only because every fork board was hours old; on mainnet it
silently truncates the total once a board outlives the window, and the
marketplace ranks on it.

Read cumulative inflow from accountTokenSnapshots instead, and subtract what
the board's GDA pools refunded: under Option B every non-#1 Markee's backers
are refunded, so gross inflow counts money that was handed back. Subtracting
outflow would be wrong in the other direction, since outflow also carries the
beneficiary stream and settlement.

Refunds are extrapolated at flowRate minus adjustmentFlowRate. The adjustment
is the remainder the GDA cannot allocate to units and returns to the admin, it
is never really refunded, and it does not appear in the admin's inflow either,
so it does not cancel out.

On failure the route now serves the last good payload tagged stale rather than
falling back to a gross figure, which would quietly overstate the headline.
Decay and stream decreases fire no transaction and no event, so the stale
title can only be caught by polling. Inert until the factory is configured.
tnrdd added 2 commits July 22, 2026 17:41
# Conflicts:
#	frontend/app/api/account/funded/route.ts
#	frontend/app/create-a-markee/page.tsx
#	frontend/app/markee/[address]/page.tsx
#	frontend/app/marketplace/page.tsx
#	frontend/app/page.tsx
#	frontend/components/modals/CreateOpenInternetModal.tsx
#	frontend/components/wallet/ConnectButton.tsx
Route StreamModal and CreateMessageModal through formatTransactionError, matching the other transaction surfaces. CreateMessageModal never read the useWriteContract error, so a rejected or reverted createMarkee left the modal silent.
tnrdd added 2 commits July 22, 2026 18:10
The history route pinned ALCHEMY_BASE_URL, so it scanned a different chain than every other read when NEXT_PUBLIC_BASE_RPC_URL is set.
Streaming backers emit no FundsAdded logs, so /api/account/funded found nothing for
them and a wallet that only opened streams saw an empty dashboard. Discover positions
from backerMarkee on each board instead, and price them with fetchBackerPositions:
ETHx streamed in, net of what the board's GDA refund pool paid back, mirroring how
board totals are already sourced.

Fixed and streaming discovery now run independently, so one failing does not blank
out the other.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants