How I automated my LinkedIn lead magnets with Unipile + Railway

Emma Guetta

Emma Guetta, co-founder of Retriever. I use these systems every day to sell it.

Before you dive in, a word about Retriever, the product this guide comes from. It's a go-to-market AI agent that builds qualified prospect lists from one prompt: you describe your ICP in plain English, the agent searches across 20+ data sources, qualifies every company against your criteria, and returns a ranked list with its reasoning visible. The guides in this section document the systems I use to sell it.

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:

  1. Every 10 to 20 minutes, during working hours, it checks the comments on the posts I told it to watch.
  2. For each new commenter, it looks at whether we're already connected on LinkedIn.
  3. If yes, it posts a public reply under their comment (mentioning them), then sends them the DM with the resource.
  4. 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.
  5. It keeps a state file so each person is processed exactly once — even if I restart the worker or redeploy.
  6. 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

  1. Create an account at dashboard.unipile.com.
  2. 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.
  3. 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 the https:// prefix.
    • The API key: a long token. Copy it.
  4. Keep these two values handy. They go into a .env file 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 @mention of 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 keyword field 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.

  1. Install the Railway CLI: npm i -g @railway/cli
  2. Log in: railway login. This opens a browser tab.
  3. From the repo folder, run railway init. It creates a new project. Pick a name like linkedin-lead-magnet-bot.
  4. Run railway up. This uploads the code and starts the worker.
  5. 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 is UNIPILE_DSN, UNIPILE_API_KEY, LINKEDIN_ACCOUNT_ID.
  6. Important: also add STATE_DIR=/data and create a Railway Volume mounted at /data for 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.json locally, commit, push. Railway redeploys automatically.
  • To pause a post without deleting it, set "disabled": true in posts.json.
  • To see who was processed, look at the file output/commenters-<post-slug>.csv inside 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 enrichToCsv function — 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.

Emma Guetta

About me

I'm Emma Guetta, co-founder of Retriever, a go-to-market AI agent that builds qualified prospect lists from a prompt (think Claude Code for go-to-market). Same principle as this guide: you describe what you want, an agent does the repetitive work, and you keep control of the result. Questions or feedback: find me on LinkedIn.