This action will delete this post on this instance and on all federated instances, and it cannot be undone. Are you certain you want to delete this post?
This action will delete this post on this instance and on all federated instances, and it cannot be undone. Are you certain you want to delete this post?
This action will block this actor and hide all of their past and future posts. Are you certain you want to block this actor?
This action will block this object. Are you certain you want to block this object?
Are you sure you want to delete the OAuth client [Client Name]? This action cannot be undone and will revoke all access tokens for this client.
Are you sure you want to revoke the OAuth token [Token ID]? This action cannot be undone and will immediately revoke access for this token.
#activitypub 108 hashtags

The previous two posts (here and here) in this series on algorithmic feeds introduced the built-in criteria judge and external custom judges.
A custom judge is a script run by the Ktistec server that decides whether a post is included in the feed or excluded from the feed based on data about the posts that the server provides in a temporary SQLite database.
There is a lot of data to work with (the owner is you, the feed's creator):
feedname : The feed's name.description : The feed's description.params : The feed's parameters.statsposting_rate_q1 : First quartile.posting_rate_median : The median.posting_rate_q3 : Third quartile.authorsposts or referenced. iri : The author's IRI.type : The actor type.handle : The author's handle.followers : The IRI of the author's followers collection.deleted : Whether the author is deleted.blocked : Whether the author is blocked.followed : Whether the owner follows the author (a confirmed follow).rate_7, rate_30, rate_90 : The author's posts per day over the last 7, 30, and 90 days, counting only posts the server has cached that aren't deleted or blocked.posts/referencedposts holds the posts to judge. referenced holds the posts replied to or quoted, one level deep. Both tables have the same columns.iri : The post's IRI.type : The object type.name : The post's name (title).summary : The post's summary (content warning).content : The post's content as HTML (Markdown is converted).language : The post's language.sensitive : Whether the post is marked sensitive.published : When the post was published.to, cc, audience : The addressing, as arrays of IRIs.attachments : An array of {url, media_type, caption}.in_reply_to : The IRI of the post this one replies to.quote : The IRI of the post this one quotes.thread : The IRI of the thread's root.hashtags, mentions : Arrays of {name, href}.labels : An array of the owner's private labels on the post.author : The author's IRI.deleted : Whether the post is deleted.blocked : Whether the post is blocked.timeline : "create" or "announce" if the post is in the owner's timeline, otherwise NULL.poll : {options, multiple_choice, closed_at}, but only when type is "Question".This data is used in the example judges shipped with Ktistec.
The polls judge builds a feed consisting of polls. It includes only posts whose type is "Question". It would be very easy to extend this judge to filter on single-choice vs. multi-choice or on polls about specific topics.
The quiet_posters judge is a little more complex. The server provides statistics on all of the actors you follow, as well as the author's posts per day over the last 7, 30, and 90 days. A post is included only if you follow its author and that author's 90-day posting rate is at or below the median, so the quieter half of your follows aren't drowned out by the noisier ones.
As you might imagine, there's a lot of value in judges built around statistics, social signals, and interactions. This is the area where I'm now putting most of my effort.


Servers
- Ktistec v3.15.0
- Smithereen v1.1
- WriteFreely v0.17.3
- Mbin v1.11.1
- Mitra v5.13.0
- Cookifed v0.2.0
- Misskey v2026.11.0
- Hatsu v0.3.6
- NeoDB v0.20.1
- Frogue: A ForgeFed-enabled software forge
Clients
- Summit v1.88.2
- RaccoonForFriendica v1.2.0
Tools and Plugins
Protocol
- FEP-c3ab: Referencing Existing Objects in ActivityPub Client-to-Server Create
Articles
- ActivityPub Note: Streams, Collections, and Endpoints
- Fediverse Report – October 2026
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a1b291-5e93-7351-993c-8e19734d0be6

In the last post in the series on algorithmic feeds in Ktistec, I introduced the built-in criteria judge, which renders verdicts on posts based on the presence or absence of terms, like hashtags and keywords.
In this post, I introduce custom algorithmic feeds by showing you what a simple implementation of the criteria judge looks like if rewritten as a custom judge. Custom judges are nothing more than scripts, external to the server, that the server runs to judge candidate posts.
Here is the custom criteria judge:
#!/usr/bin/env python3
# ktistec: batch-size=250
# ktistec: param=any required
# ktistec: param=all
# ktistec: param=none
import html, json, re, sqlite3, sys
def terms(value):
return [t.strip().lower() for t in (value or "").split(",") if t.strip()]
db = sqlite3.connect(sys.argv[1])
params = json.loads(db.execute("SELECT params FROM feed").fetchone()[0])
any_terms = terms(params.get("any"))
all_terms = terms(params.get("all"))
none_terms = terms(params.get("none"))
posts = db.execute("SELECT iri, name, summary, content, hashtags, mentions, labels FROM posts")
for iri, name, summary, content, hashtags, mentions, labels in posts.fetchall():
text = " ".join(filter(None, (content, summary, name)))
text = html.unescape(re.sub(r"<[^>]+>", " ", text)).lower()
hashtags = {h["name"].lower() for h in json.loads(hashtags)}
mentions = {m["name"].lower() for m in json.loads(mentions)}
labels = {label.lower() for label in json.loads(labels)}
def hit(term):
if term.startswith("#"):
return term[1:] in hashtags
if term.startswith("@"):
return term[1:] in mentions
if term.startswith("§"):
return term[1:] in labels
return term in text
included = not any(map(hit, none_terms)) and any(map(hit, any_terms)) and all(map(hit, all_terms))
db.execute("INSERT INTO answers (iri, included) VALUES (?, ?)", (iri, int(included)))
db.commit()It's simple, and it's not a perfect work-alike, but it shows the overall structure.
The Ktistec server runs judges, both built-in and custom, when there are candidate posts. Given a batch of posts, the judge makes exactly one decision: is each post included in the feed or excluded from the feed?
To do this, a judge is given the path to a temporary SQLite file (in sys.argv[1] in the example above) with information about the posts in the batch and their authors, as well as information about the feed itself.
Most of the time, posts arrive one at a time and the script runs on each post as it arrives. But the SQLite database can contain multiple posts—and it will when previewing or backfilling a feed—so the script must iterate over every post and judge each post independently.
In this example, the judge strips the post of markup and then looks for terms that match. It writes a verdict on the post into the answers table.
The front matter at the top of the file tells the server some things about the judge: the maximum batch size (the default is 1), and what params the judge expects the feed to provide. In this case, it expects three params, and the first is required. The params allow a user to create multiple, parameterized feeds from a single judge.
The params appear as inputs on the feed editor page.

The "Judge" dropdown at the top of the form only appears when you create a feed, and only when there are custom judges. You make a script a custom judge by placing the script in the <root>/etc/judges directory, making it executable, and waiting a few minutes until the server picks it up, or restarting the server. Here the custom criteria judge was saved as <root>/etc/judges/criteria-alike.
The example is in Python, but you can use the language of your choice.
In the next post, I'll go over what else in the SQLite database supplied to the judge is useful for judging.

Since v3.13.0, Ktistec has supported custom feeds.
A custom feed is a collection of posts selected by a set of rules called a judge. Any post Ktistec has cached that is public, or was sent to you, is a candidate. The judge's job is to decide which posts are in the feed and which posts are out.
Ktistec comes with one built-in judge called the criteria judge. It also supports any number of fully custom external judges—judges implemented as external scripts the server can call on candidate posts.
I use the criteria judge for most of my custom feeds. This post explains how it works. Later posts will dive into external judges.
The Criteria Judge
The criteria judge allows you to define feeds by flexibly matching on keywords, hashtags, and mentions. You can filter candidates to those posts that contain any of one set of keywords, hashtags, or mentions, all of a second set, and none of a third.

Keywords match anywhere in a post's text, summary, or title, ignoring case—even inside words, so "art" also matches "party". Hashtags and mentions match exactly: #art matches only posts tagged #art. Use a hashtag when you want precision. Use a keyword when you want reach.
Once you've settled on terms, you can preview the feed. To keep it fast, the preview returns up to 20 matches from the 100 most recent candidates—just enough to give you a feel for what it will look like. "Create Feed" publishes the feed and starts a task that backfills the last 30 days of posts. (Feeds are private, though—only you can see them.)
After it is published, the feed is available in the deck—the column-oriented feed reader. The deck shows up to 12 feeds side by side, and you can reorder them.

Matching is fast. I have feeds with 20+ keywords and hashtags, and there is no perceptible processing delay when a new post arrives.

This release ("savor the moments when goodness prevails") of Ktistec lands two big new features: RFC 9421 HTTP Message Signatures and private labels.
RFC 9421 support is inbound only, for the moment. The code for signing activities is written, but I want to test parsing and verification in the real world first. Signing will ship in the next release.
Private labels allow you to annotate posts with your own hashtag-like labels. Once posts are labeled, you can view them together on an index page, just like hashtags. You can use them in custom themes, and in the next release, you will be able to include them as criteria in custom feeds. Because they are private, they are not federated.
Here's the full changelog:
Added
rsa-v1_5-sha256).totalItems in label, hashtag, and mention collections.Fixed
Delete only when its origin serves it.Changed
I'll be publishing a series of posts on writing custom algorithmic feeds. Stay tuned!


Servers
- Pleroma v2.11.0
- Hollo v0.9.25
- Betula v1.9.0
- Mastodon v4.7.4
- ActivityPub for WordPress v9.4.0
- Bookwyrm v0.9.4
- PeerTube v8.3.2
- Misskey v2026.10.1
- Ktistec v3.14.0
- NeoDB v0.19.10
- Iceshrimp.NET v2026.1.3-beta
- NodeBB v4.16.5
- wanderer v0.21.2
- Wafrn v2026.10.03
- Lemmy v0.19.21
- snac v2.97
- Trunk & Tidbits, September 2026 (Mastodon)
Clients
- Pachli v3.10.0
- tooi v0.28.0
- RaccoonForFriendica v1.1.8
Tools and Plugins
- Event Bridge for ActivityPub v2.0.0 (WordPress plugin)
For developers
Protocol
- FEP-65ca: Common Client Configuration
- FEP-d1cb: Same-origin actor re-identification
- FEP-f799: Coordinated Inbox Delivery Suspension
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a16a51-7f9e-74b3-8b8e-5fc3d2c8dc9e

v3.14.0 ("question authority") is the end of a long road.
The goal was pluggable, algorithmic feeds. "Pluggable" because I want to drop a script into a directory to add a new algorithm—not recompile the server. "Algorithmic" because I want predictable, fine-grained control over what I see and how it's organized.
I've been working on this feature, across releases, for almost four months. A recent thread gave me the motivation to just get it done!
The discussion in that thread also shaped the result: algorithmic feeds ships with four example algorithms (called "judges") that demonstrate what's possible:
One interesting implementation decision: the exchange format between server and judge is an SQLite database holding the batch of posts to be judged. Rather than rolling a custom data exchange format, you get a well-tested file format that brings schema support, serialization/deserialization, and a query language nearly for free.
Here is the full set of changes in this release:
Added
etc/judges.Fixed
Changed


Servers
- Hubzilla v11.4.2
- TinyAP v0.2.1
- Iceshrimp v2026.5.4
- GoToSocial v0.22.3
- Wafrn v2026.10.02
- Castopod v1.15.6
- NeoDB v0.19.9.1
- Merp Relay v0.4.3
- NodeBB v4.16.4
- Loops Server v1.0.0-beta.15
- PieFed v1.8.6
- wanderer v0.21.1
- 5.0: A closer look (Mastodon)
Clients
- mlmym v26.4.1
- Summit v1.88.0
- RaccoonForFriendica v1.1.5
Tools and Plugins
For developers
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a1464f-ff2a-7932-98fa-d2e25de0b63a



There’s a bot for that
Five new fediverse accounts that post what FediDB sees: charts of the network, new software, new large servers, release adoption and the projects growing fastest.


Servers
- GoToSocial v0.22.2
- Hollo v0.9.23
- snac v2.96
- Funkwhale v2.0.12
- Vernissage Server v1.44.0
- FitPub v1.3
- Hometown v1.3.1
- yukimochi/Activity-Relay v2.0.12
- Iceshrimp v2026.5.2
- NeoDB v0.19.8
- Forgejo v0.17.0
- NodeBB v4.16.3
- PieFed v1.8.5
Clients
- IceCubesApp v2.1.6
- Mastodon Bird UI v4.7.3-2026-10-11
- RaccoonForFriendica v1.1.3
- chan-fe v0.5.0
Tools and Plugins
- Poduptime v6.3.3
- FediFetcher v8.2.3
- Enable Mastodon Apps v1.7.0 (WordPress plugin)
For developers
Articles
- Some quick thoughts on Unit Testing ActivityPub
- Threads and the Fediverse: The Threat That Wasn’t
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a12226-89dc-7182-949d-94663546af6c