Video transcript
- What Sift does: I used to save links on WhatsApp to read later, and then I just didn't. Each one needed work first, like figuring out if it was real and if it'd run on my hardware. So I built Sift, and now I paste links into Telegram. At 3 AM a script reads each page, searches the web, and asks Claude if it's worth my time, and at 7:15 I get a short report with the one to three links worth acting on. I'm Nate. I built Sift because I never did anything with my bookmarks. Now I know what's worth my time or my agent's tokens. If you want to skip the typing,
- Two ways to build it (prompt or follow along): there's a prompt linked in the description or at the bottom of this page. Paste it into Claude Code and it builds this with you, or you can follow along with me. There are just six steps. The page also has every command and setting from this video, so you can watch on your phone now and build it at your computer later. Each step ends with a check, so you'll know it worked before you move on. Before you start, there are five things you'll need.
- What you need: First, your computer has to be awake at 3 AM. If it's asleep, the night job just doesn't run, and in the morning you get no report and no error telling you why. So either use a computer that stays on, like a desktop, or keep your laptop plugged in and set it to stay awake while it's plugged in. And if you're on a Mac, use the scheduler called launchd, because it runs a job it missed as soon as the Mac wakes up. The second thing is Claude Code, with either a Claude plan or an API key. I run mine on my Claude plan, so there's no separate bill, but each night uses some of my plan's usage. With an API key you pay for every call. Either way, I'd start with a cap of 10 links a night and look at your usage page the first morning. The third thing is Telegram on your phone, and the fourth is a folder for your notes. Any folder of Markdown files works, and I happen to use Obsidian. The last one is an evening of your time. I built this on a Mac, Linux works the same way, and I haven't tried it on Windows. Before we build anything, I'll show you how the parts fit together.
- How the parts fit: Sift has a day side and a night side. The day side is a bot that saves each link you send it and doesn't do anything else, which is why saving only takes a second. The night side is two scheduled jobs, which just means commands your computer runs at a set time. At 3 AM the first one goes through every saved link, and at 7:15 the second one sends you the report. The bot writes links to a file I call the queue, and the night job reads from it. Doing it this way puts all the AI calls in one batch with a cap, so you know roughly what a night costs.
- Step 1: A capture inbox in Telegram: First we need somewhere to paste links, and that's a Telegram bot, which is just a Telegram account that a program runs instead of a person. You message an account called BotFather, type /newbot, and give it a name. It sends back a token, and the token is basically the bot's password, so whoever has it controls your bot. Before you save that token anywhere, have Claude Code make a .gitignore file with .env in it. .gitignore is the list of files git won't save, and .env is where your secrets go. You want that because git keeps every old version of every file, and repos get pushed and shared. So if you commit the token once, it's leaked even after you delete it, and you'd have to revoke it. Next you pick where you'll paste links. The simplest choice is a private chat with your bot. I use a topic in a Telegram group instead, which is a separate thread inside the group, because my bot does a few other jobs too. If you go the group route, make the bot an admin so it can see every message. The bot runs as one program that keeps asking Telegram if there's anything new, so your computer doesn't need a public web address. You don't have to dig up any IDs either, because after you send the bot one message, Claude Code can read them right out of Telegram's reply. Also, set the bot to only accept messages from your own user ID, because anyone can find a bot by its name. Without that check a stranger could fill up your queue and burn your Claude usage at night. So paste a link in, and within a few seconds you should get Saved to Sift back. If you don't get a reply, look at your terminal first, because the bot has to still be running there. And if you're using a group, double check that the bot is an admin.
- Step 2: The queue: Every link the bot saves goes into the queue, which is a plain file where each link gets one line of JSON added to the end. JSON is just a text format that programs can read. The bot only adds lines and doesn't touch old ones, so the night job can read the file while the bot's still writing to it. Each line gets an ID that's made from the link and the time you saved it, and not from anything the page or the model says later. That way the night job does each link exactly once. The line also stores the date you saved it in your own time zone, because that date decides which daily note it ends up in. To see it working, save two links and open the file. You should have two lines with two different IDs. If the date says tomorrow, the code used UTC time, so just tell Claude Code your time zone.
- Step 3: Schedule the jobs, keep the bot running: That's the whole day side, and everything from here on runs at night. We need two scheduled jobs for it. One's called drain and runs at 3 AM, and the other's called report and runs at 7:15. Right now they're empty shells, and the next three steps fill them in. This is a cron line, which is the simplest kind of schedule to read. The first two numbers are the minute and the hour, so 0 3 is 3 AM, and the three stars mean every day. On a Mac I'd ask Claude Code for a launchd job instead, which runs the same command and catches up after sleep. A scheduled job starts with almost no setup. It doesn't know where your tools are, and it starts in your home folder. So each line moves into the project folder first, calls bun by its full path, and writes everything it prints to a log file. If you type which bun in your terminal, it shows you that full path. The same scheduler can keep the bot running after you close the terminal. On a Mac that's a launchd job with KeepAlive turned on, so it starts the bot when you log in and restarts it if it crashes. I also make every run exit with code zero, even when a link won't load, and put the problems in the log and the note. Some schedulers pause a job that keeps failing, and one bad link shouldn't turn off your whole night. The way I'd test it is to set a test run for two minutes from now, wait, and then read the log, where you should see the run. After that, close the terminal and paste a link. If the bot still replies, the scheduler's keeping it running. The usual problem is a job that works by hand but not on the schedule. If the log says command not found, a full path is missing, and if the log's empty, the computer was asleep or the time's off.
- Step 4: Fetch the real page: At 3 AM, the first thing the night job does is get the real page, and how it does that depends on the kind of link. For a web page I use two tools. A reader service like Jina Reader turns the page into text, and I also load it in a headless browser, which is a browser with no window that a program drives. I trust the browser's text first, because my first version only used the reader, and on a lot of pages it handed back the cookie banner and the terms of service. So the model ended up judging a tool on its legal text. For a GitHub repo, the GitHub API tells you the stars, the last update, and whether it's archived, and for a paper on arXiv I just grab the abstract page. Instagram needs a login cookie, so I'd leave it for later. On long pages Sift keeps the part with the most real sentences, since the top of a page is mostly menus. Try it on one web page, one repo, and one paper, and the first lines of each should be real content. If you see a cookie banner or a menu instead, the browser step didn't work, so paste that output back into Claude Code and let it sort it out.
- Step 5: Research and judge, safely: With the real text in hand, Sift researches each link and judges it. The night job reads pages that strangers wrote, and a page can hide text that tries to give the AI orders. So the research call can only search the web, and the analysis call gets no tools at all. That way the worst a bad page can do is spoil one note. To call Claude from a script, the night job uses Claude Code's print mode, which is claude -p. It takes the prompt, prints the answer, and exits, so nobody has to be at the keyboard. It starts with two or three web searches about the tool and who made it, keeps only findings with a real source, and numbers them so the model can cite them. After that I give the model the page text, the research, my note, my computers, and my lanes, which are the areas of my life a link could be for. I also give it today's date and tell it that not knowing something isn't evidence, because early on mine called a real new AI model fake. It answers in strict JSON so the program can read it, and you decide what's worth acting on, because the prompt asks you for your own rule. And I don't let it install or run anything it finds at night, because running code from the internet is a manual step I approve. You'll know this step works when your three test links each come back as JSON that parses, and every number in the answer points to a real page. Open one of those pages and see for yourself. The snag I'd expect is the model wrapping its JSON in a code block, and Claude Code can strip that off and try once more.
- Step 6: The note and the morning report: The last step is what you get in the morning. The night job writes one note with a section for each link, and at 7:15 the report job sends a short Telegram message with the count, the one to three worth acting on, and a link to the note. If you keep daily notes, the link also goes in the daily note for the day you saved it, and if that note isn't there yet, I retry on the next run rather than use a different day. Run drain and then report by hand once, and you should get one Telegram message and the note should open. After that, run drain a second time, and it should find no new work, because it skips the links it already did. One Mac snag to know about is a log that shows the run but no note. macOS can block scheduled jobs from your Documents folder, so either move your notes folder or give the job access in your privacy settings. The next morning, look at three things.
- Your first real night: Make sure the report showed up at 7:15, check the log for a 3 AM run with no errors, and look at your Claude usage page to see what one night used. If there's no log line at 3 AM at all, your computer was asleep. So that's Sift, and I still save links all day like before,
- Wrap: but now when I start my morning I know which ones are worth my time, and which ones are worth my agent's tokens. If you want to build your own, grab the prompt linked in the description or at the bottom of this page.
Wake up to answers with Sift
I still save links from my phone all day, same as before. The difference is they don’t just sit there anymore. Every morning there’s a short list waiting for me - the one to three links worth my time, with the research already done on each one.
The story
The problem
I find a lot of good links on my phone, mostly from X or Instagram - a new tool, a GitHub repo, a smart workflow. I used to send each one to myself on WhatsApp to read later.
Then I never read them.
Why it was a problem for me
The list failed me in two ways. First, I forgot it existed. When I sat down at my laptop, the links were out of sight, out of mind. Second, when I did remember, I had no time. Each link needs work first. Is the tool real? Is it still maintained? Will it run on my hardware? Does it fit anything I’m working on? So the links just sat there and went stale.
The solution
So I built Sift. Now I paste the link into a “Sift” topic in my Telegram group (about two seconds). The bot replies “Saved to Sift.” and that’s it.
At 3 AM a job picks up the day’s links. For each one it reads the actual page, runs a few web searches, and asks an AI model (Claude) a fixed set of questions: is it real, who made it, do I already have something like it, and is it worth my time this week?
At 7:15 AM a short report shows up in Telegram: how many links came in, and the one to three worth acting on. It links to the triage note in Obsidian (my notes app), as does my daily note.
Now I start the day from finished research instead of a cold list.
How it works
Build it yourself
You can build your own Sift in an evening, plus a quick look the next morning to see that the night run worked. First comes what you need, then six steps. Each step ends with a check, so you know it worked before you go on, and with the snag I’d expect most. If you’d rather not type it all, the prompt at the bottom of this page walks a coding agent through the same six steps and the same checks.
What you need
- A computer that is awake at 3 AM. See the next part. This is the one people miss.
- Claude Code, signed in with a Claude plan or set up with an API key. The night job uses it to research and judge each link.
- Telegram on your phone.
- Bun, a program that runs TypeScript. Claude Code can install it for you. I used TypeScript, but any language works.
- A folder for notes. Any folder of Markdown files works. I use Obsidian, but you don’t need it.
- Optional: a free GitHub account, for a token that lets you check more repos each night.
I built it on a Mac. Linux works the same way, with cron or systemd. On Windows you’d use WSL or Task Scheduler, which I haven’t tested. I run Sift on Daniel Miessler’s PAI and use its scheduler, but you don’t need PAI. Your computer’s own scheduler does the same job.
Your computer has to be awake at night
The night job runs at 3 AM. If your computer is asleep then, nothing runs, and in the morning there’s no report and no error to tell you why. You have three options:
- Use a computer that stays on, like a desktop or a small always-on box. That’s what I do.
- On a laptop, keep it plugged in and set it not to sleep while it’s plugged in. Or schedule a wake a few minutes before the job. On a Mac that’s
sudo pmset repeat wakeorpoweron MTWRFSU 02:55:00. - On a Mac, use launchd instead of cron. If the Mac was asleep at 3 AM, launchd runs the job when it wakes up. Cron just skips it.
What it costs
My night job calls Claude Code on my Claude plan, so there’s no separate bill, but it does use some of the plan’s usage. That’s why each night has a cap on links. If you use an API key, you pay for each call instead. Either way, start with a cap of about 10 links a night, and look at your usage page the first morning before you raise it. The other tools here are free at this size.
How a script talks to Claude
You probably use Claude in a chat window. A script can’t type into a chat, so the night job runs Claude Code in print mode: claude -p "<your prompt>". Print mode takes the prompt, prints the answer, and exits, with no one at the keyboard. It uses the login Claude Code already has, so sign in once in a terminal before you schedule anything.
The night job reads pages that strangers wrote, and a page can hold text that tries to give the AI orders. So I give the research call one tool, web search, and the analysis call no tools at all. The worst a bad page can do then is spoil one note. Don’t run the night job with a flag that skips Claude Code’s permission checks.
Day side and night side
Sift has two halves. The day side is a bot that saves each link you send it in about a second, and does no other work. The night side is two scheduled jobs: one at 3 AM does the slow work on every saved link, and one at 7:15 AM sends you the report. A queue file sits between them. This split keeps saving instant, and it puts every AI call in one capped batch, so the cost is easy to predict. Steps 1 and 2 build the day side. Steps 3 to 6 build the night side.
Step 1. Make a capture inbox in Telegram
A bot is a Telegram account that a program controls instead of a person. In Telegram, message BotFather, send /newbot, and pick a name. BotFather replies with a token. The token is the bot’s password: anyone who has it controls your bot.
Before you save the token anywhere, make a .gitignore file that lists .env. Then put the token in .env, the private file that holds this project’s secrets. git keeps every version of every file. If you commit the token once, it stays in the history even after you delete it, and the only fix is to revoke it in BotFather.
Then pick where you’ll paste links:
- The simple way: a private chat with your bot. If Sift is the bot’s only job, every message in that chat is a capture.
- What I do: a “Sift” topic in a Telegram group with topics turned on. (A topic is a separate thread inside one group.) My bot does other jobs too, so the topic keeps Sift links apart. Make the bot a group admin with the “Manage topics” right. An admin bot sees every message, and the Telegram Bot API lets it make the topic with
createForumTopic. If you don’t want it to be an admin, turn privacy mode off in BotFather (/setprivacy), then remove the bot from the group and add it back. The change doesn’t reach a group the bot is already in.
The bot is one long-running program that uses long polling: it keeps asking Telegram “anything new?” and waits for the answer, so your computer doesn’t need a public web address. You don’t have to look up IDs by hand. Send the bot one message, and Telegram’s getUpdates reply shows your chat ID, your topic ID (message_thread_id) and your own user ID.
Make the bot accept messages only from your own user ID. Anyone can find a bot by its username, and without this check a stranger could fill your queue and spend your Claude usage. In a group, check the topic first, before any other message handler, so a pasted URL can’t be read as a command.
Keep capture cheap. The bot saves the link and replies, with no AI call and no fetch, so saving is instant. I also kept a link: <url> prefix as a fallback for other chats. It still only works for my user ID.
Check: paste a link, and within a few seconds the bot replies “Saved to Sift.”
Common snag: no reply. First make sure the bot program is still running in your terminal. (Step 3 keeps it running after you close the terminal.) In a group, make sure the bot is an admin.
Step 2. Write each link to an append-only queue
The queue is a text file with one line per saved link, written in JSON, a plain-text format that programs read and write. Append-only means the bot only adds lines at the end and doesn’t change old ones. Adding one line is safe even while the night job is reading the file.
{"id":"3f9c2a7b1e04d8a6","url":"https://example.com/new-tool","note":"for the newsletter","source":"telegram","capturedAt":"2026-10-21T05:12:44.000Z","captureDate":"2026-10-20"}Make the id from what came in: a hash (a short fingerprint) of the URL, the message ID and the capture time. Don’t make it from anything the fetch or the model returns, or a page edit turns into a “new” link. The night job keeps a separate list of processed IDs, and it adds an ID only after that link’s note section is written. So if a run stops halfway, the next run finishes the rest and skips the links that are done. If you save the same URL twice you get two items, and that’s on purpose. The repeat log in Step 6 tells you about it.
Store captureDate in your own time zone. In the example, 05:12 UTC is still the evening of the 20th in Pacific time. That date decides which daily note the link goes into later.
Check: save two links. The queue file has two lines with two different IDs, and captureDate is today’s date where you are.
Common snag: the date is tomorrow’s. That means the code used UTC, so ask for your time zone instead.
Step 3. Schedule the jobs and keep the bot running
A scheduler is a clock on your computer that runs a command at set times. You need two jobs. drain runs at 3 AM and does the slow work, and report runs at 7:15 AM and sends the headline. At this point both are empty shells that start and exit. Steps 4 to 6 fill them in.
In cron, each line is a time and then a command. The five time fields are minute, hour, day of month, month, and day of week, and * means “every”. So 0 3 * * * means 3:00 AM every day.
# minute hour day month weekday command
0 3 * * * cd /path/to/sift && /full/path/to/bun run sift.ts drain >> sift.log 2>&1
15 7 * * * cd /path/to/sift && /full/path/to/bun run sift.ts report >> sift.log 2>&1A scheduled job starts with an almost empty setup. It doesn’t load your shell settings, so it can’t find tools by name, and it starts in your home folder. So each line moves into the project folder first, calls Bun by its full path (which bun prints it), and adds its output to a log file. On a Mac, ask your coding agent for launchd jobs instead of cron lines. They do the same thing, and they catch up after sleep.
Use the same scheduler to keep the bot running. On a Mac that’s a launchd job with KeepAlive, and on Linux it’s a systemd service with Restart=always. Either one starts the bot when you log in and restarts it if it crashes.
Every run exits with code 0, the signal for “finished normally”, even when the queue is empty or a link won’t load. Problems go in the log and the note. Some schedulers pause a job that keeps failing, and one bad link shouldn’t switch off your whole night.
Check: run both jobs by hand with an empty queue. Type echo $? right after each one, and it prints 0. Then set a test run two minutes from now, wait, and read the log. Last, close the terminal and paste a link. The bot still replies.
Common snag: the job works by hand but not on schedule. If the log says “command not found”, use the full path. If the log is empty, the computer was asleep or the time line has a typo.
Step 4. Fetch the real content, by medium
Pick the fetch method from the URL. For a first build, web pages, GitHub repos and papers are enough. Add Instagram later.
- Web page: get readable text from a reader service like Jina Reader. You put
r.jina.ai/in front of the URL and get the page back as text, with no key needed for a few pages a night. Then render the page in a headless browser, which is a browser with no window that a program drives. I use Lightpanda, and your coding agent can install it. Prefer the rendered text. Only use the reader text when the render comes back empty. Gotcha 1 below is why. - GitHub repo: call the GitHub REST API, an address a program calls to get data. Stars, the last push date and the archived flag tell you if the project is alive. It works without a token for a few repos a night, and a token raises the limit.
- arXiv paper: fetch the arXiv abstract page, not the PDF.
- Instagram (optional): read the caption and author only, with yt-dlp for reels and gallery-dl for image posts. Both need a logged-in session cookie. A session cookie is a login: whoever has it can act as you, so treat it like a password.
From long text, keep the densest window: the stretch of about 4,000 characters with the most real sentences. The top of a page is usually menus, and a smaller window keeps each model call small.
Check: fetch one web page, one repo and one paper. The first lines of each are real content, with no cookie banner and no menu.
Common snag: the first lines are a cookie banner or a menu. That means the render failed or its text wasn’t used. Check that the headless browser is installed and that the code prefers its text.
Step 5. Research, then ask the model fixed questions
Before the analysis, run two or three web searches about the tool and who made it. I use the web search tool in Claude Code, through print mode with only that tool allowed. Keep only findings whose source URL was in the real search results. Number them, so the model can cite them as [1] and [2].
Then give the model the page text, the research, your note, your lanes, a short list of what you already run, your computers, and today’s date. Ask for strict JSON, which means only the JSON object with no other text, so the program can read it:
{
"type": "repo",
"lane": "<one of your lanes>",
"isNoise": false,
"title": "Example Tool",
"summary": "Example Tool converts web pages to clean text. It runs as a local command.",
"real": "The repository has working code and recent releases [1].",
"maintained": "The last push was in October 2026. The repository is not archived.",
"whoBehind": "A small open-source team [2].",
"duplicates": "My current page fetcher does the same job.",
"fit": "Skip it. It does not add a feature I need.",
"actWorthy": false,
"nextStep": "skip",
"smokeTestPlan": ""
}A lane is the area of your life a link could serve. Mine are my own systems, my day job, and a possible business, but pick your own. “Worth acting on” is your rule too. Write down what makes a link worth your time this week, and put it in the prompt.
A smoke test is two to four install-and-run commands that show a tool works. Write a smoke-test plan only for repos and tools that are worth it, and don’t run it at night. Running code from the internet stays a manual step that I approve.
Check: run your three test links. Each answer parses as JSON. Each [1] or [2] matches a URL from the search results, so open one and look. The hardware answer matches what the real page says.
Common snag: the JSON doesn’t parse, because the model wrapped it in a code fence or added a sentence. Strip the fence and retry once. If it still fails, mark that link as failed in the note and go on to the next one.
Step 6. Write the triage note and send the headline
Each night, write one dated note in your notes folder with a section for each link. The morning job then sends a short Telegram message: how many links the night job did, how many failed, the one to three worth acting on, and a link to the note. On your phone, the link opens only if your notes app syncs there. The message is still useful without it.
If you keep daily notes, add the note link to the daily note for the capture date. If that daily note doesn’t exist yet, save the link and add it on the next report run. Don’t write it into a different day.
Keep a log of every link Sift has seen. If you save the same thing twice, the report says so.
Optional: each item in my note has a “Give next steps” link. It opens Telegram with a message already typed. I add what I want and send it to my assistant, which picks it up from there.
Check: run drain and then report by hand with your three test links. You get one Telegram message, the note opens, and the right daily note links to it. Run drain again, and it finds no new work.
Common snag: the log says the job ran, but no note appears. On a Mac, a scheduled job can be blocked from Documents, Desktop and iCloud Drive. Move the notes folder, or give the scheduled program access in System Settings > Privacy & Security.
The first real night
The next morning, check three things. The report arrived at 7:15. The log shows a 3 AM run with no errors. And your Claude usage page, or your API bill, shows what one night used. If the report didn’t come, the log tells you which half failed. No log line at 3 AM means the computer was asleep.
Settings worth copying
- Plain words in the output. My prompt tells the model to write every field in ASD-STE100 Simplified Technical English. That means short sentences, one idea each, active voice, and no jargon. The notes are fast to skim at 7 AM.
- A hardware check. The prompt lists my computers. The model has to pull out each hard requirement (chip, GPU, RAM, OS) and say no when my hardware fails one.
- A repeat check. I keep a log of every link Sift has seen. If I save the same thing twice, the report says so.
- An optional skip rule. I send links about health past the normal analysis, and a stronger model reviews them later. You can do the same for any topic you want handled with more care.
Gotchas I hit
- The reader dump included a cookie wall. My first fetch only used the reader service. On a lot of pages it returned everything - the mega-menu, the footer, and the whole EU cookie and terms banner. My “find the densest text” step then picked the legal text (legal text is dense, real sentences), and my thin-content check passed it. So the model judged the tool on its terms of service. One verdict said a tool wouldn’t run on my Macs, when the real page said it did. The fix: for every page that isn’t a repo, always render it headless and trust the rendered text first.
- Make the dedup ID from the input. If the ID depends on the fetched body or the model’s answer, a page edit or a new model run makes a “new” item. Then you research the same link twice. Hash only what came in.
- The model’s knowledge is out of date. Early on, the model called a real, newly released AI model “fake,” because it had never heard of it. Now each run gets the current date, a rule that “I don’t know it” is not evidence, and the web research pass. Only real sources can back a claim that something doesn’t exist.
- “Thin” is not the same as “short.” A 200-character Instagram caption is the full content. Flag a fetch as thin only when it captured no real text. Thin items get a “retry in a browser” note. They never get marked as noise.
- Login walls need a saved session. Instagram shows nothing to a logged-out fetch. I export the session cookie to a private file (owner-only permissions, never committed to git). The night job reads that file, so it doesn’t need an open browser. The cookie lasts about six months and can’t be refreshed. A weekly check warns me only when it’s close to expiring.
- Night jobs have a short PATH. A scheduled job doesn’t load your shell setup. Command-line tools that work fine in your terminal can fail silently at 3 AM. Call them by their full path.
- Don’t grind long video at night. Short clips are fine. A six-hour conference video would run for hours. Gate on length, and only transcribe long video when you ask for it.
- Exit clean. The scheduler in PAI pauses a job after it fails three times in a row, and other schedulers have their own version of this. So a night with an empty queue, or a link that won’t load, still has to exit with code 0. Report problems in the log and the note, not in the exit code.
Built with
- Daniel Miessler’s PAI: the personal AI system Sift runs on, including its scheduler.
- Claude by Anthropic, through Claude Code: the analysis and the web research.
- Telegram Bot API: capture and the morning report.
- Lightpanda: headless page rendering.
- Jina Reader: first-pass readable text.
- GitHub REST API: repo health signals.
- yt-dlp and gallery-dl: Instagram captions.
- arXiv: paper abstracts.
- Obsidian: the triage notes and daily notes.
- Bun: the TypeScript runtime.
- Music in the tutorial video: ‘Greenpoint Avenue’ by auv on Uppbeat.
- ASD-STE100 Simplified Technical English: the writing rules for every note.
Build it with AI
Paste this into Claude Code (or another coding agent) and it will build Sift with you, asking for your own keys as it goes. It follows the same six steps and checks as above.
You are going to build Sift with me. Sift is a small personal system. During the day I paste links into Telegram. At night a job reads each link, researches it, and asks an AI model if it is worth my time. In the morning I get a short Telegram report and a triage note in my notes folder.
Sift has two halves. The day side is a bot that only saves links, so saving is instant. The night side is two scheduled jobs: `drain` at night does the slow work, and `report` in the morning sends the headline. A queue file sits between them. Steps 1 and 2 build the day side. Steps 3 to 6 build the night side.
How to work with me:
- I may be new to bots, schedulers and terminals. Explain each new term in one plain sentence the first time you use it, and tell me why we do each step.
- Build it one step at a time. After each step, run the check for that step and show me the result before you go on. If a check fails, tell me what you think went wrong and fix it before you go on.
- Ask me for every real value: the bot token, my time zone, the schedule, my notes folder, and my lanes. Never invent a value. Find IDs yourself where you can (see Step 1) instead of asking me to look them up.
- Before any secret exists, make a `.gitignore` that lists `.env`, plus any cookie file. Keep every secret in `.env`. Never put a secret in code, in a command I'd leave in my shell history, or in git. git keeps every old version, so a committed secret has to be revoked.
- If a step is not clear, or a tool does not work as written here, stop and ask me.
- Use TypeScript on Bun (https://bun.sh) unless I ask for something else. Install Bun for me if I don't have it. Keep it to one small project with one command, for example `bun run sift.ts <bot|drain|report>`.
Before you start, ask me:
1. Which computer will run Sift, and its OS. Explain that it must be awake at the night run time, and help me choose: a computer that stays on; a laptop that is plugged in and set not to sleep while plugged in, or that has a scheduled wake (on a Mac: `sudo pmset repeat wakeorpoweron MTWRFSU <a few minutes before the night run>`); or, on a Mac, launchd, which runs a missed job when the Mac wakes (cron skips it).
2. Which scheduler to use. Suggest launchd on a Mac, cron or systemd on Linux, and Task Scheduler or WSL on Windows (untested). Use the same scheduler to keep the bot running.
3. My time zone.
4. Where my notes go: any folder of Markdown files, for example an Obsidian vault folder (https://obsidian.md). Ask if I keep daily notes, and where.
5. How a script can call an AI model. The default is Claude Code (https://code.claude.com/docs/en/overview) in print mode: `claude -p "<prompt>"` prints one answer and exits, with no one at the keyboard. It uses my Claude plan's login, so check that I'm signed in. If I'd rather use an API key, tell me that I pay for each call.
6. The areas of my life a link can serve (my "lanes"), for example "my own systems", "my day job", "a possible business".
7. A short list of the tools I already run, and my computers (chip, GPU, RAM, OS).
8. The night run time and the morning report time (defaults: 3:00 AM and 7:15 AM), and the most links to process in one night (suggest 10 to start, so I can see the cost first).
Step 1. Make a capture inbox in Telegram.
- Tell me exactly what to send to BotFather (`/newbot`, a name, a username). When I give you the token, put it in `.env`.
- Ask me which inbox I want:
- A private chat with the bot. This is the simple way, when Sift is the bot's only job.
- A "Sift" topic in a Telegram group with topics turned on. Here I make the bot a group admin with the "Manage topics" right, so it sees every message and can make the topic with `createForumTopic` (Telegram Bot API: https://core.telegram.org/bots/api). If I don't want it to be an admin, I turn privacy mode off in BotFather (`/setprivacy`), then remove the bot from the group and add it back, because the change doesn't reach a group the bot is already in.
- Run the bot as one long-running process (`bot`) with long polling (`getUpdates`), so it needs no public server. For now it runs in my terminal; Step 3 keeps it running.
- Find the IDs yourself. Ask me to send the bot one message, then read the chat ID, the topic ID (`message_thread_id`, groups only) and my own user ID from the update, and save them to `.env`.
- Accept messages only from my user ID. Ignore everyone else, so a stranger can't fill my queue or spend my AI usage.
- In a group, check the topic first, before any other message handler, so a pasted URL is never read as a command.
- Keep capture cheap: no AI call and no fetch. Save the link and reply "Saved to Sift."
- Also accept a `link: <url>` prefix in other chats, as a fallback (still my user ID only).
- Check: I paste a link and the bot replies "Saved to Sift." within a few seconds.
- If there is no reply: check that the bot process is running, and (in a group) that the bot is an admin.
Step 2. Write each link to an append-only queue.
- Append one JSON line per link to a queue file. Append-only means you only add lines at the end and never change old ones, so the night job can read the file while the bot writes. Example shape (example values only):
{"id":"3f9c2a7b1e04d8a6","url":"https://example.com/new-tool","note":"for the newsletter","source":"telegram","capturedAt":"2026-10-21T05:12:44.000Z","captureDate":"2026-10-20"}
- Make `id` a hash of the URL, the message ID, and the capture time. Never build it from anything the fetch or the model returns, or a page edit makes a "new" item.
- Store `captureDate` in my time zone, not UTC. It decides which daily note the link goes into.
- Check: two captures make two lines, with two different IDs, and `captureDate` is today's date in my time zone.
Step 3. Schedule the jobs and keep the bot running.
- Make two commands. They are empty shells for now (they start, log one line, and exit 0); Steps 4 to 6 fill them in:
- `drain` (night): fetch, research, analyze, and write the note.
- `report` (morning): send the headline and link the note.
- Schedule them with the scheduler I chose. In cron syntax, with example times and example paths:
0 3 * * * cd /path/to/sift && /full/path/to/bun run sift.ts drain >> sift.log 2>&1
15 7 * * * cd /path/to/sift && /full/path/to/bun run sift.ts report >> sift.log 2>&1
Explain the five time fields to me (minute, hour, day of month, month, day of week). On a Mac, write launchd jobs instead, and install them for me.
- Scheduled jobs don't load my shell setup and start in my home folder. So start in the project folder, call every command-line tool by its full path (use `which` to find it), and write all output to a log file.
- Keep the bot running with the same scheduler: on a Mac a launchd job with `KeepAlive`, on Linux a systemd service with `Restart=always`. It should start at login and restart after a crash.
- Every run exits with code 0, even with an empty queue or a link that won't load. Put problems in the log and the note, not in the exit code. (Some schedulers pause a job that keeps failing.)
- Check: (1) run both jobs by hand with an empty queue, and both exit 0. (2) Schedule a test run two minutes from now, wait, and show me the new log lines. Then set the real times. (3) I close the terminal and paste a link, and the bot still replies.
- If the scheduled run fails but the run by hand works: "command not found" in the log means a missing full path, and no log line means the computer was asleep or the time is wrong.
Step 4. Fetch the real content, by medium. Pick the method from the URL. A first build only needs web pages, GitHub repos and arXiv papers. Ask me if I want Instagram now or later.
- Web page: get readable text from a reader service such as Jina Reader (https://github.com/jina-ai/reader; prefix the URL with `r.jina.ai/`, no key needed at low volume). Then render the page in a headless browser such as Lightpanda (https://github.com/lightpanda-io/browser). Install it for me. Prefer the rendered text. Use the reader text only when the render comes back empty. (The reader often returns cookie banners and terms, and the model then judges the tool on its legal text.)
- GitHub repo: call the GitHub REST API (https://docs.github.com/en/rest/repos/repos). Keep stars, the last push date, and the archived flag. It works without a token at low volume. Offer to add a token to `.env` for a higher limit.
- arXiv paper: fetch the abstract page (https://arxiv.org), not the PDF.
- Instagram (optional): read only the caption and the author, with yt-dlp (https://github.com/yt-dlp/yt-dlp) for reels and gallery-dl (https://github.com/mikf/gallery-dl) for image posts. Both need a logged-in session cookie. Explain that the cookie is a login, and treat it like a password. Help me export it to a private file (owner-only permissions, listed in `.gitignore`). Add a weekly check that warns me when the cookie is close to expiring.
- From long text, keep the densest window of real sentences (about 4,000 characters), not the top of the page. The top is usually menus.
- Flag a fetch as "thin" only when it captured no real text. A short caption is full content. Thin items get a "retry in a browser" note and are never marked as noise.
- Do not transcribe long video at night. Gate on length.
- Check: run the fetch on one web page, one repo, and one paper that I give you. Show me the first lines of each result. None of them is a cookie banner or a menu.
Step 5. Research, then ask the model fixed questions.
- The night job reads pages written by strangers, and a page can contain text that tries to give the AI orders. So give the research call only the web search tool (for example `claude -p` with only WebSearch allowed), and give the analysis call no tools at all. Never use a flag that skips permission checks.
- Run two or three web searches about the tool and who made it. Keep only findings whose source URL was in the real search results. Number them so the model can cite them.
- Give the model the page text, the research, my note, my lanes, my tools, and my computers. Give it today's date, and tell it that "I don't know this" is not evidence that something is fake. Only real sources can back a claim that something does not exist.
- Tell it to write every field in ASD-STE100 Simplified Technical English (https://www.asd-ste100.org): short sentences, one idea each, active voice, no jargon.
- Tell it to list each hard requirement (chip, GPU, RAM, OS) and say no when my computers fail one.
- Ask for strict JSON (only the JSON object, no other text). Example shape (example values only):
{"type":"repo","lane":"<one of your lanes>","isNoise":false,"title":"Example Tool","summary":"...","real":"... [1]","maintained":"...","whoBehind":"... [2]","duplicates":"...","fit":"...","actWorthy":false,"nextStep":"skip","smokeTestPlan":""}
- If the answer doesn't parse, strip a code fence and retry once. If it still fails, mark the item as failed in the note and go on.
- Ask me what makes a link worth acting on for me. Use my answer. Do not invent a rule.
- Write a smoke-test plan (two to four install-and-run commands) only for repos and tools worth it. Never run it at night. Running code from the internet stays a manual step that I approve.
- Optional: ask me if any topics should skip the normal analysis (for example health links I want a stronger model to review).
- Check: run it on the three test links. Each answer parses. Show me each numbered claim next to its source URL. The hardware answer matches the real page.
Step 6. Write the triage note and send the headline.
- Write one dated note with a section per link, in my notes folder.
- Mark an ID as processed only after its note section is written, in a separate processed-IDs list. Never edit the queue itself.
- Send a short Telegram message: how many links the night job did, how many failed, the one to three links worth acting on, and a link to the note.
- If I keep daily notes, add the note link to the daily note for the capture date. If that daily note does not exist yet, save the link and add it on the next `report` run. Never write it into a different day.
- Keep a log of every link seen. If I save the same thing twice, the report says so.
- Optional: give each item a "Give next steps" link that opens Telegram with a message already typed.
- Check: run drain and then report by hand with the three test links. I get one Telegram message, the note opens, and the right daily note links to it. Then run drain again: it skips the processed IDs and does no new work.
- If the job runs but no note appears on a Mac: the notes folder may be in Documents, Desktop or iCloud Drive, which scheduled jobs can be blocked from. Help me move it or grant access in System Settings > Privacy & Security.
When all six checks pass, show me a short summary: the files you made, the schedule you set, where the log is, and the one command to run each job by hand. Then tell me what to check the next morning: the report arrived, the log shows the night run with no errors, and my Claude usage page (or API bill) shows what one night used.