Skip to article
Browse documentation +
USING ELPIS / ELPIS

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.

On mainnet a treasury cannot be created without an owner wallet. An email-created trial is a local devnet path only; provisioning one against a mainnet chain is refused before a row is written.

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 transactionsYour wallet
Can Elpis move funds on its ownNo
What Elpis still holdsA 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 verifiesThat each broadcast transaction is the one it prepared
Managed on a real networkRefused 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.

This is opt-in. Without it, 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:

#ArgumentWhat to pass
1tokenThe settlement token — USDC on that network.
2operatorYour own wallet address. This is the account the vault obeys.
3guardianA second address that can halt a payment. Use one you control, ideally a different key.
4guardianMayRecoverOperatorfalse. True would let the guardian take control of the funds, which is custody by another name.
with Foundry
forge create contracts/LazarusVault.sol:LazarusVault \
  --rpc-url $RPC_URL \
  --private-key $OWNER_KEY \
  --constructor-args $USDC <your-wallet-address> $GUARDIAN false

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

check the code yourself
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_URL
Code identity proves the contract is this build. It does not prove the build is free of bugs — the contracts have not been independently audited. The code hash recorded at attach time is shown in Settings so you can compare it against your own deployment.
The vault holds money only while a payment is staged inside it. Your treasury address is your own wallet, so funding is ordinary USDC in your own wallet. Start with a small settlement and raise the ceiling deliberately.

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

A self-host deployment is never required to use Elpis, and it is not the default. It is an option for teams who want the control plane inside their own perimeter.

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.