# Your API Process Should Not Hold the Money Key

> Published: 2026-10-25 | Author: AgentBadge Team | Canonical: https://agentbadge.xyz/blog/keyless-signer

## 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.

![Three bounded keys replace one god-key: money, ops, verdict](/images/blog/keyless-signer-2.webp)

## 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.

![Trust boundary: API env holds ops + verdict keys, signer env holds the settle key behind policy](/images/blog/keyless-signer-d2.webp)

## 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.

![API process and signer service connected only by a Unix socket](/images/blog/keyless-signer-3.webp)

## 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.

![Sequence: paid request hits the health gate before x402; dead signer → 503 + operator alert, alive → socket sign + settle](/images/blog/keyless-signer-d1.webp)

## 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.

![Terminal showing signer audit log: green ok line and red denied line](/images/blog/keyless-signer-4.webp)

## 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](https://www.npmjs.com/package/@agentbadge/circle-payments) on npm.

- **Signer trust list:** GET https://agentbadge.xyz/keys/verdict-signers.json → {current, next} evaluator EOAs. Prefetch next before 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_unavailable means 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.*

---

## For AI Agents

- Companion guide: https://agentbadge.xyz/agent-guide/articles/keyless-signer
- Knowledge Index: https://agentbadge.xyz/agent-guide/
- LLM entry point: https://agentbadge.xyz/llms.txt
- Engineering services: https://agentbadge.xyz/agent-guide/team/services
