Graze
    MarketplaceHelpBlogAboutLogin
    Docs
    • Introduction
    • Creating Your First Feed
    • Nodes for Beginners
    • Custom Sorting
    • Testing and Fixing your Feed
    • Debugging a Post
    Guides
    • Dashboard Overview
    • Page Layers and Audiences
    • Using AI Nodes
    • Natural Language and Ask a Question Nodes
    • Feed Gym
    • Feed Feedback and Personalization
    • Content Moderation
    • Sticky Posts
    • Subscription Lists
    • Graze For Communities
    • Collaborators
    • Curation Desk
    • Feed Pulse
    • Version History
    • Using Custom Nodes
    • Migrating a Feed
    • Sponsored Posts
    • Embedding Feeds
    API
    • Editor Documentation
    • Ratio Comparison Node
    • Rate Limits
    • Custom Node API
    • Contrails
    • Graze Turbostream
    • API Documentation: Privacy Compliance Endpoints
    Help
    • Frequently Asked Questions
    • Feature Requests
    • Marketplace
    Getting started

    Contrails

    Stream the posts that match any Graze feed to your own app in real time, over a WebSocket.

    Contrails gives developers a live stream of the posts that match a Graze feed. Instead of reading the whole Bluesky firehose and filtering it yourself, you build the filter as a feed in Graze's editor and receive only the posts that pass it, as they arrive.

    It's a good fit for live dashboards, bots that react to a narrow slice of Bluesky, and moderation or analytics tools watching one topic.

    Getting started

    1. Build a feed in Graze. The feed's Rules decide which posts reach your stream.
    2. Turn on Contrails. Open the feed's Settings and switch on Contrails under Feed Settings. Without this, you can still connect, but you will receive no posts.
    3. Find the feed's address. Either its Bluesky link (for example https://bsky.app/profile/example.com/feed/news) or its AT-URI (for example at://example.com/app.bsky.feed.generator/news) works.
    4. Connect to wss://api.graze.social/app/contrail?feed=<feed address>.

    You can try it from the command line with websocat:

    websocat "wss://api.graze.social/app/contrail?feed=at://example.com/app.bsky.feed.generator/news"

    What you receive

    Every message is a JSON object. There are five kinds:

    • A matching post. The post's event exactly as Bluesky's Jetstream delivers it (with did, time_us, kind and commit), plus an _id field. Save the latest _id you've handled: it's your place in the stream.
    • A replayed post. The same, plus "_replay": true. When you connect without a cursor, you first get the 25 most recent matches. On a quiet feed these can be days old, so don't treat them as new.
    • A heartbeat. {"pong": true}, sent every 10 seconds.
    • A gap notice. {"_gap": true, "requested_since": …, "oldest_retained": …, "stream": …}. You asked to resume from a point that is no longer kept, and the matches between those two ids are gone.
    • An error. {"error": "…", "status_code": 404} if the feed can't be found, or 400 if your cursor isn't valid. The connection then closes.

    Ignore any other fields starting with _ that you don't recognise.

    Resuming where you left off

    Add since to pick up after a disconnect:

    wss://api.graze.social/app/contrail?feed=<feed address>&since=<last _id>

    With since, there is no replay: you get every match after that point, then live posts. since accepts:

    • an _id from a message, such as 1760000000000-0
    • a time in milliseconds since 1970, such as 1760000000000
    • $, meaning "only new posts from now on"

    Anything else is rejected with a 400 error rather than quietly ignored.

    Only a short history is kept for each feed: about 3,000 matches. For a very busy feed that can be as little as 30 seconds, so reconnect quickly. If you reconnect too late, you'll get a gap notice.

    Keep a watchdog running

    Connections do drop, and they don't always tell you. Sometimes you get a close message, sometimes the connection just breaks, and occasionally it goes silent while still looking open.

    The heartbeat is how you notice. If nothing at all arrives for about 25 seconds, close the connection and reconnect with `since` set to your last `_id`. Any message, including a heartbeat, counts.

    Example in JavaScript

    const FEED = "at://example.com/app.bsky.feed.generator/news";
    let cursor = null;
    let watchdog;
    
    function connect() {
      const params = new URLSearchParams({ feed: FEED });
      if (cursor) params.set("since", cursor);
      const ws = new WebSocket(`wss://api.graze.social/app/contrail?${params}`);
    
      const resetWatchdog = () => {
        clearTimeout(watchdog);
        watchdog = setTimeout(() => ws.close(), 25000);
      };
    
      ws.onopen = resetWatchdog;
      ws.onclose = () => {
        clearTimeout(watchdog);
        setTimeout(connect, 1000);
      };
      ws.onmessage = (event) => {
        resetWatchdog();
        const msg = JSON.parse(event.data);
        if (msg.pong) return;
        if (msg._gap) { console.warn("Some matches were missed", msg); return; }
        if (msg.error) { console.error(msg); return; }
        cursor = msg._id;
        if (msg._replay) return; // older match sent on connect
        handlePost(msg);
      };
    }
    
    function handlePost(post) {
      console.log(post);
    }
    
    connect();

    Good to know

    • An open stream keeps your feed active. Graze pauses feeds that nobody has read for a day. An open Contrails connection counts as reading, so a quiet feed wakes up again, though it can take a few minutes before matches start to flow.
    • What you see is what your feed matches. Every post has already passed your feed's Rules. Change the Rules in Graze and the stream follows.

    Feedback

    If you hit a problem or have an idea, tell us on Discord or @graze.social on Bluesky.

    • Getting started
    • What you receive
    • Resuming where you left off
    • Keep a watchdog running
    • Example in JavaScript
    • Good to know
    • Feedback
    Stay in the know
    Follow on BlueSkyJoin the Discorddiscord_fillSubscribe to the newsletter
    Company
    AboutBlogPressContact
    Support
    Community GuidelinesDocsSite StatusHelp CenterFeature Requests
    Legal
    Privacy PolicyTerms & Conditions
    © 2026 Graze.social. All rights reserved.