Sequitor

What inspired me

I scroll on X (The Everything App) almost every day, I enjoy reading funny posts and sending them to my friends. It's also a very powerful tool for connecting and networking with others and for catching up on news and developments in the tech space. I often find however, really popular posts that I don't get or have the context to understand, this entire project aims to solve that problem (I went a little overboard with it) and help me understand the context and conversation around a specific tweet and where it comes from.

How we built it

Sequitor starts with one post and asks a different question from ordinary search: how did this idea travel? A later post might repeat the wording, paraphrase the claim, argue with it, ask a question, or make a joke (especially make a joke, this is why I love twitter).

Our backend begins by sending the user query to OpenAI's Responses API. A structured response produces a neutral context label (what is the query/post actually talking about), key entities, uncertainties, and a small set of search branches. We use those branches to query X (via its api), then let the first retrieved posts inform a secondary search pass. That second pass matters: the language people use after an announcement is often very different from the original post. We retain each post's query scope and X metadata so the interface can show where a result came from.

The frontend is in React and TypeScript. A user can start from an X link or a pasted post, watch a full-screen search transition, and then see the anchor appear before the surrounding conversation gradually forms. Server-sent events are validated, deduplicated, and placed into a client-side receive queue so things gradually flow rather than just popping up. The "Conversation Space" lays out the captured posts using their dates, semantic position relative to the original post, and observed links while the side panel keeps the selected post, context card, and lineage details readable, and the activity strip can switch between hourly, daily, and monthly views. Hovering or selecting a node brings its post forward with a direct source link, while the recorded Dario run remains explorable without provider credentials.

To support the "Conversation Space", we built a Baseten Chain to run three parts of that view: gpt-oss-120b selects a small set of posts along with its quotes and replies, and a dedicated H100 scores post pairs with a fine-tuned Jev model. The chain streams its results back through an event stream, that way we can also stream things smoothly on the front end.

We collected real posts from Bluesky news-outlet feeds (bluesky api is free, X is not), joined likely reports of the same event within specific time windows, and constructed reply pairs and negative examples as well. In total, this provieded 18,948 deduplicated labelled pairs. The Jev training used 18,079 real pairs some synthetic and personal examples. An OpenAI gpt-oss-120b model served through Baseten helped label pairs, revisit tricky cases, and generate an adversarial set of examples as well.

On an H100 in Baseten, we fine-tuned OpenJev-4B with 4-bit QLoRA and a five-class head (our training set had five classes from unrelated to verbatim copy). This slightly broader set of classification seemed to help as it gave more nuance to things like ragebait posts compared to news articles. I even held out certain days for eval and removed link urls to prevent leakage (probably super useless at this scale). One big decision was deciding on what to use for classification, it turns out that Jev is kind of made for this task. On a 170-pair eval set, fine-tuned Jev reached 0.896 F1, versus 0.852 for fine-tuned BGE-large. On a separate 25-pair challenge, it reached 0.952 F1, versus 0.909 for BGE-large (yes this is probably overfit). The only time Jev suffered was because BGE-large was much faster in an H100 single-pair benchmark (5.87 ms versus 79.70 ms median). We ultimately chose Jev due to its on par or better performance and because its cool af!

Challenges we ran into

Search phrasing was the first obstacle. An announcement, commentary, and a joke can all belong to one conversation while sharing almost no words. A single query can miss examples we wanted included, so we added distinct context branches and a second pass based on the initial retrieved posts.

The training data was kinda harder than the model code. Matching posts by a shared URL can miss independent outlets covering one event while matching by shared words confuses terms without context like swapping one meaning of an acronym with another. We mined candidate pairs across outlets and dates, tested the model-assisted labeller against hand-labelled examples, and evaluated on held-out events. Small challenge sets were useful for finding specific failures, but definitely overstate performance (a-la overfitting as seen above).

What we learned

The best retrieval pipeline is not necessarily one model or one search query. OpenAI's structured planning gives us a dynamic and tunable way to expand our search, while X supplies dated posts and explicit relationships, and Baseten lets us fan out curation and GPU scoring while streaming partial results to the front end.

We also learned that a model trained to recognize aboutness is not necessarily good at recognizing claim equivalence. Jev improved substantially after we taught it the difference between a paraphrase, a reaction, and a same-topic but different fact (our eval sent went from a 25% one shot baseline to 89% after ft). The comparison with BGE introduced an interesting trade off more nuanced classification can justify a slower model for difficult pairs, while the fast cross-encoder remains valuable for screening and fallback. In the real world, I might have chose BGE for a production deployment to support more traffic with lower cost and latency, but thats no fun!

I apologize for the use of we and our despite being a solo project, I have only spoken to my agents for 24 hrs straight.

Built With

Share this project:

Updates

Submission history