Key handling
Every key in this system has one job and a temperature. The design goal is simple to state: a key that leaks should cost as little as possible, and the keys that could cost the most should be the hardest to reach.
Your keys
CoRelayer never sees a private key of yours. There is no key import, no custodial wallet and no "connect by pasting your seed phrase" — those things do not exist in the product, so they cannot be phished from it.
| You hold | Used for |
|---|---|
| Your wallet key | Signing transactions and native-auth logins |
| Optionally, a guardian | Your own protection; a guarded transaction is relayed like any other, with its extra gas accounted for |
Our keys
| Key | Where it lives | What it can do |
|---|---|---|
| Owner (the deployer) | An offline keystore. Never on a server. | Everything on chain: tariff, tiers, Relay Unit schedule, rate classes, treasury, operator, unpausing, registering relayers, upgrades. |
| Operator | A human-held warm wallet. Not on a server. | Pause anything; activate, drain, retire and reweight relayers; lower caps. Cannot unpause, reprice, or register a relayer of its own. |
| Reporter | Inside the backend's signer, on the core hosts. Hot. | settleUsage, and nothing else. Bounded by escrow and a per-window cap. |
| Relayer keys (active) | Encrypted keystores inside the signer on the hosts that use them, never as plain files. Each active key is on more than one host, so delivery survives a lost machine. | Co-sign relayed transactions. Nothing else. |
| Relayer keys (spares) | Offline, in two places. On no server. | Nothing, until activated by the owner. |
| Reserve | Offline. On no server. | Receives swept relayer float; funds top-ups. |
The registry of relayer addresses is public data in this repository — 30 wallets, 10 per shard, 9 marked active, 21 cold spares. The addresses are public because the registry is on chain anyway; the keys are what is protected.
Relayer wallets never enable a guardian: the protocol does not permit a guarded relayer, and the contract refuses to register one.
A role address may not also be a relayer. The contract enforces the disjointness, and the signer refuses to start with a key bundle that violates it.
The bound on a compromised signing host
If a host holding active relayer keys were compromised, what is reachable is the relayer float: the small working balance each relayer holds to pay fees. That is CoRelayer's own working capital.
What is not reachable, because it is not there:
- customer tokens — we never hold any;
- credits and plans — they live in the contract, and no relayer key can touch them;
- the tariff, the tier table or the registry — owner-only;
- settlement beyond the reporter's bounded cap.
The response is to drain the affected relayers, activate spares from cold storage, and replace the wallets. That is why there are 21 spares and why their keys are on no server.
Environments are separated even though addresses are not
The owner, operator, reporter and relayer wallets have the same addresses on devnet and on mainnet. Transaction replay between networks is prevented by the chain id inside the signed bytes.
Everything else that is signed or authenticated is per environment:
| Per environment | Consequence |
|---|---|
| The lease key and the quote key | A devnet lease or quote never verifies on mainnet. |
| The sponsor-key hashing key | A crk_test_… key is refused by the mainnet API, and the reverse. |
| Webhook secrets, status-feed secrets, operational-flag keys | No cross-environment replay. |
| Payment identifiers | Separate databases. |
And a hard stop in the signer itself: it pins its chain id and refuses any transaction whose
chainID differs. It cross-checks that pin against the network at start-up and refuses to serve on
a mismatch. Since the chain id is inside the signed bytes, a signature made for one network is
worthless on the other.
The devnet hosts get full production key handling from the first deploy — encrypted keystores, signer isolation, no plain key file on any server. Not because devnet matters, but because those hosts hold the real keys.
Your API keys
crk_<env>_<keyId>_<secret>
| Shown | Once, at creation. We store a keyed hash and compare in constant time. We cannot show it again. |
| Scopes | relay — pay for arbitrary senders. read — private reads of the account. |
Required policy for relay | A non-empty receiver allow-list. Not optional. It is compared with the transaction's receiver field (what that covers). |
| Optional policy | Function allow-list, per-sender daily unit limit, account daily unit limit, IP allow-list. |
| Limit | 10 keys per account. |
| Managed by | Your wallet only. A key can never create, change or revoke a key. |
| Requires | A plan that can sponsor any sender (Builder and up, Agent Pro, Agent Fleet), else API_KEY_SCOPE. |
There are no browser-visible keys, and there will not be. Sender addresses are free to create, so a key visible in a page would let anyone burn the sponsor's whole cap within the allow-list. Named wallets (on-chain authorised senders) suit addresses whose key your own programs hold, such as bots, devices and scripts. A browser dApp whose users sign in their own wallet can't be relayed for yet. How a server uses a key: Pay for your users.
The mandatory receiver allow-list is what makes a leaked key survivable: the thief can spend your Relay Units, but only on transactions to receivers you named.
If a key leaks: revoke it (DELETE /v1/account/{erd}/keys/{keyId}), mint a new one, and check
GET /v1/usage for what was spent under the old one.
Signed transactions in logs
A user-signed relayed transaction that we never co-signed is inert — it cannot execute, by the protocol. But it is still a payload somebody else can hold, so:
- the MCP tools that take one never echo the transaction, the signature, the guardian signature or the lease in their results or their errors;
- their descriptions tell hosts not to log their arguments;
- the presence proof and first-seen expiry bound what a captured payload is worth.
The residual is not zero, and it is listed as a residual rather than explained away. (MCP)