---
slug: 73-vela
title: Vela (Horizen)
description: Synsema as a Vela app — Horizen's confidential coprocessor runs your .syn inside a TEE through a thin adapter, with the contract of the program (deploy/deposit/process/deanonymize/trusted), a client in Synsema for the other side of the enclave (register, deploy, encrypted requests, reports, events, facilitator), trigger contracts, ERC-20, and the local starter kit recipe. Everything verified against the real stack.
example_ids: []
---

# Vela (Horizen)

[Vela](https://docs.horizen.io/vela/introduction/) is Horizen's **confidential coprocessor**: an
application runs as a WebAssembly module inside a TEE (an AWS Nitro Enclave in production, an
isolated process in the local kit), its state travels encrypted, and every result is settled
on-chain as a signed state root. The official toolchain is Go + TinyGo. Since the guests package,
**the application can be a `.syn` file**, and the side outside the enclave — keys, deploys,
encrypted requests, events, reports, meta-transactions — can be Synsema too.

This page is the whole picture. The repository directory is the contract:
[`packages/guests/vela`](https://github.com/kitecosmic/synsema/tree/main/packages/guests/vela)
(README, adapter, three example apps, a client, a trigger contract, an ERC-20, two probes).
Nothing here is a guess: the ABI was read from Horizen's Go code (`vela`, `vela-common-go`,
`vela-nova`, `vela-starterkit`, v0.2.0) and every flow below ran against the starter kit in Docker.

**Start from the kit.** [`synsema/vela-app`](https://github.com/synsema/vela-app) is a template repository: the app with its tests, the client, `scripts/build.sh` (your program into the release's guest module: no compiler, a few seconds), `scripts/smoke.mjs`, `scripts/devnet.sh` (Horizen's starter kit in Docker) and `scripts/e2e.sh` (keys → deploy → register → deposit → an encrypted request → your events), plus a CI workflow that builds `app.wasm` on every push. "Use this template", then the four commands in its README. No Docker? `synsema run vela_client.syn -- devnet` writes a token of your own on the public devnet (below) into `client/.env`.

**A complete app to fork.** [`synsema/vela-payroll`](https://github.com/synsema/vela-payroll) is private payroll on Vela, built on the kit: an employer funds the app with a stablecoin and runs payroll from a CSV; each person sees only their own payslips; the chain sees deposits, withdrawals and one public receipt per run (run number, head count, a hash of the items); people withdraw as pull-payments and need no ETH, because the employer's facilitator submits for them; an allowed auditor gets the plain picture from the enclave. Its client adds `fund`, `payrun <csv>`, `payslips`, `withdraw`, `pending` and `claim-for`, with amounts in tokens converted by text with the token's `decimals()`; it ships the test ERC-20 with `permit` that its `scripts/devnet.sh` deploys and allowlists locally; and its `scripts/e2e.sh` runs the whole cycle. Verified end to end on the public devnet.

**Policy inside, LLM outside.** [`synsema/vela-treasury`](https://github.com/synsema/vela-treasury) is an agent treasury: an agent (any process holding a proposer key — a worker on the Synsema platform, a laptop) proposes payments; the enclave applies the owner's policy — who may propose, who may be paid and up to how much, the automatic limit per payment, the allowance the owner granted — and either pays at once through a trigger contract (`TreasuryTrigger.sol`, one per app, ERC-20 or ETH) or holds the proposal with every reason for the owner's `approve` / `reject`; the trigger's answer settles the payment or refunds it. The standalone agent worker (`agent.syn`) turns the invoices in `inbox/` into proposals — JSON as is, free text through an LLM when a provider is configured — and holds no funds: the worst a bug or a prompt injection can do is a proposal the policy stops. The client protocol is a module (`client/vela_lib.syn`) both the worker and the CLI `use`. Verified end to end on the public devnet, trigger cycle included.

**Bids nobody sees.** [`synsema/vela-auction`](https://github.com/synsema/vela-auction) is a sealed-bid auction: a seller offers a block of one token for another; bidders deposit the payment token and send their bids encrypted to the enclave — nobody, not even the seller, sees a bid before the close; the matching runs inside (uniform price or pay-as-bid, bids ranked by cross-multiplication of `(quantity, total)` pairs, exact integers, a long division of its own because `/` goes through a float); winners and losers are settled from their escrow as pull-payments, each bidder learns only its own result and the clearing price, the losing bids are never revealed, and the chain sees `opened` and `cleared` receipts with no bidder in them. Three parties, one auction, two sealed bids, claims checked on-chain: `scripts/e2e.sh`, verified on the public devnet.

**Each kit is a web app.** The recipe's entry in all four is a console (`web.syn`, `kind = web`): the payroll console (fund, onboard people, pay runs, each person's payslips and payouts), the treasury console (deploy with a trigger made through a factory contract on the stack, policy, payees, the agent's proposals, approve or reject, receipts), the auction console (the seller's desk and a desk per bidder), and the starter kit's workbench (deploy your `app.syn`, register, deposit, any payload encrypted to the Executor, your events decrypted, users through the facilitator, reports). Every action is one request to the enclave; the console's state lives on the project's volume. Deployed from [synsema.com](https://synsema.com), a project's environment is provisioned from the public devnet at creation (`[provision] env_url` in `syn.toml`: a token of your own, the addresses, the keys); locally, `synsema serve web.syn` with the same lines in `.env`. The consoles custody keys for the accounts they make (a person a portal holds keys for, a demo bidder, a demo agent): the library signs as any key it holds (`send_tx_as`, `submit_as`) or submits for it through the facilitator (`submit_for_with`); an account with a wallet of its own uses the command-line client. All four verified end to end on the public devnet from the browser.

## Where it sits: a guest, not a new artifact

Vela does not load a WASI command (no `_start`, no argv) and does not provide the `synsema_host`
imports of the [embeddable artifact](/en/0.6.x/72-wasm): its Executor instantiates a module once
and calls **its own exports** — `allocate`, `deallocate`, `load_module`, `deploy`, `deposit`,
`process_request`, `trusted_request` — with pointers into the module's memory. So the engine stays
as it is, and a **thin adapter** in `packages/guests/vela/` maps those exports onto the generic
`synsema_call` ABI, with your program embedded: **one `.wasm` is exactly one app**, and the SHA-256
Vela verifies on-chain covers the interpreter and your logic together.

That is the *host axis* of the two-axis rule. On the *client axis*, what Vela needed and was
generic — WebCrypto primitives (`ecdh_*`, `hkdf_sha256`, `aes_gcm_*`), `--deterministic`,
`steps()` — entered the language under generic names. No company's name is in the engine.

## Your program → a module (no compiler)

The module is the interpreter plus your program. Every Synsema release publishes the interpreter
as a Vela guest — `synsema-vela-guest.wasm`, an asset of the release — with an **app slot** inside:
a fixed block of data with a header. `embed.syn` (in the kit as `scripts/embed.syn`, in the
repository as `packages/guests/vela/tools/embed.syn`) finds the slot in the file and overwrites it
with your `.syn`. The result is a module that is exactly your app, and the SHA-256 Vela verifies
on-chain covers interpreter and program together. Programs up to 512 KB; the embed takes a second.

```sh
synsema test app/app.syn                                   # the app, natively — the same code runs in the enclave
sh scripts/build.sh                                        # downloads the release's guest once, embeds app/app.syn → build/app.wasm (+ .sha256)
synsema run scripts/embed.syn -- synsema-vela-guest.wasm app/app.syn build/app.wasm   # the same, by hand
node scripts/smoke.mjs build/app.wasm                      # optional: Node 20 or 24+ (not 22, see below) — imports, exports, load_module, deploy, determinism
```

The guest always targets `wasm32-wasip1`: Vela's linker defines WASI and nothing else, so `print`
becomes the Executor's log (`INF …`). Vela's upload limit is 50 MB; the released guest carries the
example ledger, so it can be deployed as it comes.

**The adapter itself** — Rust, only if you change the adapter (`packages/guests/vela`):
`rustup target add wasm32-wasip1` and `cargo build --profile wasm` (→ `engine/target/wasm32-wasip1/wasm/synsema_vela_guest.wasm`;
`SYNSEMA_VELA_APP=… cargo build` fills the slot at build time), `cargo test --target <your host triple>` for its
unit tests, `node tests/vela_guest.probe.mjs <the .wasm>` and `cd tests/wasmtime-go && go run . <the .wasm>` for the two
probes (Node's WASI, and wasmtime-go v1.0.0 — the Executor's exact runtime).

Run the Node probe on Node 20 or Node 24+, not 22: Node 22.x crashes intermittently inside V8
(a segmentation fault from the concurrent tier-up race) while executing this module — reproduced
with 22.23.2 on Linux, one run in three; `node --no-wasm-dynamic-tiering …` avoids it. The bug
is in that host, not in the guest: wasmtime-go v1.0.0, the Executor's runtime, is unaffected,
and CI runs both probes.

## The contract of your `.syn`

The adapter calls **one task per Vela entry point**; each receives **one map** and returns **one
map**. Tasks you don't define fall back as noted.

| Vela export | task | `ctx` the task receives | map it returns |
|---|---|---|---|
| `deploy(appId, params)` | `deploy(ctx)` | `{app_id, kind: "deploy", params}` — `params` is the constructor JSON already decoded (`nothing` when empty) | `{state, fuel?}` — `state` required |
| `load_module(appId)` | `load_module(ctx)` → falls back to `deploy` with `params: nothing` | `{app_id, kind: "load_module", params: nothing}` | `{state, fuel?}` — the Executor calls this only to warm its module cache after a restart and **discards the state**; if the fallback `deploy` fails (it needed params) the adapter answers an empty state with a warning rather than an error, which would leave the app unloadable |
| `deposit(appId, sender, token, value, state)` | `deposit(ctx)` | `{app_id, kind: "deposit", sender, token, value, value_hex, state}` — addresses `0x` + 40 hex, lowercase; `value` as **exact decimal text**, `value_hex` as `0x…`; `state` decoded from JSON (text if it isn't JSON) | `{state?, events?, app_events?, fuel?, error?}` |
| `process_request(…, requestType = 1)` | `process(ctx)` | `{app_id, kind: "process", request_type: 1, sender, payload, payload_hex, state}` — `payload` decoded from JSON, or text; `payload_hex` the raw bytes as `0x…` | `{state?, events?, app_events?, withdrawals?, fuel?, error?}` |
| `process_request(…, requestType = 2)` (deanonymization) | `deanonymize(ctx)` → falls back to `process` | same, `kind: "deanonymize"`, `request_type: 2` | `{report, state?, fuel?}` — `report` **required** (Vela refuses a type-2 result without it; on any other type a report is dropped with a warning) |
| `trusted_request(appId, payload, state)` (TRUSTPROCESS from a trigger contract) | `trusted(ctx)` → falls back to `process` | `{app_id, kind: "trusted", request_type: 4, sender: nothing, payload, payload_hex, state}` — the payload is what the trigger's `getTrustProcessPayload` returned: **ABI bytes, in clear**; read `payload_hex` and `abi_decode` it | like `process`, and emit **no `app_events`** (an app event fires the trigger again; an empty one ends the loop) |

Two request types never reach the program: `AssociateKey` (3, a user registering a P-521 key and
seed — the Executor handles it) and a `PROCESS` with an **empty payload** (the Executor returns the
state untouched without calling the module; a deposit on that request still runs `deposit`). A
deposit and a process from the same on-chain request run back to back, the second on the state
the first returned.

### Shapes inside the returned map

- `state` — any value; a text is sent as-is, anything else as compact JSON in the key order your
  program built (deterministic). Omitted (or `nothing`) on `deposit`/`process`: the state stays as
  it came, byte for byte.
- `events` — `[{user: "0x…", subtype?, data?}]`, encrypted for `user` by the Executor. **Every
  `user` must have registered a P-521 key** (`novaw registeruser`, or the client's `register`): an
  event for an unregistered address makes the Executor fail the whole request
  (`CodePubKeyNotRegistered`, code 9) — don't emit to a recipient you can't vouch for. A missing
  `subtype` is 32 zero bytes (for a user who registered a seed the Executor overwrites it with an
  HMAC of the seed anyway).
- `app_events` — the same without `user`: public, unencrypted, indexed by the subgraph and handed
  to a trigger contract during `stateUpdate`.
- `subtype` — `"0x"` + 64 hex (32 bytes), or a **short label** of at most 32 bytes
  (`"execute_requested"`) that lands left-aligned and zero-padded — the starter kit's
  `subtypeToBytes32` convention, what a trigger contract compares against.
- **Bytes fields** (`state`, `report`, an event's `data`) come in three forms, first one found wins:
  `<field>_hex` (`"0x…"`, exact bytes — what a contract `abi.decode`s; build them with `abi_encode`
  and `decode(b, "hex")`), `<field>_base64`, or `<field>` (a text as-is, a map or list as compact
  JSON). A Synsema `bytes` value inside `data` would be JSON-encoded as base64 text — use
  `data_hex` when the bytes matter.
- `withdrawals` — `[{token?: "0x…", to: "0x…", amount}]`; `token` defaults to the zero address
  (ETH). Pull-payment: the recipient later calls `claim(token, payee)` on the ProcessorEndpoint. In
  a trigger app, `to` is the trigger contract and the matching app event carries the call.
- `amount`, `fuel`, `value` — integers as **decimal text** (`text(n)`) to stay exact at 256 bits; a
  JSON integer or a `0x…` text is accepted too. The adapter renders them as Vela's `Uint256` hex.
- `error` — a non-empty text makes the request fail with that message (state and events are not
  applied). A runtime error in the program does the same, with the engine's message. Never a trap.
- `fuel` — what you declare, or, if absent, the interpreter's `steps()` for that call: a
  deterministic count of executed statements. Vela charges `fuel × EXECUTOR_FUEL_PRICE_PER_UNIT`
  (at least `MIN_FEE_PER_REQUEST`, 10 wei in the starter kit) against the request's `maxFeeValue`
  and **fails the request if it doesn't cover it** — with `steps()` a handler is typically a few
  hundred units and `novaw` reserves 100 wei by default, so declare a constant like the reference
  app (`5`/`20`/`35`/`50`) or tell your users to reserve more.

`print` inside the program goes to the Executor's log; it is not a data channel. The result travels
only through the returned map.

### Determinism, by construction

The program runs under the ceiling `stdout`: **no `now()`, no `random()`/`token()`, no network, no
files, no LLM**. Vela signs the state root, so the same inputs must produce the same bytes — the
ceiling makes that a property of the runtime, not of discipline (the Go reference app stamps
`time.Now()` on every transaction; here that line cannot compile in). Maps keep insertion order
and JSON is emitted in that order, so the state bytes are the same across runs and across
instances (verified: five processes, identical hashes). Everything pure is available: types, JSON,
`decimal` (exact, 28 digits), `bytes_to_int`/`int_to_bytes` (exact 256-bit integers), `keccak256`,
`sha256`, `abi_encode`/`abi_decode`, `match`, `try`/`recover`, and `test` blocks that run natively.

Two things to know about numbers. A `decimal` or big integer is written as a bare JSON number, and
a bare number above 2⁵³ comes back as a float when the state is decoded on the next call — keep
amounts as **text** in the state (`text(n)`, or the Uint256 hex the payment app uses) and convert
on use. And `bytes(h, "hex")` takes no `0x` prefix and needs an even number of digits, while
Vela's `Uint256` hex strips leading zeros — every app writes the same two helpers (see
`hex_to_int`/`int_to_hex` in `examples/payment_app.syn`).

## The example apps

| Program | What it shows | Tests |
|---|---|---|
| `app.syn` | A private ledger: balances per account and token, transfers, withdrawals with a public **ABI receipt** (`app_events` + `data_hex` + a label subtype), a deanonymization report, a `trusted` task decoding `(address,address,uint256)` from `payload_hex`. The Node probe drives it. | 8 |
| `examples/payment_app.syn` | Horizen's own Private Transfer app (`vela-nova`'s `payment_app`) in Synsema, **wire-compatible with the `novaw` wallet**: the same payload instructions, event bodies (`balance` in Uint256 hex), `allowedTokens` deploy params, `balances`/`tx_history` reports, keccak invoice receipts — `novaw deployapp / registeruser / deposit / privatetransfer / withdraw / getprivatebalance / requestreport` drive it unchanged. No timestamps (no clock in the enclave): `tx_history` filters by address. | 9 |
| `examples/trigger_app.syn` | An execution pool for the trigger cycle (below): `execute` locks funds, withdraws to the trigger and emits `execute_requested` with abi.encode`(bytes16,address,uint256,bytes)`; `trusted` decodes `(bytes16,uint256,uint8)`, credits the remainder, clears the lock and emits no app event. | 6 |

## The client: the other side of the enclave, in Synsema

`novaw` only speaks the payment app's payload; every other app needs its own client.
`examples/client/vela_client.syn` does everything a client can do, natively, with the builtins
that arrived in v0.6.20 — no Go, no TypeScript. Copy `.env.example` to `.env`: `VELA_RPC_URL`,
`VELA_PROCESSOR`, `VELA_TEE_AUTHENTICATOR`, `VELA_AUTHORITY_URL`, `VELA_SUBGRAPH_URL`,
`VELA_APP_ID`, `VELA_MAX_FEE` (wei), `VELA_SECP_KEY` (signs transactions, pays), `VELA_P521_KEY` /
`VELA_P521_PUB` (from `keys`), `VELA_USER_KEY` (facilitator commands only).

| `synsema run vela_client.syn -- …` | What it does |
|---|---|
| `keys` | a fresh P-521 pair, printed for `.env` (`ecdh_keypair` + `reveal`) |
| `address` | the signing address (`VELA_SECP_KEY`) |
| `tee` | the Executor's P-521 communication key, read from `TeeAuthenticator.getPubSecp521r1()` |
| `deploy <wasm> [params-json\|-] [trigger]` | multipart upload to `<authority>/deploy/upload` (`multipart_encode`), then `submitDeployRequest(0, descriptor)` or `submitDeployRequestWithTrigger(0, descriptor, trigger)`; `applicationId` and `requestId` from the `DeployRequestSubmitted` log; waits on the subgraph; prints the `VELA_APP_ID` to set |
| `register` | AssociateKey: your P-521 public key ‖ the privacy seed encrypted for the Executor (226 bytes); the seed is `secp256k1_sign(keccak256("subtype-key-v1"))`, exactly like `novaw` |
| `deposit <amount> [token]` | ETH (wei) through `submitRequest` with an empty payload, or an allowlisted ERC-20 after `approve(processor, amount)` |
| `send '<json>' [wei]` | PROCESS with the payload encrypted for the Executor; an optional ETH deposit rides on the same request |
| `report '<json>'`, `report-download <id>` | DEANONYMIZATION (the caller must be an allowed authority), then the report fetched from the authority service — GET `/nonce`, an EIP-191 signature of `chainId(8) ‖ appId(8) ‖ reportId(32) ‖ nonce(32)`, POST `/getreport` — and decrypted |
| `events [n]`, `app-events [n]` | your events, filtered by your 50 HMAC subtypes and decrypted; the app's public events, with the label decoded when the subtype is one |
| `status <requestId>` | the subgraph's `requestCompleteds` / `deployRequestCompleteds` row |
| `user`, `register-for`, `send-for '<json>' [amount token]`, `events-for [n]` | **facilitator / meta-transactions** (`submitRequestFor`): the user (`VELA_USER_KEY`, needs **no ETH**) signs an EIP-712 `RequestAuthorization` (domain `Vela` / version `"0"` / chainId / the endpoint, `nonce = getFacilitatorNonce(user)`) and, for an ERC-20 deposit, an EIP-2612 `Permit` under the token's domain; `VELA_SECP_KEY` submits and pays gas and fee. Only ASSOCIATEKEY and PROCESS; the deposit can only be an ERC-20, never ETH |

Every request waits for `RequestCompleted` through the subgraph and reports the error code and
message when `status ≠ 0`. The pure parts have tests (`synsema test vela_client.syn`): the cipher
round trip, seed and subtypes, log parsing, calldata, the EIP-712 digests.

### The wire formats, so nobody has to re-read Go

- **Payloads and events**: `nonce(12) ‖ AES-256-GCM(key, data)`, no AAD, with
  `key = HKDF-SHA256(ECDH-P-521 shared X, no salt, no info, 32 bytes)` between the user's P-521 key
  and the Executor's communication key (133 bytes, uncompressed). Reports are encrypted for the
  requester's registered key. In Synsema: `ecdh_shared_secret` → `hkdf_sha256` → `aes_gcm_encrypt`.
- **AssociateKey** (type 3): the user's P-521 public key (133) ‖ the encrypted seed (93) = 226 bytes
  (133 alone is accepted). The seed is a 65-byte secp256k1 signature; the user's event subtypes are
  `HMAC-SHA256(seed, byte(i))`, i = 1..50 — the Executor stamps one on each event, and a client
  filters the subgraph by that set.
- **Request ids**: topic 2 of `RequestSubmitted`; a deploy's `applicationId` is topic 1 of
  `DeployRequestSubmitted` and its `requestId` the event's data.
- **Deploy descriptor** (the on-chain payload): `{"mode": "artifact_ref", "artifactId": "sha256:…",
  "wasmSha256": "…", "constructorParams": {…}}` after `POST /deploy/upload` (multipart field `wasm`).
- **Value rule** of `submitRequest`: ETH → `msg.value = assetAmount + maxFeeValue`; ERC-20 →
  `msg.value = maxFeeValue` and the endpoint pulls the tokens (allowance, or the permit in
  `submitRequestFor`).

## Trigger contracts: on-chain calls from inside the enclave

A trigger contract is the only autonomy an app has today: no cron, no oracle, no network. The
cycle, from the starter kit's design doc and verified end to end:

1. A user sends `process` (here `{"command": "execute", "execute": {target, value, data}}`). The app
   locks the amount, returns a **withdrawal to the trigger contract** and **one app event** whose
   `data_hex` is `abi.encode(bytes16 lockId, address target, uint256 value, bytes data)` with the
   label subtype `execute_requested`.
2. During `stateUpdate` the ProcessorEndpoint claims the withdrawn ETH into the trigger, calls
   `execute(appEventData)` — the call runs **from the pool's address** — then `withdraw()` sweeps
   what is left back, and `getTrustProcessPayload(...)` returns `abi.encode(bytes16 lockId, uint256
   remain, uint8 outcome)`. A non-empty payload enqueues a **TRUSTPROCESS** (priority queue, no
   sender, no fee).
3. The Executor calls `trusted_request`: the app decodes `payload_hex`, credits `remain` back to the
   lock's owner, clears the lock, emits an encrypted `execution_outcome` to the owner — and **no app
   event**, which is what ends the loop (the trigger's `events.length == 0` guard returns `""`).

`examples/trigger/src/PoolTrigger.sol` is the companion contract (it extends Vela's
`AbstractTrigger`; `build.sh` clones `HorizenOfficial/vela` v0.2.0 — those contracts are BSL and are
not vendored — and deploys it with `forge`). The app is deployed with `{"triggerContract": "0x…"}`
in the constructor params **and** the same address in `submitDeployRequestWithTrigger`
(`vela_client.syn -- deploy pool.wasm '{"triggerContract":"0x…"}' 0x…`). Measured: the target
received exactly 0.01 ETH during `stateUpdate`, and the TRUSTPROCESS came back about 25 s later.

## ERC-20 and the facilitator

Tokens must be in the `TokenAllowlist` (`addAllowedToken`, ADMIN role) and, for an app, in its
`allowedTokens` at deploy. `examples/erc20/src/TestToken.sol` is a minimal ERC-20 **with EIP-2612
`permit`**; `build.sh` deploys and allowlists it. From the client: `deploy payment.wasm
'{"allowedTokens":["0x…"]}'`, `deposit 1000000 0x…` (approve + pull), then transfers with
`"tokenAddress"` in the payload; the events carry `tokenAddress` and the endpoint's custody holds
the tokens.

The **facilitator** pattern is a client-side flow, not a platform limitation: `submitRequestFor`
lets someone else pay. The user signs typed data only; a user with **0 ETH** registered its key
(`register-for`) and deposited 2 TST plus a transfer in one request (`send-for … 2000000 0x…`),
the facilitator paying gas and 85 wei of fees; the user's events decrypt with the user's own seed
(`events-for`). One detail you will hit: OpenZeppelin's `ECDSA.recover` and `permit` want
`v = 27/28`, while `secp256k1_sign` returns the recovery id 0/1 — add 27.

## The public devnet

`devnet.synsema.app` runs Horizen's starter kit v0.2.0 (Anvil, the Vela contracts, the subgraph, the
Executor as an emulated TEE, the Manager, the Authority Service) behind HTTPS, for anyone who wants
to deploy a Synsema app on Vela without nine containers on their machine. A token of your own is one
command away; the client writes the lines into `client/.env`:

```sh
synsema run vela_client.syn -- devnet            # or: curl -X POST https://devnet.synsema.app/token
```

```
VELA_RPC_URL=https://devnet.synsema.app/<your token>/rpc
VELA_AUTHORITY_URL=https://devnet.synsema.app/<your token>/authority
VELA_SUBGRAPH_URL=https://devnet.synsema.app/<your token>/subgraph/subgraphs/name/hcce
VELA_PROCESSOR=0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
VELA_TEE_AUTHENTICATOR=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
VELA_TOKEN_ALLOWLIST=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
VELA_DEFAULT_AUTHORITY=0x5FbDB2315678afecb367f032d93F642f64180aa3
VELA_TEST_TOKEN=0x610178dA211FEF7D417bC0e6FeD39F05609AD788
VELA_TRIGGER_FACTORY=0xC9a43158891282A2B1475592D5719c001986Aaec
VELA_TOKEN=0x610178dA211FEF7D417bC0e6FeD39F05609AD788
VELA_SECP_KEY=…   VELA_P521_KEY=…   VELA_P521_PUB=…   (Anvil #0 and a fresh P-521 pair: everything a client needs)
```

It is a **devnet**: the keys are Anvil's public ones, there is no attestation, it is reset from time
to time (the contract addresses come back the same; app ids, balances and registered keys do not),
and nothing you put there is private from whoever runs the machine — a place to iterate, not to keep
value. The admin is Anvil account #0 (its private key is the well-known
`ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`, the kit's default `VELA_SECP_KEY`),
so everything an admin does is yours to do: deploying (`DEPLOYER_ROLE`), allowlisting an ERC-20
(`vela_client.syn -- allow-token <address>`), allowing an auditor for your app
(`vela_client.syn -- allow-authority <appId> <address>`). A test ERC-20 with `permit` (TST, 6 decimals,
the whole supply in Anvil #0) is already allowlisted: `VELA_TEST_TOKEN`. `https://devnet.synsema.app/status`
shows the block number and usage. The token is a name in the path that keeps scanners out and lets
usage be counted per user, not a secret worth anything.
Ten tokens an hour per address, except for the Synsema platform's control, which asks for one per
project it creates from a recipe. The kit's ProcessorEndpoint deploys with room for ten applications;
the devnet raises that to 100 000 (`devnet/admin.syn` in the platform's repository), so a
`MaxNumOfApplicationsExceeded` revert means a stack at its default, not a bug in your deploy.

## The local stack (starter kit v0.2.0) — a recipe that works

- `git clone HorizenOfficial/vela-starterkit`, `cp dockerfiles/.env.dev dockerfiles/.env`, `docker
  compose up -d` in `dockerfiles/` (nine images, about 1.5 GB). The chain is published on
  `localhost:8545`, the authority service on `:8081`, the subgraph on `:8000`; the internal network
  is `dockerfiles_pes_network`. The deployer writes the addresses to the `deploy-data` volume:
  ProcessorEndpoint `0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9`, TeeAuthenticator
  `0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0`, TokenAllowlist
  `0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9`, DefaultAuthority
  `0x5FbDB2315678afecb367f032d93F642f64180aa3`. Anvil account #0 is deployer and admin.
- `deployapp` needs `DEPLOYER_ROLE`; `requestreport` reverts with `AuthorityNotAllowed` until
  `DefaultAuthority.addAllowedAuthority(appId, caller)` is called by the admin (`cast` is inside the
  `vela-skit-chain` container). Multi-app works: each deploy gets its own `applicationId`.
- `novaw-linux` (the `vela-nova` release) on Windows: it cannot be executed straight from a bind
  mount — copy it into a container's filesystem first: `docker run --rm --network
  dockerfiles_pes_network --entrypoint sh -v <wallet dir>:/wallet -w /wallet postgres:14 -c 'cp
  /wallet/novaw-linux /usr/local/bin/novaw && chmod +x /usr/local/bin/novaw && novaw <cmd>'`, with
  `MSYS_NO_PATHCONV=1` in Git Bash and a `wallet.conf` pointing at `10.10.40.30:8545`,
  `10.10.40.40:8081`, `vela-skit-subgraph-node:8000`.
- `forge` and `cast` are inside the `horizen/cce-chain:v0.2.0` image. Compile contracts in a
  container on Docker's **default** network with `RPC_URL=http://host.docker.internal:8545` (the
  running chain container has no DNS for the solc and GitHub downloads).
- Timing: a deploy confirms in about 12 s, a request in 10–15 s, a TRUSTPROCESS round trip in about
  25 s. Executor limits: wasmtime-go v1.0.0, one request per block, a 30 s communication timeout,
  no fuel metering (fuel is self-declared), memory under 2 GB.

## Gotchas met while writing guests and clients

- `to` and `from` are reserved words — not even as parameter names. There are no `0xab` literals
  (write `171`). `index_of` takes a list, not text. Maps are shared by reference: a task that
  `set`s inside the state it received mutates the caller's map — it matters in tests that reuse a
  state.
- `ecdh_shared_secret`, `hkdf_sha256` and `aes_gcm_*` take keys as bytes or secret bytes: a hex key
  from `.env` is `as_secret(bytes(env("K"), "hex"), "K")` (a text `secret()` is the hex characters,
  not the key). `hmac_sha256(data, key)` stringifies a `bytes` value — wrap both in `as_secret`. The
  private key from `ecdh_keypair` is labelled `ecdh_keypair.private`
  (`require reveal("ecdh_keypair.private")` to print it once).
- `http_post(url, map)` sends JSON with the Content-Type; a `bytes` body goes raw; the response map
  has `status`, `ok`, `body`, `json`, `headers`. `multipart_encode(parts)` → `{body, content_type}`.
  `require file.read("*")` for a path given on the command line; `args()` needs no capability;
  `sleep` needs `time`; `secp256k1_sign` needs `sign("<the secret's label>")`.
- (Adapter developers) `cargo test` in the guest crate builds a `.wasm` it cannot run — pass `--target <host triple>`.

## Adding another host

Copy `packages/guests/vela/`, rename the crate, and read the host's contract **from its source**:
which exports it calls, their signatures, how it writes inputs and reads results, which imports it
provides, what it forbids. Write that at the top of `src/lib.rs` with the file and version it came
from. Keep the shape — decode inputs → one `ctx` map → `run_app(task, fallback, ctx)` → encode the
task's map into the host's exact result — pick `wasm32-wasip1` when the host links WASI and
`wasm32-unknown-unknown` only if it provides the `synsema_host` imports, treat the ceiling as the
determinism contract, write the probe, wire CI, and document the `.syn` contract in the guest's
README. If the generic ABI lacks something, add a documented `synsema_call` operation useful to
every host — never a special export for one of them.
