Customer agent setup
Deploy and operate a Signal Diff agent on your infrastructure for crawls that cloud workers cannot reach. No repository clone or .NET SDK required.
On this page
When to use a customer agent vs cloud crawl
| Cloud crawl (default) | Customer agent |
|---|---|
| Public sitemaps reachable from the internet | Internal, staging, VPN-only, or firewall-restricted targets |
| No extra infrastructure | Worker on a VM, build agent, or container in your network |
| Fastest path for CI on public URLs | CI or schedules that must hit URLs only visible from your network |
Cloud crawls run on Signal Diff infrastructure. A customer agent pulls work from the API, runs checks locally, and reports results over HTTPS.
Prerequisites
- Paid API access — Create an API key on
Developers → API keys (for CI triggers, schedules, and agent enroll/claim).
Without an active paid key, enroll, rotate, claim, refresh-credential, and enrollment-token issue return
403withreason: subscription-lapsed; heartbeat still works. See Expiry recovery. - Outbound HTTPS — The agent host must reach your API origin (e.g.
https://signaldiff.dev) on port 443 for enroll (UI session or one-timesdet_token — see Headless enrollment), heartbeat, claim, refresh-credential, and report. - Agent routing enabled — The API must have configuration
Features:EnableAgentRoutingset totrue. On signaldiff.dev this is enabled; self-hosted API operators must set it in app settings or environment variables. - No repository access — Install from published downloads or hosted install scripts.
Setup: enroll → install → configure → verify
1. Enroll in the UI
- Open Customer agents and sign in (requires an active paid API key).
- Under Agent management, optionally set Agent ID, name, and pool.
- Click Enroll agent. Copy the one-time credential from the install commands. Re-enrolling the same agent ID rotates the previous credential.
- If a notification email is set in Settings, Signal Diff emails 7-day and 1-day warnings before the credential (or paid API key) expires, with links back here to rotate or refresh from Stripe.
2. Install and configure
After enrollment, use the Windows, Linux, or Docker tab on Customer agents:
- Install — Downloads
/install/agent.ps1or/install/agent.sh. - Configure — Downloads the release zip, verifies SHA256, extracts, and writes
appsettings.json. - Start — Runs the agent in the foreground (use a service for production; see below).
Default foreground paths: Windows %LOCALAPPDATA%\SignalDiff\Agent;
Linux ~/.local/share/signaldiff/agent.
Docker image: ghcr.io/funkysi1701/signaldiff-agent with SignalDiffAgent__* environment variables.
3. Verify heartbeat
- Start the agent (foreground, service, or container).
- On Customer agents, click Send test heartbeat.
- Confirm the agent row shows a recent Last heartbeat and a semver under Version (see Version tracking).
For CI runners and servers without a browser, use Headless enrollment instead of pasting a UI credential on the agent host.
Headless enrollment
Bootstrap agents on hosts that cannot open GitHub login. An operator with a signed-in session issues a short-lived one-time token
(sdet_…); the agent calls POST /api/agents/enroll with that Bearer token, then heartbeats normally.
Paid API keys (sck_) never authorize enroll.
1. Issue a machine enrollment token
From a signed-in machine (SWA session + paid entitlement):
curl -X POST 'https://signaldiff.dev/api/agents/enrollment-tokens' \
-H 'Content-Type: application/json' \
-H 'Cookie: <SWA session cookie>' \
-d '{"suggestedAgentId":"ci-runner-1","suggestedAgentPoolId":"production","ttlHours":1}'
Plaintext token is returned once. Default TTL is 1 hour (max 24). List with
GET /api/agents/enrollment-tokens; revoke unused tokens with
DELETE /api/agents/enrollment-tokens/{id}.
2. Configure and start (no browser on the agent)
"$HOME/signaldiff-install-agent.sh" \
--api-base-url 'https://signaldiff.dev' \
--enrollment-token 'sdet_…' \
--agent-id 'ci-runner-1' \
--agent-pool-id 'production' \
--skip-start
Windows: -EnrollmentToken. Docker: SignalDiffAgent__EnrollmentToken plus
SignalDiffAgent__CredentialTokenFile so the issued credential survives restarts.
The agent never treats a failed OTT as a credential.
3. After enroll
- Renewal:
POST /api/agents/{agentId}/refresh-credentialremains the headless renewal path (credential orRefreshSecret). - Rotate: UI Rotate or
POST /api/agents/{agentId}/rotate— separate from revoking an unused enrollment OTT. - Rate limits: issue / revoke / enroll share
AgentProtocol:RateLimits:EnrollPerMinute(default 20/min). - Errors:
enrollment-token-expired,-used,-revoked,-binding-mismatch,paid-key-not-allowed.
Agent pools and CI
Pools route jobs to specific agents. Use the same agentPoolId on the enrolled agent and on the crawl (schedule, dashboard, or CI).
An empty pool on both sides uses the default pool.
curl -X POST https://signaldiff.dev/api/trigger/ci \
-H "x-ci-api-key: sck_<your-key>" \
-H "Content-Type: application/json" \
-d '{"sitemapUrl":"https://internal.example/sitemap.xml","failMode":"never","executionMode":"agent","agentPoolId":"production"}'
In Schedules, choose Customer agent as execution mode.
See the Schedules guide for pool alignment.
For GitHub Actions, set execution_mode: agent and agent_pool_id on
signal-diff-action
(CI key examples on API keys; agent routing on Customer agents).
Run as a service
Re-run the install script with elevated privileges:
- Windows (Administrator):
-InstallService— servicesignaldiff-agent, default%ProgramData%\SignalDiff\Agent - Linux (
sudo):--install-service— systemdsignaldiff-agent, default/opt/signaldiff/agent - macOS (v1): foreground or
nohupuntil launchd support is added
Credential rotation
- On Customer agents, click Rotate on the agent row (blocked while a job is active).
- Copy the one-time refresh secret into durable deploy config (
SignalDiffAgent:RefreshSecretor install-RefreshSecret/--refresh-secret). - Restart the agent process or service on every host that uses that agent ID.
Current agents auto-refresh the short-lived credential when expiry is within 7 days, and once on claim token-expired.
Prefer storing the refresh secret (it does not expire with the credential). Re-enrolling the same Agent ID also rotates; Rotate is the supported UI path.
Recover from credential expiry vs subscription lapse
These are different failures. Read the JSON reason before changing config or billing.
| What you see | Cause | Fix |
|---|---|---|
401 reason: token-expired on heartbeat, claim, or report |
Short-lived agent credential expired (or rotated elsewhere). |
Restart the agent if RefreshSecret is configured (auto-refresh within 7 days, plus a 72-hour grace window for refresh-credential).
Otherwise Rotate on Customer agents and update every host.
Do not start a new Stripe checkout for this.
|
403 reason: subscription-lapsed on enroll, rotate, claim, or refresh-credential |
No active paid API key. Heartbeat still works. | If Stripe is still active, use Refresh expiry from Stripe on Customer agents or API keys. If the subscription ended, renew on Pricing, then enroll or rotate if needed. |
If both are true, fix billing first: refresh-credential also returns subscription-lapsed until a paid key is active.
Settings notification email (when set) sends 7-day and 1-day warnings. Full operator playbook:
Troubleshooting — customer agents.
Version tracking
Each agent reports its build version on heartbeat (User-Agent: SignalDiff.Agent/<version>).
On Customer agents, the Version column shows the reported semver and a badge:
| Badge | Meaning |
|---|---|
| Up to date | Matches the latest release on downloads/agent |
| Update available | A newer release is published—open Deploy for pinned upgrade commands |
| Unknown | No version reported yet (agent offline or pre-version build) |
When updates are pending, the fleet summary shows Need update and a Latest agent release link.
Download pages list each zip with an approximate file size and a .sha256 sidecar.
Upgrades
When Update available appears, click Deploy on the agent row.
An Update available banner shows copy-paste commands pinned to the latest release
(-UpdateOnly / --update-only, optional version flag). Your credential and appsettings.json are preserved.
- Check downloads/agent or
index.jsonfor the latest release. - From Deploy wizard: use the pinned update snippet when the banner is shown (recommended).
- Manual script: re-run the install script with
-UpdateOnly/--update-onlyand-RestartService/--restart-servicefor service installs. - Manual binary: stop the agent, replace binaries, keep
appsettings.json, restart. - Docker:
docker pullwith an explicit version tag and recreate the container.
Troubleshooting
| Symptom | What to check |
|---|---|
Heartbeat fails (401 token-expired) |
Short-lived credential expired — see Expiry recovery.
Heartbeat is not blocked for a lapsed subscription. Other 401 reason values:
rotate after version-mismatch, enroll after revoked, check ApiBaseUrl for format/signature errors.
|
Enroll / rotate / claim / refresh-credential / enrollment-token issue returns 403 subscription-lapsed |
Paid API key expired or missing. Renew on Pricing, or use Refresh expiry from Stripe on Customer agents or API keys if the subscription is still active. Heartbeat still works for fleet visibility. |
Headless enroll fails (enrollment-token-* / paid-key-not-allowed) |
Issue a fresh sdet_ token (Headless enrollment).
Do not paste a paid sck_ key or a credential into EnrollmentToken.
Binding mismatch does not consume the token — align agent id / pool and retry.
|
| Heartbeat OK, no crawls | Job executionMode must be agent. API Features:EnableAgentRouting must be true. Job tenant must match the agent owner. Also check for subscription-lapsed on claim. |
| Jobs stay Pending | Agent not running, outbound HTTPS blocked, or pool mismatch. |
| UI shows Running, agent idle | Stuck run without report; agent only claims Pending jobs. Wait for stale reconciliation or clear the job. |
| 429 Too Many Requests | Too many agents or aggressive polling; avoid duplicate processes with the same agent ID. |
| Version shows Unknown | Agent not heartbeating or on an old build. Start the agent and wait for the next heartbeat. |
| Firewall | Allow outbound HTTPS to the API host; no inbound ports on the agent. |
More scenarios (stuck Running, report chunk limits, CI pool alignment): Troubleshooting — customer agents.