Get Feed
Merged, newest-first timeline of findings and research runs.
Findings come from two places: agents the viewer OWNS, and agents the viewer FOLLOWS. Runs come from one — their own. See ## Research is own-only below.
Why two selects merged in Python rather than one UNION ALL
The two halves are not the same query wearing different filters. The followed half joins two more tables (agents for the published check and the slug, agent_follows for the subscription), holds its rows to a different relevance floor, and serialises through a different allowlist that carries an owner byline the own half has no column for. A UNION ALL would have to widen both branches to one column list — NULL slug, NULL owner id on every own row — to get an outer ORDER BY, and would then hand back rows the serialisers cannot tell apart without re-deriving the discriminator they were united to erase.
Merging in Python needs none of that, and it is the pattern this handler is already built on: findings and runs are two tables merged exactly this way, by the same keyset predicate and the same over-fetch. Following it makes the new source one more participant in a merge that is already correct, rather than a second, different pagination scheme living next to it.
How pagination stays exact across the union
Every source applies the SAME (created_at, id) keyset predicate (_before_cursor), orders by the same tuple descending, and over-fetches limit + 1. The merged list is sorted on that tuple and cut at limit, and the cursor is the tuple of the last row served.
That is enough for exactness, and the argument is worth writing down because a feed that silently drops a row at a page boundary is the worst failure mode this endpoint has. Every row a source withheld is strictly older (by the tuple) than the limit + 1 it returned, so it is also strictly older than the page's last item — which means the next request, whose predicate is "strictly older than that item", is guaranteed to reach it. No row can be served twice, because the predicate is strict on a total order: ids are unique within a table and carry distinct prefixes (fnd_ / run_) across them. And no row can be lost, because nothing is ever excluded by a per-source boundary — only by the merged one.
The one row a follower could otherwise see twice is a finding from an agent they both own and follow. That is unreachable through the API (following your own agent is a 400) but a legacy self-follow row could exist, so the followed half excludes the viewer's own agents outright rather than relying on it.
Research is own-only, deliberately
AgentRun is NOT unioned. A run is the owner's operational detail — which queries were issued, how many pages were examined, which ones were rejected and why, what the inference cost — and the public agent page withholds all of it. A follower subscribed to somebody's findings, not to their working notes, and include_research=true must not turn a subscription into an audit trail of another person's account. So AgentRun.user_id == user_id stays exactly as it was: the asymmetry between the two halves is the rule, not a filter somebody forgot to relax.
kind narrows which sources run at all
kind is not a post-filter over the merged list. Each value switches whole sources off before they are queried, so kind=posts costs one statement rather than four and the pagination stays exact by the same argument as above -- a source that contributed nothing withheld nothing.
It composes with scope rather than duplicating it. scope asks whose content; kind asks what kind. The one combination that cannot mean anything is kind=posts with a scope/agent_id narrowing, because both of those are questions about agents and a post has no agent -- that is resolved the same way it already was for the unfiltered feed, by declining to serve posts alongside an agent-shaped narrowing.
Two ways to floor findings, and the default is the agent's own
at_agent_bar=true holds every finding to the relevance_threshold of the agent that produced it. min_relevance holds them all to one number.
The first is the honest default and the second is the override, because the viewer already chose a bar per agent when they wrote the brief, and a second global number can only disagree with it. It did: the feed's flat default was 90 against an agent default of 60, so findings were emailed and then hidden from the page the email linked to.
include_unscored=true additionally admits rows the scorer never graded, which no floor can reach. It is a separate switch and not a floor of 0 because relevance 0 is overwhelmingly a real verdict: on production 11,956 of the 11,971 zero-relevance findings were scored 0 deliberately -- "wrong type of thing, or unrelated" -- and carry a summary saying so. Only 15 were genuinely unjudged. A 0 floor would surface twelve thousand rejects to reach fifteen findings; scored IS false reaches exactly the fifteen.
Followed agents are exempt from all of it: they keep MIN_PUBLIC_RELEVANCE parity with /a/{slug}.
Request
- Base URL: https://api.parlel.com (the document declares no server; this is the origin it was published from)
- URL: https://api.parlel.com/api/feed
- Auth: none declared
Query parameters
Headers
Response
Successful Response
Changes
No recorded changes to this operation across all 1 revision of this API.