Skip to main content
Selo fetches URLs that other people give it, and it spends real money. Its security model answers five questions: where Selo will connect, how much it can spend, what it keeps out of its records, who can read a report, and what the CLI does with your wallet. Every rule on this page is enforced in code and covered by tests.

Authorization comes first

Selo sends no request to a route unless a verified, unexpired authorization covers its exact origin, path and method. The address rules below apply on top of that: even an authorized URL is refused if it points at a private network.

Target URL rules

Every target URL, and every redirect hop Selo is allowed to follow, must pass these rules before any DNS lookup. The fragment is dropped; the query string is kept.

Blocked address ranges

After DNS resolution, every resolved address is classified. Selo connects only if every address in the answer is public. For IPv6:
  • Any address outside 2000::/3 (global unicast) is blocked. That covers ::1 loopback, :: unspecified, fc00::/7 unique local, fe80::/10 link-local and ff00::/8 multicast.
  • 2001::/32 (Teredo) and 2001:db8::/32 (documentation) are blocked.
  • IPv4-mapped (::ffff:a.b.c.d), IPv4-compatible, NAT64 (64:ff9b::/96) and 6to4 (2002::/16) addresses are judged by the IPv4 address they embed.
  • Any address with a zone index (%eth0) is blocked.

DNS resolution and pinning

Checking a hostname and then letting the HTTP client resolve it again would leave a gap: a DNS rebinding attack can answer the second lookup with a private address. Selo closes that gap:
  1. It resolves the hostname once and collects every address in the answer. An empty answer is a network failure.
  2. It refuses the request if any address is not public.
  3. It connects to the first vetted address through a connector pinned to that hostname and address. The connector refuses to resolve any other hostname.
  4. TLS still validates the certificate against the hostname, so pinning does not weaken HTTPS.
  5. Each redirect hop is resolved, classified and pinned again from scratch.
The connection must be established within 5 seconds.

Redirects

When Selo follows a redirect, each hop must pass every URL rule and stay on the same origin, and Selo strips authorization, cookie, proxy-authorization, payment-signature and x-payment from the request. A 301, 302 or 303 becomes a GET without a body.

Time and size limits

A body that announces a content-length over the limit is refused before it is read; a streamed body is cut off as soon as it passes the limit.

Spend caps

A job can spend at most 0.50 USDC paying your endpoint, and no configuration can raise a job’s budget above 5.00 USDC. The budget is enforced by one conditional database update in the same transaction that records the payment, with CHECK constraints as a backstop, and any payment with an unclear outcome stops all further spending for the job. See Payments and spend limits.

Secrets never reach evidence or logs

Selo redacts in two layers. A secret is replaced by sha256: followed by the hex SHA-256 of its value, so you can still match it against a value you hold without Selo keeping it.

Layer 1: headers at capture

When Selo records a response’s headers, it replaces the values of authorization, proxy-authorization, cookie, set-cookie (each cookie separately), payment-signature and x-payment with their hash. The signed payment header that Selo sends is never stored; evidence keeps only its hash.

Layer 2: every stored or logged value

Every value written to evidence, scenario records, the evidence packet and alerts passes through a sanitizer:
  • Secret keys. A field whose name, lowercased with _ and - removed, is authorization, proxyauthorization, cookie, setcookie, xpayment, paymentsignature, sk, sig, accesstoken, refreshtoken, idtoken, apikey or bearer, or contains mnemonic, secret, privatekey, password, passphrase or signature, has its value hashed (strings) or replaced by [redacted] (anything else). An authorization field that holds an object, such as an authorization snapshot, is sanitized field by field instead.
  • Secret values anywhere. Inside any string, a base64 token of 40 or more characters that decodes to a signed Algorand transaction, or to JSON that contains a secret, is hashed.
  • Wallet phrases. A run of 24 or more words of which at least 90% are Algorand mnemonic words is hashed, even with a mistyped word or inside longer text.
  • Bounds. Strings are truncated at 4,096 characters after secrets are hashed, and nesting is cut at depth 12.
The test A22: logs, evidence, reports and the evidence packet contain no wallet secret runs a full paid release test, then searches stdout, stderr, the evidence table, the API response, the report and the evidence packet for any three consecutive words of the mnemonic of Selo’s operating wallet, which signs the downstream payment, and for every signed payment header. None may appear.
Each report lives at https://api.useselo.xyz/v1/reports/<token>:
  • The token is an HMAC-SHA256 of the job id under a server secret of at least 32 characters, encoded as 43 base64url characters. It cannot be guessed from the job id.
  • Selo stores only the SHA-256 of the token. A database copy does not reveal any report link.
  • The route serves only jobs whose report is written. An unknown and a malformed token get the same 404 answer.
  • Every answer carries X-Robots-Tag: noindex, nofollow, Cache-Control: private, no-store and Referrer-Policy: no-referrer.
Anyone who holds the link can read the report, so treat it like a password. The CLI prints the link with its token replaced by <private> and writes the same redacted form into gate summaries; the full link is kept only in the local run file under .selo/runs/.

The CLI and your wallet

The selo CLI pays Selo from the wallet in SELO_PAYER_MNEMONIC. Use a dedicated payer wallet that holds only what you are willing to spend.
  • The mnemonic is read from the environment and never printed or written to disk; neither is the signed payment or the report token.
  • On Mainnet, the CLI refuses to pay without --approve-mainnet and the --pay-to address you expect Selo to use. It checks both before sending any request.
  • It refuses before signing if Selo’s price is above --max-usdc or Selo’s 402 names a network other than --network.
  • It never retries a payment on its own. If an outcome is unconfirmed, it records the attempt in .selo/runs/<release>.json and refuses to pay again until you resume.
  • With GITHUB_STEP_SUMMARY set, it appends a sanitized summary of the gate decision.
See the CLI overview for every flag.

What Selo stores

Selo never stores your mnemonic (the CLI never sends it), the signed payment header, the report token or the verification token.
The preflight requestBody is stored as you sent it and is sent to your endpoint on every request of the run. Use test data, not real credentials or personal data.

Authorization

How a route is authorized and when Selo re-checks it.

Payments and spend limits

The budget invariant and the payment states.

Known limitations

What Selo does not do yet.

Output files

The run file, the gate summary and the attestation the CLI writes.