Know when an x402 endpoint changes its terms
Settlens Monitor reruns the passive scan on your schedule and compares it with the last run. When a check that passed starts failing, such as a price above your ceiling or a recipient outside your allowlist, it exits non-zero and can post an alert to your webhook.
Real output: the demo /drift route alternates its price between requests.
settlens monitor --config settlens.config.json \
--state settlens.state.json --webhook http://127.0.0.1:4517/settlens
priced-api (GET http://127.0.0.1:4402/drift)
ALERT [high] policy.maximum_amount: pass -> fail — Amounts stay within the approved ceiling
1 blocking alert(s) at or above high.
In short
Settlens Monitor reruns the passive x402 scan on your schedule and compares every check with a stored baseline per method and URL. Regressions, new failing or warning checks and removed checks become alerts; blocking alerts exit non-zero and can POST a JSON webhook. It never sends a payment.
What Monitor does
-
A passive scan every run
Each run requests your endpoints exactly like Assess in passive mode. It never sends a payment signature, even if the active flags are given.
-
A baseline per endpoint
The state file stores the last observed status of every check, keyed by method and URL. The first run records it and alerts on nothing.
-
Regressions, not noise
A check that gets worse, a new check that fails or warns, or a check that disappears (lost coverage) is an alert. A check that stays failing is not repeated on every run.
-
Your severity threshold
Alerts at or above fail-on (high by default) are blocking: they set exit code 1 and trigger the webhook. Lower ones are printed as notes.
-
A webhook alert
With --webhook, a blocking alert sends one JSON POST with the threshold and every run's method, URL and alerts, which a relay such as n8n or an internal service can forward to chat or incident tools.
-
Bring your scheduler
Monitor is one command that exits. Cron, a systemd timer or a scheduled GitHub workflow decides how often it runs.
How a run works
-
Load the configuration and the baseline
Targets and policy come from the same configuration file as
settlens check. The baseline comes from--state; a missing file means a first run; unreadable or corrupt files fail closed. Without--state, every run is a first run and nothing is compared. -
Scan every target passively
Handshake, schema, binding, drift and policy checks run for each URL, plus facilitator checks for targets that name a facilitator.
-
Compare with the baseline
Each check is compared by id with its previous status. Regressions, new failures and removed checks become alerts; improvements are listed as resolved.
-
Save, report, alert
The new results replace the baseline in the state file. If any alert reaches the threshold, the webhook is called and the command exits with
1.
What counts as a regression
| Baseline | Current | Reported as |
|---|---|---|
| pass or skip | warn or fail | Alert: regression |
| warn | fail | Alert: regression |
| (not present) | warn or fail | Alert: new check |
| fail or warn | a better status | Resolved |
| any | the same status | Nothing |
| present | (not present) | Alert: removed (coverage lost) |
| (no baseline) | any | Baseline recorded |
An alert is blocking when the check's severity is at or above --fail-on, or failOn in the configuration. Blocking alerts print as ALERT, count toward the exit code and trigger the webhook. The others print as note.
A skip ranks like a pass, so a check moving between pass and skip, for example drift with a single sample, never raises an alert. A removed check keeps the severity it had in the baseline, so removing a high-severity check, such as the price ceiling, blocks at the default threshold.
Example: a price that changes between runs
The demo /drift route alternates its price between 10000 and 25000 atomic units on each request. With repeat set to 1, each run sees one price, so the change shows up between runs.
{
"version": 1,
"failOn": "high",
"targets": [
{
"name": "priced-api",
"url": "http://127.0.0.1:4402/drift",
"repeat": 1,
"expected": {
"networks": ["eip155:84532"],
"payTo": ["0x209693Bc6afc0C5328bA36FaF03C514EF312287C"],
"maxAmount": "10000"
}
}
]
}
settlens monitor --config settlens.config.json \
--state settlens.state.json --webhook http://127.0.0.1:4517/settlens
priced-api (GET http://127.0.0.1:4402/drift)
baseline recorded (first run)
0 blocking alert(s) at or above high.
settlens monitor --config settlens.config.json \
--state settlens.state.json --webhook http://127.0.0.1:4517/settlens
priced-api (GET http://127.0.0.1:4402/drift)
no regressions
resolved policy.maximum_amount: fail -> pass
0 blocking alert(s) at or above high.
The webhook body
Run 2 had one blocking alert, so Settlens sent this JSON as a POST with content-type: application/json to the local test receiver. The request times out after 5 seconds and refuses redirects. A webhook failure is printed on stderr and does not change the exit code.
{
"product": "Settlens Monitor",
"alerting": 1,
"failOn": "high",
"runs": [
{
"method": "GET",
"url": "http://127.0.0.1:4402/drift",
"alerts": [
{
"kind": "regression",
"id": "policy.maximum_amount",
"from": "pass",
"to": "fail",
"severity": "high",
"title": "Amounts stay within the approved ceiling"
}
]
}
]
}
Schedule it
Run Monitor wherever your endpoints are reachable from. Keep the state file somewhere that survives between runs.
# crontab: every 15 minutes, Monday to Friday
SETTLENS_WEBHOOK_URL=https://hooks.example.com/settlens
*/15 * * * 1-5 cd /opt/settlens && node src/cli.js monitor --config /etc/settlens/settlens.config.json --state /var/lib/settlens/settlens.state.json --webhook "$SETTLENS_WEBHOOK_URL" >> /var/log/settlens-monitor.log 2>&1
name: Settlens monitor
on:
schedule:
- cron: "*/30 * * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: "24"
package-manager-cache: false
- run: npm ci --omit=dev --ignore-scripts
- name: Restore the previous baseline
uses: actions/cache/restore@v4 # pin to a reviewed commit SHA
with:
path: settlens.state.json
key: settlens-state-${{ github.run_id }}
restore-keys: settlens-state-
- name: Compare with the baseline
env:
SETTLENS_WEBHOOK_URL: ${{ secrets.SETTLENS_WEBHOOK_URL }}
run: >-
node src/cli.js monitor
--config settlens.config.json
--state settlens.state.json
--webhook "$SETTLENS_WEBHOOK_URL"
- name: Save the new baseline
if: always()
uses: actions/cache/save@v4 # pin to a reviewed commit SHA
with:
path: settlens.state.json
key: settlens-state-${{ github.run_id }}
On GitHub Actions
This workflow runs in the Settlens repository, like the CI gate. The checkout and setup-node pins are the ones the repository already uses.
- The baseline lives in the Actions cache: each run restores the most recent
settlens-state-entry and saves a new one, even when the comparison fails. - The webhook URL is a secret passed through an environment variable, not interpolated into the command.
- GitHub runs schedules in UTC and can delay them under load; use a host scheduler when timing matters.
- Pin
actions/cacheto a commit SHA you have reviewed before using this in production.
The state file
The state file is plain JSON keyed by each target's method and URL, such as GET https://api.example.com/paid. For each target it keeps the id, status, severity and title of every check, the summary counts and the time of the run. It holds no header values, response bodies or keys.
The private workspace supports explicitly approved references, bounded price ranges, persistent incidents, acknowledgement, comments, daily reminders and durable notification delivery. Endpoint name and policy editing retains history. Solana profile checks remain offline and cannot establish settlement.
Because the key is the method and URL, changing an endpoint URL starts a new baseline; renaming it retains its method/URL identity, while a GET and a POST target on one URL keep separate ones. State files from Settlens 0.4.0, keyed by URL only, are migrated on the next run. Deleting the file resets every baseline; the next run records fresh ones without alerting.
Monitor prints its comparison but does not write report files. For a report you can archive or sign, run settlens check with the same configuration and attest it as evidence.
{
"GET http://127.0.0.1:4402/drift": {
"checks": [
{
"id": "transport.reachable",
"status": "pass",
"severity": "high",
"title": "Endpoint is reachable"
},
…
{
"id": "policy.maximum_amount",
"status": "pass",
"severity": "high",
"title": "Amounts stay within the approved ceiling"
}
],
"summary": { "pass": 15, "fail": 0, "warn": 0, "skip": 1 },
"observedAt": "2026-10-13T11:16:44.404Z"
}
}
What each run re-checks
Every passive, content and facilitator check from the catalog. Policy checks run when their field is set, facilitator checks when the target names a facilitator. Details in the check reference.
| Category | Checks |
|---|---|
| transport | transport.reachable, transport.status_402, transport.payment_required_header, transport.payment_required_encoding, transport.response_body_limit, transport.https, transport.redirect |
| schema | schema.official_x402_sdk, schema.payment_required, schema.payment_requirements, schema.duplicate_requirements |
| binding | binding.resource_url |
| freshness | freshness.timeout_window |
| policy | policy.scheme, policy.network, policy.recipient, policy.asset, policy.maximum_amount |
| stability | stability.payment_requirements |
| facilitator | facilitator.reachable, facilitator.response_body_limit, facilitator.supported_status, facilitator.content_type, facilitator.https, facilitator.supported_schema, facilitator.v2_capability, facilitator.duplicate_kinds, facilitator.requirement_coverage, facilitator.signer_coverage |
What Monitor does not do
- Legacy state mode alerts on changes. The baseline moves forward on every run, so a check that keeps failing alerts once, on the run where it changed. Journal mode retains active incidents and gates persistent findings; settlens check also provides an absolute pass or fail.
- No active probe. Monitor is passive only. The fail-closed probe stays a deliberate, authorized step.
- No scheduler and no history. It runs once and keeps only the latest state. Retention and dashboards are up to the system that runs it.
- A simple webhook. One attempt, a 5-second timeout, no retries and no signature or authentication header. Point it at an endpoint that can accept unauthenticated JSON, such as an internal relay.
- Not uptime monitoring. Reachability is one check among many, sampled when the run happens.
- Repeat comes from the configuration. The number of samples per run is set by
repeatin the configuration file (two by default).
Questions about monitoring
Does Monitor ever send a payment?
No. Every run is a passive scan with ordinary unauthenticated requests. It never sends a payment signature and never calls /settle.
How often should it run?
As often as a change would matter to you and your endpoints can tolerate: every 15 or 30 minutes is common. Each run makes a few requests per target (two by default, plus one per facilitator).
Why did a failing check stop alerting?
Monitor alerts when a status changes. After the run that reported it, the failure is part of the new baseline. Use settlens check in CI for an absolute gate.
What does the first run do?
It records the baseline for each target and exits 0, whatever it finds. Read its output or run settlens check first if you want to start from a known-good state.
What happens if I remove a check from the configuration?
The next run reports each check that disappeared as an alert of kind removed, printed as coverage lost, with the severity it had before. That is deliberate: deleting a price ceiling or a recipient allowlist should not go unnoticed. The run after that uses the new configuration as its baseline.
Can I monitor a facilitator?
Yes, through a target that names a facilitator: each run also reads its GET /supported and compares those checks with the baseline.
Watch one production endpoint
In a pilot we write the policy with you, set up the schedule and route alerts to the tool your team already reads.