HDev Team HAF Updates - Waggle & HAF FYP: A For You Feed for Hive
This is the second post in our development-update series. The prior post covered our core-project analytics work. Today is about Waggle — an algorithmic "For You" discovery layer for Hive, built as an open HAF app, plus the app we built on top of it so you can actually feel what it does.
Hive has never had a personalized feed. Trending and Hot are the same for everyone, tag feeds are manual, and a brand-new account lands on an empty feed with no idea where to go. Every mainstream social app answers this with a per-user ranking layer — a feed that differs depending on who's looking. Hive didn't have one. So we built it.
(A note on the name: a bee's waggle dance is how the hive tells everyone else where the good stuff is. That's the whole product in one word.)
To be clear up front: this is an addition to Trending / Hot / New, not a replacement for them — a fourth, personalized lens that any Hive front-end can adopt. It's early, it's running on one node so far, and we're still tuning it. More on all of that below.
The work in this post:
- haf_fyp — the open-source HAF app that does the ranking. This is Waggle's engine, and it's usable by any Hive front-end, not just ours.
- Waggle — a TikTok-style vertical feed app built directly on that API. (Live: hdev-core.github.io/waggle)
- HivePulse — our browser extension, which now carries a ✨ For You tab alongside trending/tag/new, powered by the same API. The first proof that this drops into an existing front-end.
The short version
Waggle watches what you already do on-chain — the posts you upvote, the ones you reblog, the communities you subscribe to — and uses that to rank fresh content for you. No tracking pixels, no ad profile, no off-chain surveillance: the input is your public Hive history, and nothing else.
Two things make it different from a normal recommendation feed:
- It's open, and it's inspectable. The ranking code is public, the weights are published in the API, and every post in your feed can tell you why it's there — tap "Why this post" and you get the actual per-signal scores that produced it.
- It plugs into Hive rather than replacing it. The feed endpoints return posts in the exact shape Hivemind's
bridgeAPI already returns, so any existing Hive front-end can render a Waggle feed with essentially no work.
This is a fourth sort, not a replacement
Hive gives you Trending, Hot and New. Those are global orderings — everyone sees the same list, and that's a feature: they're transparent, predictable, and stake-legible. We're not proposing to replace any of them.
For You is simply a fourth lens, and the only one that differs per person. It's the one Hive has been missing, and it's the one every user coming from a mainstream social app expects to find. Adding it is additive value for the whole ecosystem — and because the API returns the same object shape as bridge.get_ranked_posts, any front-end can add it as one more tab next to Trending/Hot/New.
That's not hypothetical. We've already shipped it in our own HivePulse browser extension, which now carries a ✨ For You tab beside trending/tag/new — personalized when you're signed in, the global ranked feed when you're not, with a short "why you're seeing this" line on each post. It took very little work precisely because of the bridge-shape compatibility. Any other Hive front-end could do the same.
We'd genuinely like other front-ends to pick this up. It's open source, it's a HAF app, and it's designed to be adopted rather than owned.
One caveat we'd rather state early than bury: for now, any front-end adding this would be pointing at our node, since ours is the only one running haf_fyp today. Getting it into the standard haf_api_node stack — so any operator can serve it — is the next step, and we're on it.
The For You feed. Note the "For You" chip on the card — every post tells you whether it came from your personalized feed or the global one.
Try it right now (no login, these are live):
- Personalized feed: /v1/fyp/feed/mcfarhat
- Global feed: /v1/fyp/global
- The live algorithm + weights: /v1/fyp/algorithm
How the feed actually decides what you see
This is the part we most want people to understand, so here it is in plain language first.
Every recent post gets a score out of 1. That score is a blend of four signals:
| Signal | Weight | What it asks |
|---|---|---|
| Relevance | 40% | Is this post about the kind of thing you usually engage with? |
| Recency | 30% | Is it fresh? |
| Engagement | 20% | Are other people actually responding to it? |
| Credibility | 10% | Is the author established and staked in Hive? |
Then a community boost is applied on top: a post from a community you're subscribed to (or picked when you signed up) gets a +20% lift; a post from a community you merely engage with a lot gets +10%. A post never gets boosted twice, and the score is capped at 1.
And you can see all of it, for any post, right in the app:
Tap "Why this post" on any card. These aren't illustrative numbers — they're the actual scores that produced this ranking, served by the API. This post scored 65 on relevance, 99 on recency, 96 on engagement and 94 on credibility, took the ×1.2 boost because the reader subscribes to that community, and came out at rank #1.
We think an algorithmic feed you can't interrogate is a bad deal. So ours shows its work.
A few things worth spelling out, because they're the interesting bits:
"Relevance" is semantic, not keyword-matching. Waggle doesn't look at tags at all. Instead, every post is turned into a 1,024-dimension vector — a numeric fingerprint of what the post means — using HiveSense, Hive's embedding app. Your own taste is turned into a vector the same way: we take the posts you've recently upvoted and reblogged, and average their fingerprints into a single "this is what you like" vector. Relevance is then simply how closely a post's fingerprint points in the same direction as yours. That's why it can surface a post about, say, urban beekeeping even if it shares no tags with anything you've read — it understands the subject matter rather than string-matching it.
Your taste vector decays. Recent activity counts for more than old activity, and a reblog counts for more than an upvote (you're vouching harder). We look at your last 200 interactions over 90 days, weight each by how recently it happened, and average.
Recency has a 24-hour half-life. A post that's a day old is worth half a brand-new one on that signal; two days, a quarter. This is what stops a permanently-high-scoring "greatest hits" feed from calcifying.
Engagement is deliberately hard to buy. It's built from unique commenters, reblogs and rshares — but each is log-compressed before being combined, so the 500th upvote adds far less than the 5th. A whale self-voting doesn't dominate; a post lots of different people are talking about does.
Credibility is stake- and reputation-aware, at only 10% — enough to damp spam and fresh throwaway accounts, not enough to make the feed an oligarchy.
What if you're brand new and have no history?
This is the empty-feed problem, and it's the one thing a ranking engine can't solve with on-chain history — because you don't have any.
So on first run Waggle shows you a community picker. You pick what you're into, and we synthesize a starting taste vector for you: we take the top-performing posts from the communities you chose, average their fingerprints, and use that as your seed. You get a genuinely personalized feed immediately, not a generic one.
Under the hood, saving your interests pings the ranker directly (over Redis) instead of waiting for the next scheduled pass, so your first real feed lands in seconds. The app polls for it and flips your feed from global to personalized the moment it's ready — you'll see a "Personalizing your feed…" banner, and then it just changes under you.
Until that vector exists, relevance is simply dropped and the remaining three signals are re-weighted to fill the gap — so a cold feed is still ranked sensibly (fresh + well-received + credible), just not yet yours.
And you can always tell which one you're getting: every post in every response carries an fyp.source field, reading either personalized or global. No guessing, no extra API call.
Signing your interests — without giving anything away
Declaring your interests writes to our database, so we have to know it's really you. But we're not going to run passwords, and we're certainly not asking for your keys.
Instead, the app asks your wallet to sign a short message with your posting key. The server recovers the public key from that signature and checks it against your account's real on-chain posting authority. If it matches, the write is accepted.
This is a signature, not a transaction. Nothing is broadcast to the chain, it costs nothing, it burns no Resource Credits, and your private key never leaves your wallet.
Waggle — the app
To make all of that tangible, we built Waggle: a full-screen, vertical, swipe-through feed for Hive.
- Two tabs — For You and Discover. Discover is the global ranked feed and needs no login at all; For You is yours.
- Everything plays inline. Images, YouTube (including Shorts), 3Speak, and native video all render right in the card, with shared controls — seek, 1×/1.5×/2× speed, volume — and your preferences carry across every post. Only the post you're actually looking at plays; the rest unmount so your browser isn't quietly buffering ten videos.
- Every action is a real on-chain Hive operation. Upvote (double-tap the card, or pick a weight), reblog, follow, and comment are all built and signed client-side in your wallet and broadcast to Hive. They're not Waggle actions — they're Hive actions, visible everywhere else on Hive. And they flow straight back into the ranker as fresh signal.
- Sign in with Hive Keychain or HiveAuth. HiveAuth means mobile works too — QR on desktop, deep-link on your phone. There's also a read-only mode: type your username, get your personalized feed, sign nothing.
- "Why this post." Tap it on any card for the actual score breakdown — relevance, recency, engagement, credibility, whether a community boost applied, and its final rank. We think an algorithmic feed you can't interrogate is a bad deal, so we made ours explain itself.
- Sensitive content is gated, not silently dropped.
nsfw/nsfltags and Hivemind's own moderation signals blur the post and disable autoplay behind the blur, with a tap to reveal.
| Discover — the global ranked feed, no login needed. | Reader — the full post, rendered inline, with vote / comment / reblog / follow right there. |
Sign in with HiveAuth (works on mobile) or Keychain — or skip signing entirely and browse your feed read-only. Your default upvote weight is yours to set.
It installs to your home screen as a PWA, and it ships as static files — there is no Waggle backend. The browser talks straight to the HAF node.
Where this actually stands — please read this bit
We'd rather set expectations honestly than oversell a launch.
It only runs on our node right now. Everything in this post is served from testapi.hivescan.info, and ours is currently the only public node running it. haf_fyp isn't yet part of haf_api_node — the standard stack HAF API nodes deploy — so for the moment it isn't something an operator can simply switch on. Getting it there is the natural next step, and we're working on it. Until then: if you build against it today, you're pointing at our infrastructure, and we'd rather say that plainly now than have someone discover it later.
It's a work in progress. Expect rough edges. This is early software shipped deliberately early, because a discovery algorithm is worthless without real users generating real signal. There will be bugs, there will be posts in your feed that make you go "why on earth is that here", and there will be gaps — we've listed several ourselves in the technical section below rather than hiding them. We're actively working on it, and feedback is the whole point: tell us when the feed gets it wrong. That's more useful to us than praise.
The algorithm is not fixed — and it's the node's to choose. The weights live in a database table, versioned and served live at /v1/fyp/algorithm. Any node running haf_fyp can adjust them, or activate an entirely different scoring version, without touching a line of code. What's live today (v1.1.0) is our current experiment, not a pronouncement — we fully expect to tune it, and we may end up running something quite different once we see how it behaves at scale.
That's a deliberate design choice, and worth being clear about: this is not an attempt to install one canonical algorithm at the heart of Hive. Different nodes could run different rankings, and front-ends could offer a choice between them. On a chain whose whole premise is that no single party controls the feed, a pluggable, inspectable, operator-chosen algorithm is the only version of this idea that belongs here.
For the more technical
The stack
hived ──► sql_serializer ──► HAF PostgreSQL
│
haf_fyp_app schema ◄── PL/pgSQL block loop
post_candidates · engagement_metrics
user_communities · user_interactions
│
Python ranker (5-min cycle) ◄── Redis: on-demand refresh
scores → feed_cache / global_feed_cache
│
PostgREST + FastAPI (writes)
│
nginx rewriter → clean REST paths
It's a stock HAF app: hived pushes blocks into PostgreSQL via sql_serializer, and a PL/pgSQL loop (hive.app_next_iteration()) processes each block range into the app's own schema — root posts into post_candidates, comments/reblogs/votes into engagement_metrics, community subscribes into user_communities, and each user's votes/reblogs into user_interactions. Fork handling is HAF's problem, not ours.
Scoring runs outside SQL, in a Python daemon, on a 5-minute cycle over a candidate pool of recent + still-in-payout-window posts.
The scoring function
Weights live in an algorithm_versions table and are served live at /v1/fyp/algorithm. Active version is 1.1.0:
final = 0.40·relevance + 0.30·recency + 0.20·engagement + 0.10·credibility
then, multiplicative and capped at 1.0:
subscribed / declared community → × 1.20
engaged (top-5) community → × 1.10
The four signals, precisely:
relevance— cosine similarity between the user's interest vector and the post's embedding, mapped from[-1,1]to[0,1]as(sim+1)/2. Embeddings are HiveSense'shalfvec(1024), averaged across chunks for long posts. No embedding available → neutral0.5.recency—exp(-ln2 · age_hours / 24). A clean 24-hour half-life.engagement—log1p(unique_commenters) + log1p(reblogs) + log1p(rshares/1e9), pushed through a sigmoid (k=0.5, midpoint3.0). Note it uses unique commenters, not comment count, and never raw vote count.credibility—0.6·norm(reputation) + 0.4·norm(vests), sourced fromreptrackerand the balance tracker. Unknown author →0.3.
The interest vector: last 200 interactions within 90 days, each weighted action_weight · 0.95^age_days (vote 1.0, reblog 1.5), averaged over those posts' embeddings and L2-normalised into a vector(1024). Cold start instead averages the embeddings of the top-50 posts by rshares in the user's declared communities. With no vector at all, relevance is zeroed and the remaining weights are renormalised over 0.60 (→ recency 0.5, engagement 0.33, credibility 0.17).
Two honest notes, since we'd rather say them than have you find them: there is currently no feed diversification — no per-author cap, no MMR — ranking is a straight sort by final score. And the similarity is computed in-process with numpy over embeddings fetched by post id; haf_fyp does not run a pgvector ANN index search (the HNSW work lives in HiveSense, a separate app). Both are on the list.
Bridge compatibility, properly
The feed endpoints don't return a bridge-like object. They call Hivemind's own get_full_post_view_by_id() and create_bridge_post_object(), so payout maths, reputation and vote state are never reimplemented and the output stays correct as Hivemind evolves. Waggle's data is merged in as a nested fyp object:
"fyp": {
"rank": 1, "source": "personalized", "final_score": 1.0,
"score_relevance": 0.652, "score_recency": 0.989,
"score_engagement": 0.962, "score_credibility": 0.939,
"community_boost_applied": true, "boost_source": "subscribed"
}
That's what makes this drop-in for any Hive client: if your front-end already renders bridge.get_ranked_posts, it can render this.
The API surface: GET /v1/fyp/feed/{user}, /v1/fyp/global, /v1/fyp/profile/{user}, /v1/fyp/explain/{user}/{post_id}, /v1/fyp/algorithm, plus a signed POST /v1/fyp/interests. Full OpenAPI spec at the API root.
If you want to add a "For You" tab to your front-end
The minimum viable integration is genuinely small, and HivePulse is the worked example:
- Add a sort key next to
trending/hot/new. - Point it at
GET /v1/fyp/feed/{user}when a user is signed in, andGET /v1/fyp/globalwhen they're not. Today that base URL has to betestapi.hivescan.info— ours is the only node servinghaf_fyp. Keep it configurable, so it can point elsewhere the moment another node runs it. - Render the response with the same component you already use for
bridge.get_ranked_posts— it's the same object.
That's it. Everything else is optional polish: read the nested fyp object if you want to show why a post surfaced (HivePulse renders a one-line reason from boost_source and the scores), and read fyp.source to tell the user whether they're seeing a personalized feed or the global fallback.
The interest picker (POST /v1/fyp/interests) is only needed if you want to solve cold-start for brand-new accounts; it's the one endpoint that requires signing.
Swapping the algorithm
Scoring weights are rows in an algorithm_versions table, not constants in the source. The active version is served at /v1/fyp/algorithm, and a node operator can activate a different one through an admin endpoint without redeploying anything. Version 1.0.0 (flat community boost) and 1.1.0 (the tiered boost, currently active) both ship seeded.
This is the mechanism behind the claim earlier in the post: the algorithm belongs to whoever runs the node. If you run haf_fyp, the ranking your users get is yours to set — and if you think our weights are wrong, you can simply run different ones and we'd be interested to hear how it goes.
Interest-write auth
Posting-key signature over a canonical challenge string binding username, timestamp and the sorted, de-duplicated community id list (so a captured request can't be replayed with a different body). The server rebuilds the string, recovers the pubkey from the 65-byte graphene compact signature, fetches the account's on-chain posting authority, and requires the recovered key to clear weight_threshold on its own — a single key of an N-of-M multisig is rejected. Signatures expire after 300s. The node URL used for that authority lookup has no default and fails closed: a misconfigured deploy returns 503 rather than trusting an attacker-supplied key.
Known gap, stated in the code: delegated posting authority (account_auths) isn't resolved yet, so signing with a delegated account's key is a false negative.
Performance work
This cycle was mostly about making the feed path fast enough to be a real-time UI. It now serves a 20-post page in ~0.8s end-to-end against mainnet-scale data (~0.44s TTFB), from a cold HTTP client.
- Precomputed
hive_post_id. haf_fyp's post ids are HAF operation ids, not Hivemind ids, so every feed request used to run a 3-way join into Hivemind to bridge the keyspaces. The processor now resolves it once and stores it, turning the feed into a single primary-key join. - Cached rendered bridge JSON. The dominant per-request cost was the Hivemind bridge build itself, at ~25–30 ms/post — brutal at 20 posts/page. Those fields are volatile only during the ~7-day payout window and then freeze, so the ranker now pre-renders the observer-agnostic bridge object into
post_bridge_cacheand endpoints read it, falling back to a live render on a miss. - A partial index that gave the whole node back. The per-block payout-window
UPDATEwas sequentially scanning all ofpost_candidates— 25M rows, 17 GB — every ~3 seconds, which came to roughly 98% of the app's per-block CPU and was starving the other HAF apps on the same box. A partial index on(created_at) WHERE payout_windowturned it into a sub-millisecond range scan. Node load dropped from ~5.7 to ~1.6, and the block explorer stopped throwing 503s. - Page size capped at 20, so enrichment cost is bounded per request. (Pagination uses
page/page-size, notlimit/offset— PostgREST reserves those.) - Graceful degradation throughout: if HiveSense or the credibility apps are unreachable, the ranker falls back to neutral relevance/credibility and keeps producing feeds rather than failing the cycle.
The app
React + Vite + TypeScript, no backend and no BFF — the browser calls PostgREST directly (the node already sends Access-Control-Allow-Origin: *). State is TanStack Query; the pager is CSS scroll-snap rather than JS; two IntersectionObservers separate mount/preload (~1.2 screens out) from play (centred), so video is prefetched but only ever one plays.
Ships with 39 unit tests plus 6 live contract tests — the latter run against the real API and assert the cold-start shape (score_relevance nullable, unknown user still returns has_vector: false) and that a real 3Speak manifest is fetchable and CORS-open. Those exist because every one of the upstream surprises above bit us at least once.
The code
15 MRs merged this cycle on haf_fyp (26 total). It runs on the same self-hosted node as everything else in this series (testapi.hivescan.info), alongside Hivemind, HAF Block Explorer, Balance Tracker and haf_stats — it composes with the stack rather than sitting beside it. The Waggle app deploys from main to GitHub Pages on push.
What we're asking for
Try it and tell us when it's wrong. A ranking algorithm can't be evaluated in a vacuum — it needs real people with real taste telling us the feed missed. Bug reports and "this recommendation made no sense" are both genuinely useful.
Front-end devs: you're welcome to try it — but know what you'd be depending on. Adding a For You tab is a small change (see above), and the code is open source. The one honest catch: ours is currently the only node serving it, so today that tab would point at testapi.hivescan.info — our infrastructure, our uptime, our algorithm version. Keep the base URL configurable and it moves the moment that changes. Getting haf_fyp into the standard haf_api_node stack is the natural next step, and it's something we're working on.
Join our discord
Next up in the series: the new developer cohort we've brought onto Hive this cycle — 15 engineers and the apps they're building — plus a spotlight on Vylor, an AI voice-first finance app coming to Hive. We'll share it in a couple of days.
- Support our witness
@actifit on Hive — vote for us or set us as proxy on actifit profile, or via peakd, ecency, hive blog or hivesigner.
- Support our witness
@actifit-he on Hive-engine — vote for us on Tribaldex.
HDev Team
Leave HDev Team HAF Updates - Waggle & HAF FYP: A For You Feed for Hive to:
Read more #hive-139531 posts
Best Posts From hdev
We have not curated any of hdev's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.
More Posts From hdev
- HDev Team Updates — New Developers Cohort, New Hive Apps, and Vylor (Voice-First AI Finance)
- HDev Team HAF Updates - Waggle & HAF FYP: A For You Feed for Hive
- HDev Team HAF Updates June - New HAF Analytics & Explorer Dashboards
- HivePulse v1.9.0 — The Action Update: Firefox, FYP, and more
- HivePulse v1.7.0: The New SEO + GEO Post Analyzer for Hive
- HivePulse v1.6.0: Multi-Account, Full Wallet & Smarter Notifications! 🔔💼
- HDev Team HAF Updates May 2026 - Front End, Expanded APIs, FYP & Stats HAF Apps
- HivePulse v1.5.0: Your Hive Wallet, Now Supercharged! 💰 Hive-Engine Assets, Node Control & More
- HivePulse v1.4.0: The Heartbeat of Hive Gets Stronger! ✨ Real-time Prices, User Notifications & More
- Introducing HivePulse ⚡ The heartbeat of your Hive experience — Now LIVE on Chrome Web Store!