Docs · Self-hosted runtime
  1. Getting started
  2. Building
  3. Iterating
  4. Memory
  5. Models and keys
  6. Gateway
  7. Self-hosted runtime
  8. Usage and billing
  9. Deploy a token
  10. FAQ
Self-hosted

Self-hosted runtime

Horai also ships as something you run yourself: an agent that holds its own wallets, signs its own transactions, and calls this platform for the parts it does not do locally. It installs as two systemd services on one Linux box.

This is a different product surface from the hosted app. Nothing here talks to your account in the browser — the connection between them is a gateway key you paste in, and the build tool that key unlocks.

Two processes, and why it is two

horai-signer    holds the keys, enforces the policy, signs
horai-agent     runs the model loop, the sandbox, the channels

They run as two separate OS accounts that cannot read each other's files, and they speak over a unix socket owned by a shared group. The split is the whole design:

  • The signer has no network. Its unit sets RestrictAddressFamilies=AF_UNIX, so the kernel refuses to give it a TCP socket at all. Even fully compromised, the process holding your keys has nowhere to send them.
  • The agent has no keys. Its unit sets ProtectHome=true, so it cannot read the signer's home directory — where the encrypted key material lives — and the filesystem permissions say the same thing a second time.
  • The agent cannot restart the signer. It depends on it and cannot control it.

The agent is the part that reads untrusted input: model output, web pages, messages from a channel. It is arranged so that being wrong there costs you an API bill rather than a wallet.

What the signer will and will not do

The socket exposes a fixed set of operations. Everything else is not disabled by permission — it does not exist in the process:

operations that exist               operations that do not exist, at any permission level
  getAddress                          exportKey
  createWallet                        exportPrivateKey
  signTransaction.transfer            getPrivateKey
  signTransaction.swap                revealMnemonic
  signTransaction.deployToken         dumpKeystore
  useTool.build                       signRaw
  useTool.read

signRaw is absent on purpose. An agent that can sign arbitrary bytes can sign anything, and every cap above it becomes decoration.

Key export exists, as a command a person runs on the box. It is not reachable from the agent, from any channel, or from any model tool — the module that performs it is not linked into the process that serves the socket. Run horai doctor on your own machine and it prints these lists, so you can check the claim rather than trust it.

Install

Two lines, as root, on a fresh Linux box:

curl -fsSO https://horai.sh/horai.tar.gz
mkdir -p /opt/horai && tar -xzf horai.tar.gz -C /opt/horai && bash /opt/horai/packaging/install.sh

Unpack into /opt/horai rather than wherever you are standing. The signer runs as its own account and every directory above the code has to be traversable by it — a root shell starts in /root, which is mode 0700. The installer checks this by having each account actually open a file, and refuses with the reason rather than leaving you a service that crash-loops while systemctl reports it active.

It creates the two accounts, the shared group, the key directory, the run directory and both units, then prints the next steps. It does not start anything.

First run

Set the key-encryption key and start the signer. The key that encrypts your wallet keys lives in a systemd drop-in, not in a file in the repository:

systemctl edit horai-signer.service
  [Service]
  Environment=HORAI_SIGNER_KEK=<32 random bytes, hex>

systemctl enable --now horai-signer.service

Then run the guided setup, which creates the seed, shows it once, and asks for your caps, your allowlist and your permissions:

sudo -u horai-signer HORAI_SIGNER_KEK=<your-kek> HOME=/home/horai-signer horai setup

The sudo -u prefix matters. The CLI opens the signer's database directly and that database lives in the signer's home — run it as root and you quietly configure a different one under /root, then enforce the other. Commands that only read the machine's layout, like doctor, do not need it.

horai doctor

doctor prints the accounts, the key directory and its mode, whether the socket exists, whether the signer answers, and the two operation lists above. On a fresh install it reports the socket as not found until you start the service, which is the correct answer rather than an error.

Giving it a model

The agent reads its inference keys from a drop-in on its own unit:

systemctl edit horai-agent.service
  [Service]
  Environment=HORAI_API_KEY=horai_your_gateway_key

That is a gateway key from this platform, and it gives the runtime the same router the hosted chat uses, billed to the same balance. Your own provider key works instead, and the two can coexist:

Environment=HORAI_ANTHROPIC_API_KEY=sk-ant-...

Never put HORAI_SIGNER_KEK in the agent's drop-in. A leaked inference key costs an API bill; the key-encryption key costs the wallet.

HORAI_API_BASE points the runtime at a different platform origin. It accepts https anywhere and http only on loopback, and a value it will not accept is refused rather than quietly ignored — falling back would send your key somewhere you did not choose.

Policy

The policy is what the signer enforces before it signs. You choose it during setup, and you read it back with:

sudo -u horai-signer HORAI_SIGNER_KEK=<your-kek> HOME=/home/horai-signer horai policy show

It prints in two groups, and the grouping is the point:

  • Caps enforced on the transaction itself. Native value, gas, and fees, read by the signer out of the bytes it is about to sign. These hold even if the agent is entirely compromised.
  • Caps enforced on a number the agent declares. Per-transaction and rolling daily USD. The signer has no price feed and cannot check them, so they protect you from an honest agent's mistakes and not from a dishonest one. Set the first group as your real limit.

Below those sit the permission switches — signing, transfers, swaps, token deploys, wallet creation, tool use — and a recipient allowlist. A fresh install has every cap at zero, every permission off, and an empty allowlist, so it can do nothing until you decide otherwise.

Two rules shape how it changes:

  • Until one transaction lands, caps may only go down. You cannot raise a limit you have never exercised.
  • horai policy lock freezes it. After that the running agent can no longer alter its own constraints.

The build tool

With a gateway key configured, the runtime can build and deploy an app the same way the hosted chat does. A sentence goes in, the job streams back, and the source archive lands in the working directory:

▸ proposal  e970dfbd-86b4-4e51-9eed-8f904eb174e7
▸ queued
▸ executing
▸ settled
▸ saved  splitbill-source.zip  (20796 bytes)
▸ live   https://apps.horai.sh/splitbill/

The tool is governed by the policy — it runs only when tool use is permitted, and the signer decides that over the socket, in the other process. It also cannot reach the signer at all: it carries fetch, a place to print, and nothing else. That is enforced by a test that walks its import graph and fails if the signer client is reachable through any path, because everything this tool touches — the sentence, the job's status strings, the URLs in the response — arrives from outside.

Cross-chain routes and refundTo

Routes that cross chains go through Relay, and every quote the runtime requests sets refundTo to an address on the origin chain.

It is required rather than optional here. Without it the route falls back to the recipient, which is an address on the destination chain — and a route that fails after the origin leg would refund to a chain the money never reached. The address kind is validated against the origin chain before the quote is requested, so a Solana address on an EVM origin is rejected at the call rather than discovered during a refund.