seller check) goes through @rail402.dev/safe-fetch, a fetch
replacement on node:http(s) that refuses anything the egress policy does not allow. The Rail402 catalog uses
the same address rules for its own origin checks. Endpoints you configure yourself (the RPC, the Bazaar, the
facilitator) are not seller URLs and use the normal fetch.
A refusal is an error with an egress_* code and the stage it happened at (url, dns, connect, tls,
request, redirect, response). Errors name the host and the class of problem, never a resolved address.
Nothing is signed or reserved when a probe is refused, and an egress failure before the paid request reaches
the seller reports paid: none.
The default policy
Before the response headers arrive, a server may stay silent for as long as the headers timeout allows; the
idle timeout bounds the silence after them.
A paid request waits longer, because the seller verifies, serves and settles before it answers: its headers
and idle timeouts are the option’s
maxTimeoutSeconds plus 30 s, at most 120 s, and the whole exchange at most
150 s. This applies to paid HTTP requests and to paid MCP tool calls alike (for the legacy SSE transport, to its
stream from the start). A paid MCP tool call that would exceed the 64 KiB request limit once the payment is
attached is refused before it is sent (egress_request_too_large, paid: none).
Environment proxies are never used. Hop-by-hop headers (host, connection, content-length,
transfer-encoding, upgrade, expect, accept-encoding and similar) cannot be set by the caller.
Destinations
A host is refused when it is, or resolves to, anything outside the public internet:- IPv4:
0.0.0.0/8,10.0.0.0/8,100.64.0.0/10(carrier-grade NAT),127.0.0.0/8,169.254.0.0/16(link-local, including the169.254.169.254metadata address),172.16.0.0/12,192.0.0.0/24,192.0.2.0/24,192.88.99.0/24,192.168.0.0/16,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24, multicast and reserved (224.0.0.0/3); - IPv6: unspecified and loopback, IPv4-compatible, site-local, NAT64 (
64:ff9b::/96,64:ff9b:1::/48), discard, Teredo and other IETF assignments (2001::/23), documentation (2001:db8::/32,3fff::/20), 6to4 (2002::/16), IPv4-translated (::ffff:0:0:0/96), unique local (fc00::/7, which holdsfd00:ec2::254), link-local and multicast. IPv4-mapped addresses are judged by the IPv4 rules, and zone ids (fe80::1%eth0) are refused; - local names:
localhostand*.localhost,*.local,*.internal,*.home.arpa,*.lan, and single-label names.
0x7f.1 and 2130706433 for 127.0.0.1). Names are resolved once, and the connection is pinned to the
addresses that were checked: if any address a name resolves to is private, the name is refused, and the
connection dials only validated addresses. A DNS answer that changes between the check and the connection (DNS
rebinding) therefore cannot redirect the request.
Redirects
A paid request never follows a redirect: the payment is bound to one URL, so a3xx answer is reported as
payment_redirect_after_payment and the PAYMENT-SIGNATURE is never sent anywhere else. Requests that opt into
same-origin redirects (URL probes) follow at most 3 hops on the same host and port, each hop checked again; an
http URL may upgrade to https on the same host, and https never downgrades. 303 (and 301/302 after a POST)
continue as a body-less GET, as fetch does.
Local development
For a seller on your own machine, testnet buyers can allow plain http and loopback destinations explicitly. Nothing else private is ever allowed, and all three switches are refused onstellar:pubnet (a port list
there may only name 443).
Connections are bounded so that requests to many distinct hosts cannot pile up sockets:
maxSocketsPerHost
(default 4), maxTotalSockets (256), maxFreeSocketsPerHost (2) and idleSocketMs (15,000). With
keepAlive: false every connection closes after its answer, which suits a fetch shared by callers that name
arbitrary hosts.
Using safe-fetch directly
createSafeFetch(policy) takes schemes, ports, allowPrivateNetwork, redirects, limits and
timeouts. It returns a native Response. The test suites in packages/safe-fetch/test cover the address
ranges, IP literals in every spelling, local names, mixed and empty DNS answers, a rebinding name against real
local servers, redirect loops, cross-origin hops and downgrades, oversized requests, headers and bodies, a
compressed bomb, and header, idle and total timeouts.
@rail402.dev/safe-fetch/shared holds the parts with no Node module (the SafeFetch types, the policy
defaults, markFinalRequest and finalRequestOf), for transports such as a browser relay that keep the same
contract.
The Playground’s relay
The Playground’s page cannot reach most sellers itself, so its server relays each seller request with safe-fetch under a fixed policy: https on port 443 to public addresses only, redirects refused except same-origin hops for unpaid probes, and the limits above. It never relays to the Playground itself, the facilitator, the Bazaar or a host the operator denies, and forwards only allowlisted headers both ways. Its seller check (POST /api/seller-check) runs the SDK’s seller checks with the same
https-only, public-only policy. The server’s tests cover every private and metadata literal, DNS answers
with a private address, every spelling of the denied hosts and redirects off the seller’s origin, through
both the relay and the seller check (apps/playground/test/ssrf.test.ts).