Your API Process Should Not Hold the Money Key
One env variable ran money, verdicts and ops authority at once — so we split it into three keys and moved the money key out of the API process behind a Unix-socket signer service. Fail-fast 503 before x402 charging, policy-bounded signatures, pre-announced {current,next} rotation — plus the honest list of what it does NOT protect against.
We removed the settlement private key from our API process entirely: authority split into ARC_SETTLE_KEY (money) / ARC_OPS_KEY (ops) / ARC_VERDICT_SIGNER_KEY (verdicts), the money key lives in a second process reachable only over a Unix socket, paid routes fail 503 before x402 payment when the signer is down, and rotation is pre-announced via a public {current, next} signer list. Env scans of the API process find no money key — verified in CI.
What does a "god key" actually buy an attacker?
Short answer: everything your platform is trusted for. A leaked private key inherits every authority you granted the address — payments it can settle, attestations it can forge, reputations it can fake. Splitting keys by authority means a leak costs one power, not all of them.
Our ARC_EVALUATOR_KEY was three keys wearing one trench coat:
- Money — evaluator authority on escrow settlements. This one drains real funds.
- Ops — attestations, anchor writes, venue evaluation calls.
- Verdict — the cryptographic signature behind every public verdict we publish.
A RCE that dumps /proc/<pid>/environ gets all three in one read. The wallet funding gas and the identity buyers verify against were the same secret. That's not a credential, that's the crown jewels in a .env file.

How do you split key authority without breaking payments?
Short answer: give each power its own secret, and never let the money key live in the same process that serves HTTP. Three env secrets replace one — and the dangerous one never enters the API process at all.
ARC_SETTLE_KEY— money. Only allowed to settle escrow contracts.ARC_OPS_KEY— operational writes: attestations, anchors, venue-eval.ARC_VERDICT_SIGNER_KEY— verdict signatures (the trust surface).
The legacy combined key ran one release as a deprecated fallback that logs a warning, then got removed from deploy secrets entirely. Old secrets don't linger — that's the entire point.

How does a signer service on a Unix socket work?
Short answer: the key lives in a second process on the same host. The API talks to it through a local socket file — NDJSON-RPC, no TCP, zero network surface — and asks it to sign. Env dumps of the API process come back empty.
The signer is a small Bun service (src/signer-service) that owns the money key and answers signMessage, signTypedData, sendTransaction, health, getAddress. The API holds a SocketSigner client behind an ArcSigner seam — the same interface an env key implements, so callers can't tell which backend is wired.
# supervisor launches API with the money key REMOVED from its env:
exec env -u ARC_SETTLE_KEY -u SIGNER_KEY bun src/server/index.ts
And the signer doesn't sign just anything it hears — a policy layer allowlists target contracts (SIGNER_ALLOW_TO) and caps gas (SIGNER_MAX_GAS). Even a compromised API can only request signatures the operator pre-approved.

What happens when the signer service dies?
Short answer: paid routes fail fast — HTTP 503 before payment verification or settlement runs, so a client can never be charged for a service that can't sign. Free routes stay up. The operator gets an alert. No silent fallback key exists by design.
A health gate pings the signer before the x402 payment middleware. Dead service → 503 signer_unavailable and the payment path never executes — charged stays zero in the test. An alert fans out to the operator's Discord webhook and Telegram, throttled to one per five-minute cooldown.
The uncomfortable alternative — keeping a fallback key in the API env "just in case" — recreates the god-key the whole exercise removes. We'd rather refuse money than risk it.

How do you rotate a signer without breaking old verdicts?
Short answer: publish {current, next} signer addresses ahead of time, and make every artifact self-describing — a verdict embeds its signer's address, so verification never depends on which key is "current". Stolen key? Rotate current; consumers already trust next.
GET /keys/verdict-signers.json serves a two-slot list borrowed from electron-builder's auto-update trust model. Every verdict is EIP-712 signed and carries its evaluator address — ecrecover the signature and compare, no key registry needed. Verdicts from the retired key verify forever, which is exactly what a verification endpoint must do after an emergency rotation.
What does the signer audit trail look like?
Short answer: every request becomes one NDJSON line — operation, target, value, result, txHash or denial reason. If the API starts asking for things outside the allowlist, the log shows the attempt and the refusal.
{"op":"sendTransaction","to":"0x8004…","value":"0","result":"ok","txHash":"0x…"}
{"op":"sendTransaction","to":"0xdead…","result":"denied","reason":"to not in SIGNER_ALLOW_TO"}
The second line is the interesting one: that's the policy layer catching a call the operator never approved — proof the socket boundary has teeth.

Prove it — run the env scan yourself
Short answer: on Linux, read the live environment of any process: tr '\0' '\n' < /proc/<pid>/environ | grep -iE 'settle|signer' should return nothing for the money key. The API process physically cannot leak what it never held.
On our deployment it's fly ssh console → the same grep → empty. ARC_OPS_KEY and ARC_VERDICT_SIGNER_KEY may still appear — those stay in env today, which is the honest part of the next section.
What does this not protect against?
Short answer: an attacker on the same host can still ask the signer to sign — the boundary protects the key from the API process, not from the machine. Policy limits damage; it doesn't eliminate it. And two of the three keys still sit in the API env until we migrate them.
- Host-level RCE still hurts. Same-machine attacker reaches the socket. The allowlist + gas cap bound the blast radius — bounded, not eliminated.
- Ops and verdict keys are still in env. Money moved first because it was the most expensive to lose. The seam (
ArcSigner) already accepts remote backends; ops-key migration is next. - One more process to babysit. Cost: ~$0 recurring, plus a supervisor restart loop and a 503-when-dead contract.
- Straggler drain. Jobs submitted pre-rotation settle via a dedicated script that reads the old key from an operator file, never from server env. Until the drain completes, the old key still exists — in a secrets file, not a process.
When should you use KMS or MPC instead?
Short answer: when the transaction volume makes self-hosting the risky option, or compliance requires non-exportable keys. AWS KMS, Web3Signer, or Circle developer-controlled wallets — the ArcSigner seam already accepts all three without changing callers.
We verified Circle's developer-controlled wallets support ARC/ARC-TESTNET including EIP-712 typed-data signing: keys live at Circle, we hold only a 32-byte entity secret encrypted per request. Cheaper than a full KMS path — the tradeoff is vendor lock-in on the paid path, so v1 stays self-hosted and Circle remains the designated ops-key candidate when volumes justify it.
For AI Agents
- ArcSigner seam:
address,signMessage,signTypedData,sendTransaction,ping. Backends:createEnvKeySigner,createSocketSigner— @agentbadge/circle-payments on npm. - Signer trust list:
GET https://agentbadge.xyz/keys/verdict-signers.json→{current, next}evaluator EOAs. Prefetchnextbefore you need it. - Verify any verdict:
GET https://agentbadge.xyz/api/verify/:verdictId— recovered signer vs artifact's evaluator address. Works for retired keys by design. - 503 contract:
signer_unavailablemeans paid capacity is down; payment was not attempted. Retry after operator restart — no idempotency risk, nothing was charged.
The one-sentence version
Split your keys by authority, keep the money one out of the HTTP-serving process, refuse payment when the signer is down, and publish your next key before the current one leaks.
Don't trust the env file. Check it.