# ultron-li — LinkedIn outreach engine

"Smartlead for LinkedIn", Milestone 1. Sits on top of **Unipile** (the managed, api-first LinkedIn
transport) the same way `ultron-wa/` sits on Baileys. Schema lives in `db/schema.sql` §26.

## What it is
A single plain Node process (systemd `ultron-li` on the VPS) that ticks every 60s and:
1. syncs connected LinkedIn accounts from Unipile (`li_accounts`),
2. resolves each queued recipient's LinkedIn URL → Unipile `provider_id` (a small buffer ahead),
3. **drips connection invites** per running campaign × account — inside the send window / active
   days, under per-account **daily + weekly caps** and a jittered cooldown that spreads sends
   across the window,
4. on **invite-accepted** (webhook → `li_ingest_event` → recipient flips to `accepted`), sends the
   **opener**, then **2 follow-ups** on their day offsets — a **reply stops the sequence**,
5. withdraws stale pending invites (hygiene), pings Ahmed on Telegram for every reply, and marks
   drained campaigns done.

No socket to babysit — the transport is REST. Inbound events arrive via the Railway dashboard's
webhook receiver (`POST /api/li/webhook`), which calls the `li_ingest_event` RPC.

## The safe envelope (research 2026-07-30 — do NOT loosen without cause)
- ~100 invites/**week** rolling is LinkedIn's real cap regardless of tier → default 12/day/account,
  daily cap 15, weekly cap 90, all in `li_accounts` (editable per account in the UI).
- **Acceptance rate is the tripwire** — below ~20% LinkedIn throttles you before any numeric cap.
  Tight ICP targeting is the safety mechanism, not an afterthought.
- Blank invite by default (research: notes don't raise acceptance, only later reply rate — so
  personalize the **opener**, not the invite). Max **4 touches** then stop.
- Saudi work week Sun–Thu, 09:00–17:00 Asia/Riyadh defaults.

## Config (env, in `/root/ultron/.env`)
- `UNIPILE_DSN` — e.g. `api8.unipile.com:13851` (from the Unipile dashboard).
- `UNIPILE_API_KEY` — the X-API-KEY.
- `UNIPILE_WEBHOOK_SECRET` — shared secret the dashboard checks on `/api/li/webhook` (optional).
- Kill switch: `LI_KILL=1`. Tick: `LI_TICK_MS` (default 60000).
Not configured → the engine idles quietly (logs once) so it's safe to deploy before onboarding.

## Connecting a LinkedIn account
The dashboard (`/li-numbers`) calls Unipile **hosted auth** and shows the returned link — Ahmed (or
a teammate) opens it, logs LinkedIn in on Unipile's page (2FA handled there), Unipile assigns a
sticky per-account proxy, and a `name=account` webhook flips the row to `connected`.

## Deploy
```bash
rsync -az --exclude node_modules ultron-li/ ultron:/root/ultron/ultron-li/
ssh ultron 'cd /root/ultron/ultron-li && npm install --omit=dev'
ssh ultron 'cp /root/ultron/ultron-li/ultron-li.service /etc/systemd/system/ && systemctl daemon-reload && systemctl enable --now ultron-li'
ssh ultron 'systemctl restart ultron-li && journalctl -u ultron-li -n 30 --no-pager'
```
Register the Unipile webhook once (points at the Railway dashboard):
`POST https://<dsn>/api/v1/webhooks` with the messaging + account-status sources →
`https://<dashboard-origin>/api/li/webhook?secret=<UNIPILE_WEBHOOK_SECRET>`.
```
