feat(streaming): Superfluid streaming pricing strategy - #9
Draft
tnrdd wants to merge 29 commits into
Draft
Conversation
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.
… a bare Stream label
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
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.
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.
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.
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.
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.
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.
# 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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
StreamingBoardDetail, unified under/markee/[address](strategy detected on-chain by contract version).useStreamingMarkeesranks byeffectiveRate.CreateMessageModalto add a message,StreamModalto open/adjust a Superfluid CFA flow (single-tx wrap + permit + createFlow)./api/streaming/leaderboards): ranks boards by cumulative ETHx streamed, reconstructed from SuperfluidFlowUpdatedlogs (totalFundsRaw), witheffectiveRateRawfor the $/mo label./api/streaming/registerfor board metadata.useStreamingBoardTotal+useFlowingAmountcount streamed amounts up between on-chain refetches./api/cron/streaming-keeper+lib/streaming/keeper.ts, plus fork/seed/verify scripts underfrontend/scripts/.lib/strategy.ts+StrategyBadge(strategy orthogonal to vertical),lib/superfluid/streaming.tsencoding helpers, flow-rate ($/mo) labels in marketplace listings.NEXT_PUBLIC_BASE_RPC_URLorigin inconnect-src; repeated contract ABIs deduped intolib/contracts/abis.ts; view writes made production-only (VERCEL_ENV === 'production') so previews/local read through but never increment the shared KV store; root.gitignoreadded.Status / open items
STREAMING_ENABLEDand the API is inert untilNEXT_PUBLIC_STREAMING_FACTORYis configured, so merging does not activate streaming in production on its own.