---
title: "x402 CI gate: GitHub Action with SARIF | Settlens"
description: "Run Settlens in GitHub Actions: block merges on high-severity x402 findings and keep JSON, HTML and SARIF reports as build evidence for every run."
url: https://settlens.com/product/ci
last_updated: 2026-10-13
---

# Block merges that break x402 payments

> **In short:** The Settlens GitHub Action runs the passive x402 checks of your configuration in pull request workflows. It fails the job when a failing check reaches the fail-on severity, high by default, and writes JSON, HTML and SARIF reports that the example workflow uploads to code scanning.

The Settlens GitHub Action runs the passive Settlens checks on every pull request. It fails the job when a finding reaches your severity threshold and writes JSON, HTML and SARIF reports as evidence.

[Request a pilot](https://settlens.com/contact) [GitHub Action docs](https://settlens.com/docs/github-action)

Real output against the demo target, with a policy and a facilitator configured. The facilitator does not support the advertised network, so the job fails with exit code 1.

**settlens check (what the Action runs)**

```text
$ settlens check --config settlens.config.json
Settlens x402 assessment c7bbc707
Targets 1 · Active 0 · Settlement calls 0
 
paid-api  GET http://127.0.0.1:4402/secure
26 pass · 1 fail · 0 warn · 0 skip
  FAIL high     Facilitator covers advertised payment requirements
       1 advertised payment choice(s) are absent from facilitator capabilities.
 
Total: 26 pass · 1 fail · 0 warn · 0 skip
```

## What the Action does

- **The same checks as Assess**

  It runs settlens check on your configuration: the 402 handshake, schema, binding, drift, your policy and, when configured, facilitator coverage.

  [What Assess checks](https://settlens.com/product/assess)

- **A severity threshold**

  fail-on decides which failures block the job. The default, high, blocks on high and critical findings.

- **SARIF, JSON and HTML**

  One run writes all three formats to the output directory, with a SHA-256 integrity digest in the JSON report.

- **Secrets go only to approved hosts**

  Custom headers are only sent to hosts listed in allowed-hosts, an input kept outside the configuration file.

- **Pinned and minimal**

  Node.js 24 through a commit-pinned setup action, then a locked production install with install scripts disabled.

- **Passive only**

  The Action never sends a payment signature. The authorized active probe is a manual, approved step.

## How it works

The composite Action has three steps, read here from action.yml.

1. **Set up Node.js**

   `actions/setup-node` pinned to commit `48b55a011bda`, Node.js 24, with the package-manager cache disabled.

2. **Install locked production dependencies**

   `npm ci --omit=dev --ignore-scripts` installs exactly the locked runtime dependencies and runs no install scripts.

3. **Run Settlens**

   Runs `settlens check --config … --out … --format … --fail-on …` with your inputs, exports `allowed-hosts` as `SETTLENS_ALLOWED_HOSTS`, and returns the CLI exit code as the step result. The `report-directory` output is set on every run, passing or failing.

## Inputs and outputs

_Inputs (from action.yml)_

| Input | Default | Description |
| --- | --- | --- |
| `config` | `settlens.config.json` | Path to a Settlens JSON configuration file. |
| `output` | `reports` | Directory for JSON, HTML and SARIF evidence reports. |
| `formats` | `json,html,sarif` | Comma-separated report formats. |
| `fail-on` | `high` | Minimum failing severity that blocks CI. |
| `allowed-hosts` | (empty) | Comma-separated exact hostnames allowed to receive custom headers. |

_Outputs (from action.yml)_

| Output | Description |
| --- | --- |
| `report-directory` | Directory containing the generated evidence. |

## Example workflow

This is the workflow shipped in examples/github-workflow.yml, shown unchanged.

**.github/workflows/settlens.yml**

```yaml
name: Settlens

on:
  pull_request:
  workflow_dispatch:

permissions:
  contents: read
  security-events: write

jobs:
  x402-assessment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

      # Until Settlens is published as a remote action, this repository can run
      # its local composite action directly.
      - name: Assess x402 endpoints
        uses: ./
        with:
          config: settlens.config.json
          output: reports
          formats: json,html,sarif
          fail-on: high
          # Set this from a repository variable when the config uses secrets.
          allowed-hosts: ${{ vars.SETTLENS_ALLOWED_HOSTS }}

      - name: Upload SARIF to code scanning
        if: >-
          always() &&
          hashFiles('reports/**/*.sarif') != '' &&
          (github.event_name != 'pull_request' ||
          github.event.pull_request.head.repo.full_name == github.repository)
        uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a
        with:
          sarif_file: reports

      - name: Upload evidence
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
        with:
          name: settlens-reports
          path: reports/
          if-no-files-found: error
          retention-days: 30
```

### What to notice

- **Permissions.** `contents: read` to check out, `security-events: write` to upload SARIF. Nothing else.
- **Pinned actions.** Third-party actions are referenced by commit SHA, not by a movable tag.
- **`uses: ./`** runs the composite Action from the Settlens checkout itself.
- **SARIF upload** runs even when the gate fails, but is skipped for pull requests from forks, which cannot write security events. See [SARIF and code scanning](https://settlens.com/product/ci#sarif).
- **Evidence** (JSON, HTML and SARIF) is uploaded as an artifact on every run and kept for 30 days; the step fails if no report was written.
- **`allowed-hosts`** comes from a repository variable, so a pull request that edits the configuration cannot add a host.

## Choose the threshold

Only failing checks count. Warnings, such as drift between requests or a plain-HTTP remote endpoint, are reported in every format but never fail the job. Today every medium- and low-severity check the Action runs reports a warning, not a failure, so fail-on: low or fail-on: medium blocks exactly the same findings as fail-on: high.

_fail-on values_

| fail-on | The job fails when | Checks at or above this severity |
| --- | --- | --- |
| `critical` | A check fails with critical severity. No check the Action runs is critical by default: critical findings come from the authorized active probe. | 0 |
| `high` | A check fails with high or critical severity. The default. | 19 |
| `medium` | A check fails with medium severity or above. | 27 |
| `low` | A check fails with low severity or above. | 29 |
| `none` | Never. Reports are still written. | 0 |

_Exit codes_

| Code | Meaning |
| --- | --- |
| 0 | No failing check at or above the threshold. |
| 1 | At least one failing check at or above the threshold. |
| 2 | CLI or configuration error, for example an unknown fail-on value or a custom header without an allowed host. |

## SARIF output

Each failing or warning check becomes one SARIF result whose rule id is the Settlens check id. Passing and skipped checks are left out.

_How results map to SARIF_

| Settlens result | SARIF level | security-severity |
| --- | --- | --- |
| fail, critical | `error` | 9.5 |
| fail, high | `error` | 8.0 |
| fail, medium | `warning` | 5.0 |
| fail, low | `warning` | 2.0 |
| warn, any severity | `warning` | From the severity, as above |

Every rule carries the tags `security`, `x402` and `agentic-payments`, and the run records the JSON report's integrity digest so a SARIF result can be traced back to the exact [evidence](https://settlens.com/product/evidence).

The results describe HTTP endpoints, not source lines, so each one is anchored at line 1 of the configuration file that declared the target, the `config` input of the Action. Code scanning shows Settlens findings as alerts on that file, with the endpoint in the message, and each target keeps its own fingerprint.

> **Code scanning on private repositories**
>
> On private repositories, code scanning requires GitHub Code Security; without it the upload step fails. GitHub's documented upload workflow also grants `actions: read` there, which the example above leaves out. The gate decision comes from the Settlens step's exit code, not from the upload, so add `continue-on-error: true` to the upload step if it should never fail the job on its own.

**One SARIF result (demo bypass)**

```json
{
  "ruleId": "security.invalid_signature_fail_closed",
  "level": "error",
  "message": {
    "text": "The endpoint returned HTTP 200 after an invalid payment authorization. Target: GET http://127.0.0.1:4402/bypass"
  },
  "locations": [
    {
      "physicalLocation": {
        "artifactLocation": { "uri": "settlens.config.json", "uriBaseId": "%SRCROOT%" },
        "region": { "startLine": 1 }
      }
    }
  ],
  "partialFingerprints": { "primaryLocationLineHash": "aee170b410d68e01:1" },
  "properties": {
    "status": "fail",
    "severity": "critical",
    "target": "http://127.0.0.1:4402/bypass",
    "recommendation": "Block resource execution unless cryptographic verification and settlement succeed. Treat this as a potential payment bypass.",
    "reportId": "d2145529"
  }
}
```

## Authenticated endpoints without leaking secrets

Configuration values can reference environment variables. Any target with custom headers must have its host allowlisted separately, so an edited configuration cannot forward a CI secret to a new host.

**settlens.config.json**

```json
{
  "version": 1,
  "failOn": "high",
  "targets": [
    {
      "name": "paid-api",
      "url": "https://merchant.example/api/data",
      "headers": { "Authorization": "Bearer ${ASSESSMENT_TOKEN}" },
      "expected": { "networks": ["eip155:8453"], "maxAmount": "10000" }
    }
  ]
}
```

**Workflow step**

```yaml
- name: Assess x402 endpoints
  uses: ./
  env:
    ASSESSMENT_TOKEN: ${{ secrets.ASSESSMENT_TOKEN }}
  with:
    config: settlens.config.json
    allowed-hosts: merchant.example
```

**The allowlist is enforced before any request**

```text
$ settlens scan http://127.0.0.1:4402/secure \
    --header "Authorization: Bearer test"
Settlens error: Custom headers require an explicit allowed host. Add --allow-host for: 127.0.0.1
$ SETTLENS_ALLOWED_HOSTS=127.0.0.1 settlens scan http://127.0.0.1:4402/secure \
    --header "Authorization: Bearer test" --no-write
…
Total: 13 pass · 0 fail · 0 warn · 0 skip
```

- `${ASSESSMENT_TOKEN}` is expanded at run time; a missing variable stops the run.
- Reports keep request header _names_ only, never values.
- A `PAYMENT-SIGNATURE` header in the configuration is refused outright.
- Outside the Action, set `SETTLENS_ALLOWED_HOSTS` to a comma-separated list of exact hostnames, as above.

## Outside GitHub: the container

The same CLI ships as a container image for other CI systems and isolated hosts. The base image is pinned by digest, dependencies are installed from the lockfile with install scripts disabled, npm is removed from the runtime image and the process runs as the unprivileged `node` user. The working directory is `/workspace`.

Mount the configuration read-only and a reports directory writable by UID 1000. The container exits with the same codes as the CLI, so any CI system can gate on it.

**Build and run**

```shell
docker build -t settlens:local .
docker run --rm settlens:local --version
docker run --rm \
  -v "$PWD/config:/workspace/config:ro" \
  -v "$PWD/evidence:/workspace/reports" \
  settlens:local check --config config/settlens.config.json
```

## Checks the gate can run

Rendered from the check catalog. Facilitator checks run only for targets with a `facilitator`; policy checks only when their field is set. See the [check reference](https://settlens.com/docs/checks) for fixes.

_29 checks the Action can report_

| Check | Default severity | Blocks at fail-on high |
| --- | --- | --- |
| `transport.reachable` ; Endpoint is reachable | high | Yes, when it fails |
| `transport.status_402` ; Unauthenticated request requires payment | high | Yes, when it fails |
| `transport.payment_required_header` ; PAYMENT-REQUIRED header is present | high | Yes, when it fails |
| `transport.payment_required_encoding` ; PAYMENT-REQUIRED decodes as Base64 JSON | high | Yes, when it fails |
| `transport.response_body_limit` ; Response body stays within the evidence limit | medium | No |
| `transport.https` ; Transport protects payment metadata | medium | No |
| `transport.redirect` ; Payment endpoint does not redirect unexpectedly | medium | No |
| `schema.official_x402_sdk` ; Official x402 SDK accepts PaymentRequired | high | Yes, when it fails |
| `schema.payment_required` ; PaymentRequired matches the x402 V2 core shape | high | Yes, when it fails |
| `binding.resource_url` ; Payment requirement is bound to the requested resource | high | Yes, when it fails |
| `schema.payment_requirements` ; Accepted payment requirements are well formed | high | Yes, when it fails |
| `schema.duplicate_requirements` ; Payment choices are unambiguous | low | No |
| `freshness.timeout_window` ; Authorization windows are reasonably bounded | medium | No |
| `policy.scheme` ; Schemes match the local policy | high | Yes, when it fails |
| `policy.network` ; Networks match the local policy | high | Yes, when it fails |
| `policy.recipient` ; Recipients match the local policy | high | Yes, when it fails |
| `policy.asset` ; Assets match the local policy | high | Yes, when it fails |
| `policy.maximum_amount` ; Amounts stay within the approved ceiling | high | Yes, when it fails |
| `stability.payment_requirements` ; Payment requirements remain stable | medium | No |
| `facilitator.reachable` ; Facilitator /supported is reachable | high | Yes, when it fails |
| `facilitator.response_body_limit` ; Facilitator capability response fits the evidence limit | high | Yes, when it fails |
| `facilitator.supported_status` ; Facilitator exposes capabilities successfully | high | Yes, when it fails |
| `facilitator.content_type` ; Facilitator capabilities use a JSON media type | medium | No |
| `facilitator.https` ; Facilitator capability transport is protected | medium | No |
| `facilitator.supported_schema` ; SupportedResponse matches the x402 core shape | high | Yes, when it fails |
| `facilitator.v2_capability` ; Facilitator advertises x402 V2 capabilities | high | Yes, when it fails |
| `facilitator.duplicate_kinds` ; Facilitator capabilities are unambiguous | low | No |
| `facilitator.requirement_coverage` ; Facilitator covers advertised payment requirements | high | Yes, when it fails |
| `facilitator.signer_coverage` ; Facilitator publishes signer coverage | medium | No |

## What the CI gate does not do

- **No active probe.** The Action runs passive checks only. Prove fail-closed behavior with an [authorized probe](https://settlens.com/product/assess#active-probe), run deliberately.
- **Not on the Marketplace.** The Action ships in the private Settlens repository given to pilot participants and runs from that checkout.
- **Network access required.** The runner must reach your endpoints. Staging behind a VPN needs a runner inside it.
- **Warnings never block.** Drift, redirects, plain HTTP and other warnings appear in the reports but do not fail the job.
- **Not signed automatically.** Reports carry an integrity digest; sign them with [evidence attestations](https://settlens.com/product/evidence) when they leave your team.
- **A green build is a sample.** It shows the endpoint passed at build time. Use [Monitor](https://settlens.com/product/monitor) to keep watching it in production.

## Questions about the CI gate

**Which findings fail the job by default?**

Failing checks with high or critical severity. Set fail-on to critical, medium, low or none to change that. Warnings never fail the job.

**Does the Action send payments or need a wallet?**

No. It runs passive checks with ordinary unauthenticated requests. It never sends a payment signature, never calls /settle and needs no key.

**Can I use it in GitLab CI, Jenkins or another system?**

Yes, through the CLI or the container image: run settlens check and gate on the exit code (0 pass, 1 findings at the threshold, 2 error).

**Where do the reports go?**

To the output directory (reports by default), which every run, passing or failing, also exposes as the report-directory output. The example workflow uploads them as a build artifact kept for 30 days.

**Why is allowed-hosts separate from the configuration?**

So that a pull request that changes the configuration cannot redirect a CI secret to a host you did not approve. The allowlist lives in a repository variable instead.

## Related

- **[Assess](https://settlens.com/product/assess)**

  Passive conformance and policy scan, plus the authorized fail-closed probe.

- **[Verify](https://settlens.com/product/verify)**

  Offline EIP-3009 and Solana signature checks, and facilitator checks.

- **[Monitor](https://settlens.com/product/monitor)**

  Scheduled passive scans compared to a baseline, with regression alerts.

- **[Evidence](https://settlens.com/product/evidence)**

  ed25519 attestations over the report digest, verifiable offline.

Reference: [GitHub Action guide](https://settlens.com/docs/github-action) · [configuration file](https://settlens.com/docs/configuration) · [report formats](https://settlens.com/docs/reports) · [CLI exit codes](https://settlens.com/docs/cli) · [Settlens for API sellers](https://settlens.com/use-cases/api-sellers) · [the x402 security checklist](https://settlens.com/learn/x402-security-checklist) · [gate an x402 V1 to V2 migration](https://settlens.com/learn/x402-v1-to-v2-migration).

## Put one endpoint behind the gate

Pilot participants get the repository, the Action and help writing the first policy for their endpoint.

[Request a pilot](https://settlens.com/contact) [See how Assess works](https://settlens.com/product/assess)

## Sources

1. [Static Analysis Results Interchange Format (SARIF) Version 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) — OASIS
2. [Uploading a SARIF file to GitHub](https://docs.github.com/en/code-security/how-tos/find-and-fix-code-vulnerabilities/integrate-with-existing-tools/upload-sarif-file) — GitHub Docs
