Documentation
Empra runs an on-chain agent on a machine we own. The agent's signing key is generated on that machine and never leaves it, every signature is checked against a policy that is hashed into the code measurement, and the rig publishes an hourly receipt chain that anyone can verify without asking us anything.
This page covers the whole thing: how a key is earned, what the rig actually guarantees, every route and command, and a plain statement of what is not true yet.
The attestation is currently simulated. The pipeline is real, every check runs against real signed evidence, and the code path is exercised on every test run. The root of trust is generated locally rather than by a chip, so it proves what code a rig claims to run, not that hardware protected the key. The limits section says exactly where that line falls.
Getting a key
A key is gated on holding 1,000 empra. The token is 0xd804bd7771066825c4c8869bae42d6ab19279ad3 on Robinhood Chain, chain id 4663.
- Open the dashboard and connect the wallet that holds the token.
- The control plane reads your balance from the chain. Nothing is taken from the browser, because the browser is what is being checked.
- Sign one message. It names your address, the token, the chain and a single-use nonce. It moves nothing and costs no gas.
- The key appears once. It is stored in that browser and nowhere else, and we only ever keep its hash.
A nonce is bound to one address, expires after ten minutes, and cannot be replayed. One wallet cannot claim with another wallet's nonce. Claiming again revokes the previous key, because we cannot show a key twice.
A new holder is credited two hours of compute so the key is worth something the moment it exists.
Reserving a rig reads the holding again. Compute stays with the tokens, so a given 1,000 can only run one rig at a time, and selling them stops the rig you have.
Losing a key
There is no recovery, because there is nothing to recover from: we hold a hash. Claim again with the same wallet and the old key is revoked in the same transaction that mints the new one.
Quickstart
With a key in $KEY and the control plane at $EMPRA:
# what you have
curl -H "Authorization: Bearer $KEY" $EMPRA/api/account
# reserve two hours. the token in the reply is how the rig reports back,
# and it is shown once
TOKEN=$(curl -s -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"hours":2,"id":"my-rig"}' $EMPRA/api/rigs | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
# boot it
curl -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d "{\"token\":\"$TOKEN\"}" $EMPRA/api/rigs/my-rig/boot
# check it yourself, holding nothing but its name
node cli/empra.mjs verify my-rig --url $EMPRA
The rig generates its key, reports an attestation, and starts signing. Everything after that is receipts.
The measurement
The measurement is a hash of the code the rig booted plus the configuration that changes how it behaves. It is what the site means by "compare it to the published build".
manifest = [{path, sha256}] over rig/**, sorted by path
manifestHash = sha256(canonicalJson(manifest))
bootConfig = { chainId, inference, policyHash, runtimeVersion }
measurement = sha256(canonicalJson({ v: 1, manifestHash, bootConfig }))
Recompute it from any checkout:
node cli/empra.mjs build-hash --chain 4663 --inference local:qwen2.5:3b
Two rigs running the same code with the same policy and configuration measure identically. The rig's own name is deliberately outside the hash, because a name is identity, not behaviour, and a measurement that changed per rig could never be compared to anything.
This matters more than it first appears. A confidential VM's own launch measurement covers firmware and bootloader, not your application, so on its own it says "a VM booted" rather than "this code is running". The application-level measurement is the part that identifies the code, with or without hardware underneath it.
The signer and its policy
The rig is two processes. The signer owns the key. The agent is the brain and can only ask.
Three properties
- The key is born inside. Generated in the signer process on first boot, written to disk only sealed, and there is no method that exports it.
- There is no generic "sign this". Every method builds the payload it signs, so the agent cannot borrow the key for something else. The whole surface is
address,attest,signTx,signReceipt,log,stats. - It needs no network. Transactions are built outside, arrive fully formed, get checked, and go back signed. The agent broadcasts them. The signer never opens a socket to the internet.
The policy
rig/policy.json is hashed into the measurement, so the rules cannot change without producing a different attestation.
{
"maxValuePerTx": "0.05",
"maxValuePerDay": "0.5",
"allowTo": [],
"allowSelectors": [],
"denySelectors": ["0x095ea7b3"],
"maxGasPriceGwei": "50",
"rateLimitPerHour": 20
}
The chain id comes from the boot config rather than the policy file, so the two can never disagree. An empty allowTo or allowSelectors means no restriction on that axis; a non-empty one is an allowlist.
Refusals are the point
Every decision, allowed or refused, is appended to a hash-chained log. A refusal is not an error path, it is the evidence that the boundary exists, and it is counted in the receipt for that hour. Ask the signer to send more than the policy allows and it will tell you which rule stopped it and what to do about it.
Attestation
An attestation is a document plus evidence. The document is what the rig claims. The evidence is what makes it worth believing.
{
"v": 1,
"rig": "my-rig",
"measurement": "4e57681f…",
"manifestHash": "…",
"bootConfig": { "chainId": 4663, "inference": "local:qwen2.5:3b", … },
"address": "0xC3d3…1DA6",
"pubKeyHash": "…",
"nonce": "…",
"bootedAt": 0,
"attester": { "kind": "simulated", "note": "…" }
}
The document is signed by the key it names, which proves the rig holds it. The evidence binds that key to that measurement.
Why it is shaped like this
Real hardware attestation puts hash(publicKey) into the 64-byte report data of an SEV-SNP report or a TDX quote, and the silicon signs a statement saying this measured software, on genuine attested hardware, is presenting this public key. The document above is built to that shape from day one, so moving to hardware is a swap rather than a redesign.
The simulated attester
It signs structurally valid evidence under a generated test root. It is not a stub that returns true. Parsing, chain walking, nonce freshness and measurement comparison all run against it, and the test suite requires it to fail when the measurement is altered, when a nonce is replayed, or when the platform key was not endorsed by the root. Going live swaps the root of trust, not the code.
Receipts
Every hour the rig signs a statement of what it did and posts it.
{
"v": 1, "rig": "my-rig", "seq": 41,
"prev": "<sha256 of receipt 40>",
"measurement": "…", "address": "0x…",
"from": 0, "to": 0,
"gpuSeconds": 3412, "uptimeS": 0,
"txs": ["0x…"], "signed": 12, "refused": 1,
"bytesIn": 0, "bytesOut": 0, "inference": "local:qwen2.5:3b"
}
prev plus a contiguous seq is what makes a missing hour visible: delete or edit one and the walk breaks. The control plane checks the signature and the chaining before it stores a receipt, so it never publishes one it has not verified, and it bills once per receipt sequence rather than once per wall-clock hour, because a rig legitimately emits more than one receipt in an hour on shutdown or after a restart.
gpuSeconds is time actually spent inside inference calls on a machine with a GPU. On a rig without one it is zero, and it says zero rather than estimating.
Verifying a rig
Five checks. Run them yourself:
node cli/empra.mjs verify my-rig --url https://your-control-plane
measurement matches published build ok 4e57681fb784e0cd
attestation signed by the attested key ok 0xC3d3…1DA6
attestation evidence verifies ok simulated root
41 receipts, chain intact ok
12 txs confirmed from attested address ok
- Recompute the measurement from the published build and compare it to what the rig claims.
- Check the attestation document is signed by the key it names.
- Verify the evidence binds that key to that measurement, for whichever attester produced it.
- Verify every receipt signature and walk
prevandseqfor continuity. - Ask the chain whether each transaction exists and came from the attested address.
The fifth needs no trust in us at all: the chain independently proves the attested key signed those transactions.
The dashboard shows the same five checks, but it runs them on our server. That makes it a convenience, not the authority, and it says so on the page and prints the command above. If the two ever disagree, the command is right.
HTTP API
Three ways to authenticate. A bearer key for customers, a rig token for a rig reporting about itself, and an admin token for the operator. Errors are always { "error": { "code", "message" } }.
Open
GET /api/config | Chain, price, attestation mode, whether rig hardware is attached, the gate, the wallet project id. |
GET /api/gate/check?address= | What that address holds, what is required, whether it passes. Read live from the chain. |
POST /api/gate/nonce | {address}. Returns a nonce and the exact message to sign. |
POST /api/gate/claim | {address, nonce, signature}. Verifies the signature, reads the balance, issues a key once. |
POST /api/reserve | {email}. The waiting list on the home page. |
GET /api/rigs/:id | Status, address, measurement, attestation mode, receipts and GPU-seconds billed. |
GET /api/rigs/:id/attestation | The document and its evidence. Public because it is signed. |
GET /api/rigs/:id/receipts | The whole chain. Public for the same reason. |
GET /api/rigs/:id/verify | Runs the five checks on our server. A convenience, and it says so. |
GET /api/bundle.tar | The exact rig code the measurement covers, which is how a rented machine boots. |
With a key
GET /api/account | Balance in seconds and hours, and every rig you own. |
POST /api/rigs | {hours, id?}. Reserves atomically, debits the balance, returns a one-time rig token. |
POST /api/rigs/:id/boot | {token}. Starts the rig on our hardware. |
POST /api/rigs/:id/stop | Stops it and refunds whatever was never billed. |
As a rig
POST /api/rigs/:id/attestation | Reported once at boot. Rejected unless the document is signed by the key it names. |
POST /api/rigs/:id/receipts | One signed receipt. Rejected if the signature, address, measurement, sequence or chaining is wrong. |
As the operator
POST /admin/keys | {owner} with x-admin-token. Mints a key without the gate. |
POST /admin/credit | {owner, seconds}. Adds compute to an account. |
CLI
Run from a checkout. The package is private, so there is nothing on the registry to npx.
node cli/empra.mjs build-hash | Recompute the measurement from this checkout. --write saves build.json. |
node cli/empra.mjs verify <id> --url <cp> | The five checks, against a rig you do not host. This is the authority. |
node cli/empra.mjs receipts <id> | The receipt chain as a table. |
node cli/empra.mjs keygen | An X25519 keypair. Snapshots are encrypted to the public half. |
node cli/empra.mjs snapshot <id> --to <pub> | Seal the rig's memory to a key only you hold. |
node cli/empra.mjs boot <id> | Operator only: bring a rig up by hand on this host. |
MCP tools
For running Empra from inside another agent. Stdio transport, a thin client over the same API, so every rule is enforced in one place.
claude mcp add empra -e EMPRA_KEY=ek_live_… -e EMPRA_URL=https://your-control-plane \
-- node /path/to/empra/mcp/index.mjs
empra_account | Compute left and the rigs it owns. Call before reserving anything. |
empra_reserve_rig | Reserve hours. Returns the rig id and its one-time token. |
empra_boot_rig | Start a reserved rig with that token. |
empra_rig | Status, address, measurement, what it has billed. |
empra_receipts | The receipt chain, which is also the bill. |
empra_attestation | The document and evidence, to check before funding an address. |
empra_stop_rig | Stop and refund. |
The rig runtime
A tick is a short tool loop, not one answer, so the agent can look before it acts. Up to four steps, then it stops.
balance | What it holds, read from the chain. |
recall {query} | Search its own memory. |
remember {text} | Write one note. |
transfer {to, eth} | Ask the signer to send. May be refused, and a refusal is information. |
done | End the turn. |
A tool that throws hands the failure back as a result rather than ending the turn.
Inference stays on the rig
The agent talks to a model on 127.0.0.1: ollama in development, vLLM on a GPU machine. The endpoint and model are named in the boot config and therefore in the measurement. Nothing it thinks crosses the network.
What the host can see
The rig writes operational events to standard output, which the host captures: it booted, it reported, it posted receipt 12, a tick failed. What the agent decides goes to its own memory instead, and the test suite fails if a decision ever appears in the host log.
Memory and snapshots
Memory is a SQLite file inside the rig's state directory. The snapshot command seals it to an X25519 public key you generated with keygen, using key agreement, HKDF and AES-256-GCM from the standard library. We cannot open it.
The contract
EmpraReceipts.sol gives a receipt chain an immutable timestamp. A rig registers itself with the measurement it booted, then anchors the hash of each receipt in strict sequence.
register(bytes32 rigId, bytes32 measurement)
anchor(bytes32 rigId, uint64 seq, bytes32 receiptHash)
close(bytes32 rigId)
anchored(bytes32 rigId) → uint64
The rig's own key is the only thing that can anchor for it, so the address in an attestation is the address that appears on chain. Skipping or replaying a sequence number reverts rather than quietly landing. A retired rig can never anchor again.
It holds no funds, deliberately. A rig that stops posting must not be able to strand anyone's money, so the prepaid balance lives in the control plane where it can be refunded.
Self-hosting
npm install
EMPRA_SEAL=<16+ random chars> ADMIN_TOKEN=<token> npm start
EMPRA_SEAL | Seals rig keys at rest. Never change it: rotating it orphans every rig that exists. |
EMPRA_PROVISIONER | local runs rigs as processes on this host, runpod rents them, none refuses honestly. |
EMPRA_ATTESTER | simulated, or dstack inside a confidential VM. |
REOWN_PROJECT_ID | Set this before deploying. The default is a shared id that only works from localhost, and without your own the dashboard falls back to whatever wallet the browser already has. |
GATE_TOKEN, GATE_AMOUNT | What must be held, and how much. |
RUNPOD_API_KEY, PUBLIC_URL | Needed to rent machines. The pod fetches the bundle, so the URL has to be reachable. |
DB_PATH, EMPRA_STATE_ROOT | Put both on a persistent disk. |
One instance only: the ledger is SQLite on a disk and the provisioner holds state in process, so a second instance would split both.
A web host has no GPU, no local model and no confidential mode, so a deployment there runs with the provisioner set to none, which is also what it defaults to on Render. In that mode reserving returns a clear refusal before taking any balance, and the dashboard disables the button. The gate, the key and the balance all work; only booting a rig needs a machine attached.
Limits, said plainly
| Measured boot | Enforced. Recompute it yourself with one command. |
| Key custody | Enforced in software. Born in the signer, sealed at rest, no method returns it. A root operator on the host could still read it today. |
| Policy boundary | Enforced. Every signature checked, refusals recorded in a hash-chained log. |
| Receipts | Enforced. Chained, signed, verified before storage, anchorable on chain. |
| Snapshots | Enforced. Sealed to a key we do not have. |
| Hardware root of trust | Not yet. The attester is simulated. The evidence is really signed, but under a locally generated root, so the chain does not terminate at an Intel or AMD certificate. |
| Renting real machines | Written, never run. The RunPod path is built to their documented API and has not been executed against it. |
Two things that stay true even with hardware
GPU memory is not encrypted. Confidential mode gives an access-controlled compute region fenced by PCIe and NVLink firewalls, plus an encrypted tunnel over PCIe. That is a real protection and it is not the same as encrypted VRAM.
We can always turn the rig off. Denial of service is outside every attestation's threat model, including ours. Nothing on this page fixes that, and the FAQ says so.