Custody
Your wallet signs every treasury transaction and Elpis holds no key that can move funds. Self-custody is the only mode on mainnet; managed keys exist only for local devnet sandboxes.
Self-custody is the only mode
Your own wallet signs every treasury transaction. Elpis holds no operator key, cannot move funds on its own, and — if it ever tried to sign server-side — the code fails loudly rather than quietly doing so.
Concretely, the operator address on the vault is yours. Elpis builds the transaction, surfaces it, and waits for your signature; nothing executes until you sign. It also verifies what you broadcast: each transaction is read back and matched against the one that was prepared, so a signature cannot be recorded as a payment it was not.
Managed mode is refused on mainnet
A mode where a key Elpis controls signs on your behalf is not offered on a real network: it would make Elpis the custodian of your money — a different business, on a different legal footing, and the opposite of the guarantee this product is built on.
On a local devnet it exists only for email-created trials and sandboxes, where the assets have no value. This is enforced in code, not policy: provisioning a MANAGED treasury on a mainnet chain is refused outright, before a row is written.
| Self-custody | |
|---|---|
| Signs treasury transactions | Your wallet |
| Can Elpis move funds on its own | No |
| What Elpis still holds | A deploy key with no authority after deployment, and a guardian that can halt a payment and return it to you — never take control of it |
| What the server verifies | That each broadcast transaction is the one it prepared |
| Managed on a real network | Refused by construction |
An external signer (KMS/HSM)
For a deployment that holds an operator key at all, the key need not be a plain secret. Set SIGNER_URL and SIGNER_ADDRESS and the operator account signs through a KMS/HSM: the unsigned transaction is serialised, hashed, and the hash is signed by the service — the only shape a hardware signer accepts. The private key never exists in the deployment.
The Arc adapter refuses to start with a signer URL but no address, and GET /api/health reports signing.kind — self-custody, external-signer, or hot-key — so the posture is visible rather than inferred. The protocol a signer service implements is in src/chain/evm/reference-signer.ts.
TREASURY_SIGNER_PRIVATE_KEY is a hot key; the on-chain allowlist and ceiling bound what a leak can do, but do not prevent it.Deploy your vault
On a real network the vault contract is deployed by you, not by Elpis. Sending that deployment is Elpis holding an operator's power, so it does not send it — which makes this the first step of using a real treasury, and the only one that is manual.
The contract is contracts/LazarusVault.sol in the repository. It takes four constructor arguments, in this order:
| # | Argument | What to pass |
|---|---|---|
| 1 | token | The settlement token — USDC on that network. |
| 2 | operator | Your own wallet address. This is the account the vault obeys. |
| 3 | guardian | A second address that can halt a payment. Use one you control, ideally a different key. |
| 4 | guardianMayRecoverOperator | false. True would let the guardian take control of the funds, which is custody by another name. |
forge create contracts/LazarusVault.sol:LazarusVault \
--rpc-url $RPC_URL \
--private-key $OWNER_KEY \
--constructor-args $USDC <your-wallet-address> $GUARDIAN falseThen attach it, in Settings → Your vault, which shows the same command with your treasury's own token and wallet already filled in. Elpis does not take your word for it, and it does not rely on operator() alone — any contract can answer a view function with any address. It reads the deployed code back and compares it to the build this repository compiles, excluding the immutable values and compiler metadata that legitimately differ per deployment. A contract that is not that exact build is refused, as is one whose guardian could install a new operator.
cast code <vault-address> --rpc-url $RPC_URL
cast call <vault-address> "operator()(address)" --rpc-url $RPC_URL
cast call <vault-address> "guardianMayRecoverOperator()(bool)" --rpc-url $RPC_URLHow the keys relate
- Each organization gets its own operator and guardian addresses — there is no shared treasury address across tenants.
- In managed mode, which exists only on a local devnet, those keys are derived per organization from a single master secret, so one organization’s key never touches another’s.
- In self-custody, the operator is your address, and the derived operator key is never used to sign.
- The guardian is a brake: it can pause the vault. It can never move money to itself, and it cannot undo a pause the way the operator could.
Self-custody is not self-host
These are different things, and it is worth keeping them apart: self-custody is about who holds the key; self-host is about who runs the software. You can self-custody on the hosted service, and self-hosting is a separate deployment choice.
What Elpis cannot do
- It cannot sign as you in self-custody mode — there is no key to sign with.
- It cannot release an escrow early, or take a staged payment for itself.
- It cannot unpause a vault the guardian paused, unless a guardian says so.
- It cannot make a blocked action proceed; a blocking rule is not advisory.