A Rust CLI that sends LinkedIn connection invitations to a CSV-defined target list, ranking each target by shared 1st-degree connections (the "X mutual connections" social-proof signal) and pacing the outreach against LinkedIn's anti-abuse heuristics through a five-tactic humanization layer.
The problem this solves: bulk-blasting connection invitations through LinkedIn's web UI gets accounts soft-restricted within days. The throttle isn't just request volume -- LinkedIn fingerprints request patterns: timing distribution, navigation graph, action determinism. Conversely, requests sent to targets with shared mutuals accept at rates several times higher than cold targets, and acceptance rate is itself one of the strongest anti-spam signals LinkedIn weights.
This tool addresses both axes. It hits LinkedIn's internal Voyager API directly (no Selenium, no headless Chrome at run time) using cookies extracted once from a manually authenticated browser session, ranks targets by mutual-connection bucket using the memberDistance field that profile resolution already returns, and routes every read through humanized decoy traffic so request patterns resemble organic browsing rather than scripted automation.
The runner alternates between two phases until the candidate pool stabilizes, then falls through to lower-priority targets:
+------------------------------+
| Phase 1: Discovery | Resolve each unsent profile, label it
| decoy browse | by memberDistance: "2" (mutuals) or
| profile resolve | "3" (none). Persist degree and
| write degree to CSV | timestamp back to the CSV.
+--------------+---------------+
|
v
+------------------------------+
| Phase 2: Send 2nd-degrees | Pull all rows where degree="2" and
| decoy browse | Is_Sent=0. Send invitations with
| maybe skip (D5.C) | humanized pacing. Mark Is_Sent on
| send invitation | success.
| lognormal delay |
+--------------+---------------+
|
no new 2nds AND
no sends this pass?
|
no | yes
+-------+-------+
| |
loop back v
+------------------------------+
| Phase 3: 3rd-degree | Send remaining 3+
| fall-through | rows with the same
+------------------------------+ pacing. Then stop.
After each send pass, accepted invitations expand the user's network. Some profiles previously labeled 3+ are now 2nd-degree because they're connected to someone who just accepted. The next discovery pass picks them up. The loop converges naturally once no more 2nd-degrees can be discovered.
LinkedIn's anti-abuse system tracks request patterns, not just volume. Five tactics target distinct fingerprinting axes:
| Tactic | What it does | Why it matters |
|---|---|---|
| A. Lognormal delays | Send-to-send gaps drawn from LogNormal(mu=ln(720), sigma=0.6). Median ~12 min, with rare 30-60 min and 2 hr+ tails. |
Uniform-distributed delays are themselves a tell. Real human gaps are skewed. |
| B. Decoy browsing | Before each real action, fire 1-3 of: feed updates, notifications, /me ping, with random "reading time" pauses. Plus a periodic /me every ~5 sends. |
Breaks the deterministic "fetch -> invite" pattern. Refreshes rotating cookies (__cf_bm, lidc) as a side effect. |
| C. Random skip-the-send | ~7% of the time, browse a profile and don't send. Configurable. | Real users browse without always connecting. |
| D. Daily window + cap | Active only 09:00-19:00 system-local; max 18 sends/day (configurable). Counters persist across restarts. | LinkedIn's known soft cap is ~100/week; 18/day x 5 weekdays = 90/week. No 4 a.m. sends. |
| E. Mid-session breaks | After every 3-7 sends (random), inject a 20-60 min "lunch break" sleep. | Real sessions aren't continuous. |
The ideal ranking signal is the exact count of shared connections. But LinkedIn migrated this data to a graphql endpoint (voyagerSearchDashClusters) whose queryId hash rotates with every web deploy (~weekly). Hardcoding a hash means the tool breaks every few weeks; auto-discovering the hash from JS bundles works but adds a fragile scrape step.
Instead, this tool uses memberDistance, which is already returned for free in the existing profile-resolve call (identity/dash/profiles?decorationId=WebTopCardCore-16):
DISTANCE_1/DISTANCE_2-> 2nd-degree -> shares >=1 mutual -> high priorityDISTANCE_3/OUT_OF_NETWORK-> 3rd-degree+ -> no mutuals -> low priority
A binary bucket loses fine ranking within a bucket but captures the dominant social-proof signal at zero API cost and survives every LinkedIn deploy.
| Platform | Chrome (login only) | Rust | Status |
|---|---|---|---|
| macOS | required for one-time login | 1.78+ | Tested |
| Linux | required for one-time login | 1.78+ | Should work; not actively tested |
| Windows | required for one-time login | 1.78+ | Untested |
Once cookies are saved, neither Chrome nor a webdriver runs at automation time -- the tool makes plain HTTPS calls to linkedin.com/voyager/api/....
macOS
# 1. Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. Chrome (skip if already installed)
brew install --cask google-chrome
# 3. Project
git clone https://github.com/tr4m0ryp/Linkedin_Automation.git
cd Linkedin_Automation
cp .env.example .env
# Defaults are sensible; edit only if you want to deviate.
# 4. Build
cargo build --releaseLinux (Debian/Ubuntu)
# 1. Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. Chrome
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" \
>> /etc/apt/sources.list.d/google-chrome.list'
sudo apt update && sudo apt install -y google-chrome-stable
# 3. Project
git clone https://github.com/tr4m0ryp/Linkedin_Automation.git
cd Linkedin_Automation
cp .env.example .env
cargo build --releaseTwo columns required, two more added on first run:
linkedin_url,Is_Sent
https://www.linkedin.com/in/example-1/,0
https://www.linkedin.com/in/example-2/,0After the first discovery pass:
linkedin_url,Is_Sent,degree,degree_checked_at
https://www.linkedin.com/in/example-1/,1,2,2026-04-28T18:23:11+00:00
https://www.linkedin.com/in/example-2/,0,3,2026-04-28T18:24:42+00:00The reader is backwards-compatible -- legacy two-column files are accepted; missing columns are treated as unfetched.
cargo run --release -- --loginA Chrome window opens at linkedin.com/login. Log in by hand. The tool detects login completion, extracts cookies via the Chrome DevTools Protocol, and writes them to sessions/linkedin_cookies.json. Subsequent runs reuse those cookies until they expire.
cargo run --release -- --dry-run --csv-path linkedin_profiles.csvWalks the discovery pass and reports what would be sent without firing any invitations. Useful for verifying the CSV parses, the session is valid, and the rate-limit budget is comfortable.
cargo run --releaseThe runner enters its phased loop. Expect:
- Discovery on a few hundred unsent rows: ~30-60 minutes with humanized pacing.
- Send phase: 18 sends/day means a 500-row CSV takes ~28 working days at full pace. This is intentional; the cap is what keeps the account healthy.
- All actions logged via
tracing. SetRUST_LOG=linkedin_automation=debugfor fine-grained tracing.
src/
main.rs CLI entry, clap parsing, runner kickoff
lib.rs Crate root, module wiring
config.rs AppConfig + HumanizerConfig (envy parse)
error.rs LinkedInError (thiserror) + Result alias
automation/
runner.rs Phased orchestrator (D4)
discovery.rs Single discovery pass
connection_sender.rs Per-profile send logic
csv_reader/ CSV read/write with degree columns
humanizer/
mod.rs Humanizer facade
delay.rs LogNormalDelay (D5.A)
window.rs ActivityWindow + SessionStats (D5.D)
breaks.rs BreakScheduler (D5.E)
decoy.rs DecoyBrowser (D5.B)
types.rs Degree enum, ProfileRow
linkedin_api/
client/
mod.rs LinkedInClient (Voyager HTTP)
decoys.rs Decoy GET helpers (D5.B)
cdp.rs Chrome DevTools Protocol client
login.rs One-time browser login flow
session.rs Cookie jar load/save, CSRF extract
types.rs ProfileData, ConnectionState, etc.
Module split policy: 300-line cap per file, enforced. When a file approaches 200 lines it is proactively split into a directory module with mod.rs re-exporting the public API.
All knobs live in .env. Defaults are conservative.
| Key | Default | What it controls |
|---|---|---|
CSV_PATH |
linkedin_profiles.csv |
Target list path |
DAILY_WINDOW_START / _END |
09:00 / 19:00 |
Activity window in system-local time |
DAILY_SEND_CAP |
18 |
Max real invitations per day |
DEGREE_RECHECK_DAYS |
30 |
Refresh degree label if older than this |
SKIP_SEND_PROBABILITY |
0.07 |
Random skip-the-send rate (D5.C) |
BREAK_EVERY_MIN_SENDS / _MAX_ |
3 / 7 |
Trigger lunch break after this many sends |
BREAK_DURATION_MIN_SECS / _MAX_ |
1200 / 3600 |
Break length (20-60 min) |
DELAY_LOGNORMAL_MEDIAN_SECS |
720 |
Median send-to-send gap (12 min) |
DELAY_LOGNORMAL_SIGMA |
0.6 |
Distribution spread |
ME_PING_EVERY_N_SENDS |
5 |
/voyager/api/me decoy frequency |
COOKIE_FILE |
sessions/linkedin_cookies.json |
Persisted cookie path |
USER_AGENT |
Chrome 120 desktop | UA pinned per session |
RUST_LOG |
info |
Standard tracing level filter |
| Endpoint | Purpose |
|---|---|
GET /voyager/api/me |
Auth ping; periodic decoy /me |
GET /voyager/api/identity/dash/profiles?decorationId=WebTopCardCore-16 |
Resolve profile, get memberDistance, entityUrn, connection state |
POST /voyager/api/voyagerRelationshipsDashMemberRelationships?action=verifyQuotaAndCreateV2 |
Send invitation |
GET /voyager/api/feed/updatesV2 |
Decoy browse |
GET /voyager/api/me/notifications (with fallback) |
Decoy browse |
The tool optimizes against patterns LinkedIn's anti-abuse pipeline is documented (in patent filings and post-mortems on third-party sites) to track:
- Request rate distribution -- uniform timing is a giveaway. The lognormal distribution mimics organic gap distributions.
- Action graph --
fetch profile -> click connect1000 times in a row is unique to bots. Decoy browsing breaks this. - Time-of-day -- a session at 4 a.m. local time is suspicious. The activity window prevents this.
- Continuous activity -- humans take breaks. The break scheduler enforces them.
- Acceptance ratio -- declining/ignored invitations are a strong signal. Mutual-ranked targets accept at higher rates, indirectly improving this ratio.
cargo test # 19 unit tests across humanizer + csv + types
cargo fmt
cargo clippy --all-targets -- -D warnings
cargo audit # security advisoriesSee CLAUDE.md for project coding rules (file size limits, error-handling conventions, no-emojis policy).
- Exact mutual-connection counts via graphql
voyagerSearchDashClusterswith auto-discoveredqueryId. Currently deferred -- the queryId hash rotates per LinkedIn deploy and would require an ongoing maintenance burden. The binarymemberDistancebucket is good enough in practice. - Account-warmth-aware daily caps -- younger LinkedIn accounts have lower thresholds. Currently a static 18/day for all accounts.
- Per-day pacing variation -- mimic weekday/weekend asymmetry. Currently the same window applies every day.
- Configurable timezone -- currently uses system-local; should be overridable.
- Optional message customization -- currently sends a blank
customMessagefield; future versions may template per-target messages from CSV columns.
This tool is intended for authorized use on accounts you own, in compliance with:
- LinkedIn's User Agreement and Professional Community Policies
- GDPR / CCPA where the targets reside
- Any platform-specific or jurisdictional consent requirements
Automating contact with people who have not consented to be contacted may violate the laws of your jurisdiction and is not a use case the maintainers endorse. Users are solely responsible for the lawfulness and ethics of their target lists.
LinkedIn may flag, restrict, or terminate accounts that use third-party automation regardless of how carefully traffic is humanized. Use at your own risk.
MIT. See LICENSE (add one if missing -- without it, the code is legally unusable by anyone but the author).