Skip to main content
The Selo release gate is a composite GitHub Action at .github/actions/release-gate in the Selo repository. It runs the selo CLI in one of three modes: a free preflight, a paid test that fails the step unless every release check passed with evidence, and an offline verify of a signed attestation.
For a guided setup, see Gate deploys in GitHub Actions. This page is the reference.

Modes

Any other mode fails the step with exit 3 and the annotation mode must be preflight, test or verify.

preflight

Runs selo preflight with api, authorization, url, method and, when set, body. It checks authorization, terms and eligibility, and spends nothing.

test

  1. Deletes any existing summary and attestation files, so a stale file can never stand in for this run.
  2. Runs selo test with api, authorization, url, method, body (when set), contract, release, network, max-usdc, summary and attestation. It adds --pay-to when pay-to is set, and --approve-mainnet when approve-mainnet is exactly true.
  3. Reads gate and verdict from the summary file. If no summary was written, gate is BLOCK and verdict is none.
  4. If the CLI exited 0 but the summary does not say ALLOW, it blocks with exit 2 and the error annotation selo exited 0 without an ALLOW summary; blocking.
  5. If the CLI exited 2, it adds the warning annotation: The release test is not confirmed. Do not re-run this job until you have checked the earlier payment; a re-run starts a new paid test.
  6. Sets the outputs and exits with the CLI’s code.
Because GITHUB_STEP_SUMMARY is set in every step, the run’s summary page shows the verdict, each check, the job id and both transaction ids. See GitHub step summary.

verify

Runs selo verify with attestation, keys, release, url and method. In verify mode, an attested INCONCLUSIVE and an attestation that cannot be verified both exit 2, so verdict is none for both.

Inputs

An input the CLI requires but that is left empty fails the step with exit 3, for example --contract is required. File paths resolve against the job’s working directory, normally your checkout, so check out your repository before the action runs.

Outputs

Read them in later steps, or expose them as job outputs as the example does. Promote only when the gate job succeeded and its gate output is ALLOW.

How it installs the CLI

When you reference Enoch208/selo/.github/actions/release-gate@<ref>, GitHub fetches the Selo repository at that ref. The action then runs three steps:
1

Set up Node 24

actions/setup-node, pinned to commit 820762786026740c76f36085b0efc47a31fe5020, with node-version: "24".
2

Install the selo CLI

In the root of the fetched Selo repository:
The filter installs only the CLI and the workspace packages it depends on, from the committed lockfile.
3

Run the Selo gate

node <selo repository>/packages/cli/bin/selo.js <command> …, with the arguments built from the inputs. The step runs in your job’s working directory, so your files and the .selo/ run state live in your workspace.
Your repository does not need pnpm or a copy of Selo’s source.

Full example

This workflow, from examples/github-actions/release-gate.yml in the Selo repository, promotes a commit to production only after Selo has paid the staged release as a real x402 client and every release check passed with evidence.
.github/workflows/release-gate.yml
./scripts/build-and-test.sh and ./scripts/deploy.sh stand for your own build and deploy commands. Replace @main with a commit SHA you have reviewed.

Configuration it expects

The demo merchant in examples/merchant ships an example contract and request body. An authorization is valid for 24 hours after it is verified, so authorize again before a release window.

Verify-only example

The Selo repository’s own Release gate demo workflow (.github/workflows/release-gate-demo.yml) uses only verify mode. You pick a demo release, broken-v1 or fixed-v2, through its workflow_dispatch input release. It checks that release’s committed Mainnet attestation against the pinned key, and promotes only on ALLOW. Its jobs:
.github/workflows/release-gate-demo.yml (jobs)
On 26 October 2026 it blocked broken-v1, skipping promotion, and promoted fixed-v2.

Security

These rules come from the action and its example. Follow all of them before you put a funded wallet behind a workflow.
Run the paid job only on push to main or a manual dispatch, inside a protected environment with required reviewers that is limited to the main branch. Pull requests, including those from forks, never reach it. Do not add pull_request_target to the workflow.
Add SELO_PAYER_MNEMONIC as a secret of the protected environment, not as a repository secret, and pass it only through the env of the paid step. The CLI never prints it or writes it to disk.
Opt a dedicated payer wallet in to USDC (ASA 31566704 on Mainnet) and fund it with only what a few release tests cost. A release test costs Selo’s price, 1.00 USDC on the public API; Selo pays the target’s own price from its wallet. max-usdc caps what one run may pay.
The action refuses a 402 on another network, a different payee, a price above max-usdc, or Mainnet without approve-mainnet and pay-to, all before signing.
The action never retries a payment. If the outcome is unconfirmed, it exits 2 with a warning. The run state that would stop a second payment lives in the job’s workspace and does not carry over to a re-run, so a re-run starts a new paid test. Check the payer account for the expected transaction id printed in the log first. See Recover an interrupted run.
Logs, the job summary, selo-gate.json and selo-attestation.json contain no mnemonic, no signed payment and no private report token. Do not upload the .selo/ directory: it holds the private report link.
Replace @main with a commit SHA of the Selo repository that you have reviewed. Download https://api.useselo.xyz/.well-known/selo-attestation-keys.json once, review it and commit it, so the promote job trusts only a PASS that Selo signed for this exact commit, URL and method.
The example uses concurrency with cancel-in-progress: false, so runs for the same ref queue instead of cancelling a paid run part-way through.