> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rail402.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Secret handling

> Where the SDK, the MCP server and the CLI keep a buyer's signing key, what they never print, log or store, and the automated scan that checks it on every test run.

A buyer's signing key can move money, and a signed authorization is spendable by whoever holds it until it
expires. The SDK, `rail402-mcp` and `rail402` keep both out of everything they print, log, store or return.

## Keys

* **Key files are the preferred source.** A key file holds one `S…` seed and, on POSIX systems, must not be
  readable or writable by group or others; otherwise it is refused with `key_file_insecure`. `rail402 keys new`
  and `rail402 keys import` write key files with mode `0600` and never overwrite an existing file
  (`key_exists`). `keys import` reads the seed from stdin, without echo on a terminal. No command prints a seed.
* **A seed in the environment is read once.** `RAIL402_SECRET_KEY` is removed from the process environment as
  soon as the configuration is read, whether or not it is used, and the configuration object holds it as a
  hidden field that serializes and inspects as `[redacted]`. `RAIL402_MCP_TOKEN` is removed the same way.
* **The config file never holds a key.** The CLI refuses a config file with a field named like a secret, or any
  value that is a valid secret seed (`cli_config_invalid`). It may name a `keyFile`.
* **In memory, the key is a `KeyHolder`.** The seed is in a private field; the holder exposes only its address
  and `signAuthEntry`. Serializing, logging or inspecting it shows the address alone. Any SEP-43 signer (a
  wallet, a hardware signer) can take its place, and then the SDK never sees a key at all.

The key signs only what the [signing guard](/reference/spending-policy#4-the-signing-guard) allows: Soroban
authorization preimages for the selected payment, at most one per call; a payment never needs the buyer to sign
a transaction envelope. The only transactions signed with the key are the ones `rail402 account setup` builds
for the account itself (trustline and testnet exchange).

## Signed payments

* The `PAYMENT-SIGNATURE` header (or the MCP `_meta["x402/payment"]`) is sent only with the paid request to the
  origin that asked for payment. The unpaid probe carries none, and paid requests never follow redirects.
* Errors, events and results carry transaction hashes, public addresses, amounts and codes, never signatures,
  preimages or signed XDR. Every reason and detail string passes a redaction step that replaces any secret seed
  and any run of 64 or more base64 or hex characters with `[redacted]`, keeping public `G…`, `C…` and `M…`
  addresses. Seller-supplied rejection text is redacted before it is shortened, so a truncated echo of a signed
  payload cannot slip through.
* A seller's paid answer is untrusted content, and a seller may echo the request it received. Before a result is
  returned or stored, the call's own payment is removed from the seller's text and JSON (HTTP body, MCP content
  and structured content): the `PAYMENT-SIGNATURE` value, the payment JSON, its transaction or authorization
  XDR, and for each signed authorization entry the entry XDR, its signature value and bytes (hex, base64,
  base64url) and the signed preimage. Each is matched verbatim, JSON-escaped and URL-encoded, and a copy cut off
  at the end of truncated content is removed as well; every match is replaced with `[redacted]`. The rest of the
  seller's content is returned as sent.
* The budget ledger stores amounts, recipients, transaction hashes and the sanitized outcome of each call for
  idempotent replays (with the call's payment removed from the seller's content, as above); it never stores
  payment payloads or signatures. The SQLite file is created with mode
  `0600` in a `0700` directory.

## Output channels

* `rail402-mcp` writes logs to stderr only (stdout is the MCP stream). Its tool schemas contain no field for a
  key, seed, signature, token or authorization; tool results and errors never contain one, even when a seller
  echoes the payment into its answer.
* `rail402` writes one JSON document to stdout with `--json`; logs, progress and `--verbose` events go to stderr.
* `rail402-mcp --http` requires a bearer token whenever it can pay, compares it in constant time, and never logs
  it.

## The canary scan

Every `pnpm test` and `pnpm test:integration` run drives the payment flows with a fresh canary key and scans
everything the SDK, the MCP server and the CLI produced for it (`packages/testkit/src/secret-scan.ts`, with a self-test that
plants leaks and must fail). The needles are the seed (as strkey, hex, base64 and base64url), every signature
and preimage the key produced, every signed authorization entry, the payment transaction XDR, the
`PAYMENT-SIGNATURE` value, the MCP payment `_meta`, and the MCP HTTP bearer token, each also URL-encoded and
JSON-escaped, with every 24-character window of the long values searched so a truncated leak is found too. Any
valid secret seed found anywhere fails the scan as well.

The channels scanned:

* thrown errors: message, stack, details, the `cause` chain, `JSON.stringify` and `util.inspect`;
* SDK events;
* the MCP `tools/list` schemas, every tool result on success and on every error path, and every JSON-RPC
  frame and stderr line of the spawned server at `trace` log level;
* the CLI's stdout and stderr for every command, including `--verbose` and `--help`;
* the SQLite ledger file and its WAL and shared-memory files;
* a decoy origin, used as a redirect target and a second seller, that records every header it receives.

Every suite also pays an HTTP resource and an MCP tool that echo the payment they were paid with into their
answer, verbatim, as JSON, JSON-escaped and URL-encoded (`paymentEcho` in the same file), and scans what the
caller receives, idempotent replays and the ledger.

The suites are `packages/{sdk,mcp,cli}/test/secrets.test.ts` (in-process doubles) and
`secrets.integration.test.ts` (a private Stellar network, Rail402 in-process, stock x402 sellers, including
refusals, a failed settlement, a redirect after payment and echoing sellers).

## In the Playground

The [Playground](/playground/how-it-works#the-wallet) keeps a testnet key in the browser, encrypted under a
non-extractable key in IndexedDB, and signs through the same guard. The page never shows or stores the seed in
plain form; the only way out is a key file the visitor downloads after a confirmation. Its server holds no
key and refuses to start when any environment value is a secret seed.

The same canary scan runs over the Playground, with an imported canary key, in the server's tests
(`apps/playground/test/secret-scan.test.ts`), the engine's integration test on the private network
(`apps/playground/web/test/integration/engine.integration.test.ts`) and the browser tests in Chromium
(`apps/playground/e2e/playground.spec.ts`). Every payment the page sent is a needle, and a payment may appear
only in the request that carried it:

| Channel | Seed or key | Payment or signed data |
| - | - | - |
| The paid request to a seller (`PAYMENT-SIGNATURE` through the relay or to a lab seller, MCP `_meta`) | never | only in that request |
| What the page sends the RPC | never | bodies not scanned: simulations and submitted transactions are what the network must see |
| Every other request and answer: the relay, the proxies, `/api/config`, the seller check, Friendbot, Horizon, a decoy redirect target | never | never |
| The server's logs at `trace` level, its stderr, `Rail402-Trace` and every error body | never | never |
| The browser console, the page's text after each run, the run reports, localStorage, sessionStorage and IndexedDB | never | never |
| The key file the visitor downloads | the seed, by design | never |

The requests exempt from the payment needles (RPC calls and the paid requests) are still scanned, headers and
bodies, for the seed and its key bytes. The browser tests also check that the stored wallet key is not
extractable, and plant leaks that the scans must find: a payment where it does not belong, and the seed in an
RPC request body. The [live Playground run](/reference/conformance#playground) repeats the browser scan on the public
testnet.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.