The guide
The setup I'm describing here cost me about half a day to build and runs on its own for the price of a coffee per month. The code is open source on GitHub at emmaguetta/linkedin-lead-magnet-bot. I'll walk you through how it works and how to deploy your own copy.
The problem
If you've ever posted something like "comment GUIDE and I'll send you the link", you know the rhythm. Open the post. Read the comment. Reply something nice. Open the DM. Paste the link. Send. Next one.
The first ten are fun. By number fifty you start asking yourself if there isn't a better way.
I tried delegating it to a virtual assistant. The issue is that the assistant doesn't know what to write, and reviewing their replies takes more time than just doing it myself. I tried Phantombuster and the closer-source automation tools. They work, but you give up control over the message, and the ones I tried did not handle the "they have to connect with me first" case cleanly.
So I built something small. Here's what it does:
- Every 10 to 20 minutes, during working hours, it checks the comments on the posts I told it to watch.
- For each new commenter, it looks at whether we're already connected on LinkedIn.
- If yes, it posts a public reply under their comment (mentioning them), then sends them the DM with the resource.
- If not, it posts a "please connect with me" reply instead, and waits. When they send the invitation, it accepts it automatically. The next pass delivers the DM.
- It keeps a state file so each person is processed exactly once — even if I restart the worker or redeploy.
- It enforces daily caps and human-like pauses between actions so LinkedIn doesn't flag the account.
What you'll need
- A Unipile account. This is the API that talks to LinkedIn for you. dashboard.unipile.com. The free tier is enough to test; you'll need a paid plan once you connect a long-lived LinkedIn session.
- A Railway account. railway.com. The worker runs there 24/7. The cost for this workload is small — a few dollars per month.
- A GitHub account, to clone the open-source repo.
- Node.js 20 or newer if you want to run things locally first. Optional, but recommended for the dry-run step.
- A LinkedIn post with a clear keyword in the call to action ("comment GUIDE", "comment SKILLS", etc.). Keywords help the bot skip the random "love this!" comments and only target the people who actually want the resource.
Step 1 — Set up Unipile
- Create an account at dashboard.unipile.com.
- In the dashboard, go to "Accounts" and connect your LinkedIn. Unipile will guide you through the connection flow — it's a hosted login, so you don't paste your password into anything sketchy.
- Once connected, go to "Access tokens" in the dashboard. Note down two things:
- The DSN: this is the host:port of your Unipile instance, something like
api8.unipile.com:13852. Copy it without thehttps://prefix. - The API key: a long token. Copy it.
- The DSN: this is the host:port of your Unipile instance, something like
- Keep these two values handy. They go into a
.envfile in the next step.
Step 2 — Clone the repo and run a dry-run locally
You can skip this step and deploy straight to Railway, but I recommend doing it once locally first. A dry-run prints exactly what the bot would do, without sending anything to LinkedIn. It's the safest way to confirm the setup is correct.
git clone https://github.com/emmaguetta/linkedin-lead-magnet-bot
cd linkedin-lead-magnet-bot
npm install
cp .env.example .env
Open .env and fill in:
UNIPILE_DSN=api8.unipile.com:13852
UNIPILE_API_KEY=your-api-key
LINKEDIN_ACCOUNT_ID=
Then run this command to list your connected accounts and find the LinkedIn id:
npm run accounts
It prints one line per account. Find the line with LINKEDIN and copy the id (starts with acc_) into .env as LINKEDIN_ACCOUNT_ID.
Now configure the post you want to harvest. Copy the example file:
cp posts.example.json posts.json
Open posts.json and replace the example entry with your own. The id field accepts any of these formats — paste whichever is easiest:
- The activity URN:
urn:li:activity:7400000000000000000 - The share URL:
https://www.linkedin.com/feed/update/urn:li:activity:7400000000000000000/ - The bare id:
7400000000000000000
A working example:
A few details about the templates:
{{0}}in the public replies becomes an@mentionof the commenter. LinkedIn displays it as their clickable name and notifies them.{name}in the DM becomes the commenter's first name. The bot pulls it from their LinkedIn profile.- The
keywordfield is the safety net. The bot only processes commenters whose comment contains the keyword (case-insensitive, tolerates plural and small typos). Leave it empty if you want to process every commenter, but I'd advise against it — you'll DM people who left a generic "great post!".
Now do the dry-run:
npm run lead-magnet -- --post "https://www.linkedin.com/feed/update/urn:li:activity:7400000000000000000/"
You'll see a list of commenters and what the bot would do for each one — reply + DM, or please-connect reply, or skip because already handled. No action is sent yet.
If the output looks right, add --send to the same command to do it for real on the first few. The MAX_ACTIONS_PER_RUN cap (default 20) protects you from accidents.
Step 3 — Deploy on Railway
Once the local dry-run looks correct, you can deploy the worker so it runs 24/7. The repo includes a railway.json file that tells Railway exactly what to do.
- Install the Railway CLI:
npm i -g @railway/cli - Log in:
railway login. This opens a browser tab. - From the repo folder, run
railway init. It creates a new project. Pick a name likelinkedin-lead-magnet-bot. - Run
railway up. This uploads the code and starts the worker. - Open the project in the Railway dashboard. Go to the "Variables" tab and add every variable from your local
.env, one by one. The minimum isUNIPILE_DSN,UNIPILE_API_KEY,LINKEDIN_ACCOUNT_ID. - Important: also add
STATE_DIR=/dataand create a Railway Volume mounted at/datafor that service. The volume keeps the state file (which commenters have been processed) across deploys. Without it, every redeploy starts from scratch and you risk DMing the same person twice.
That's it. Open the "Logs" tab and you'll see the worker ticking every 10 to 20 minutes.
Step 4 — Manage it day to day
Once it runs, there is almost nothing to do.
- To add a new post to the harvesting list, edit
posts.jsonlocally, commit, push. Railway redeploys automatically. - To pause a post without deleting it, set
"disabled": trueinposts.json. - To see who was processed, look at the file
output/commenters-<post-slug>.csvinside the Railway volume. The bot enriches each new commenter with their first name, last name, LinkedIn URL, current role, headline, location, the comment text and the delivery status. That CSV is your lead list. - The daily caps live in env vars. The defaults (40 DMs, 80 replies, 50 accepted invitations per day) are conservative. I'd keep them where they are unless you have a strong reason to go higher.
Safety notes — please read these
This kind of bot lives in a gray zone. LinkedIn's terms of service don't love automation. Two practical consequences:
- Keep volumes low. The defaults in this repo are deliberately conservative. The minute-to-minute pacing matters more than the daily volume — five DMs sent within one minute look much more bot-like than fifty DMs sent over a full day. The default delays (45 to 120 seconds between two actions) handle this for you, but don't lower them.
- Use a real DM body. Templated yes, but written by you, on a real topic, mentioning the recipient's first name. Don't try to scale this to a cold-outbound machine — that's a different use case and it's where accounts get restricted.
The bot I'm describing here automates work you would otherwise do by hand on people who explicitly asked for the resource. That's the boring, safe end of the spectrum.
Why I built it with Claude Code
The code is about a thousand lines of TypeScript. I wrote it with Claude Code over a couple of afternoons. Two things made it much faster than doing it without:
- The Unipile SDK is well-typed but the docs around some endpoints (handling received invitations, fetching paginated comments) are thin. Claude reads the SDK source directly, so it figured out the right method signatures in seconds.
- Most of the complexity is in the edge cases — already replied manually, pending invitation in the inbox, daily cap on DMs but not on replies, working hours timezone-aware. Claude is good at listing those upfront and pushing me to handle them, which saved me from a few "oh no it sent the DM twice" incidents.
If you want to start with Claude Code, the installation guide is the simplest place to start. The skill I keep coming back to is the ability to point it at a repo and have it explain the architecture before I touch anything.
What I would do differently next time
A few honest caveats, in case you want to fork the repo and improve it:
- The bot currently watches the comments on a fixed list of posts that you maintain by hand in
posts.json. A nicer version would auto-detect posts where you used a keyword pattern in the caption, and harvest them automatically. I haven't built that yet because I only run two or three lead-magnet posts at a time. - There is no UI. Everything is logs and JSON files. Fine for me, probably not fine for a less technical user. The first feature I'd add if I were turning this into a product would be a small dashboard showing the queue, the daily counters, and recent failures.
- The CSV enrichment is decent but flat. If you're using this for actual outbound and want to score / segment, the right place to plug into is the
enrichToCsvfunction — that's the one spot where each new lead's full profile is available.
Open source code
Everything is at github.com/emmaguetta/linkedin-lead-magnet-bot. MIT license. PRs welcome, especially on the edge cases I haven't hit yet.
