Skip to content

Operator Guide

This guide walks through deploying and configuring an Aspens Market Stack (AMS). Stack ops live in the infra repository (Docker Swarm + a justfile); per-stack configuration is driven by the aspens-admin CLI from the SDK repository.

1. Prerequisites

  • Docker (with Swarm mode) on the host
  • A domain name pointing at the host (for any non-local deploy)
  • RPC endpoints for each chain you want to support
  • A funded admin wallet (EVM key for EIP-712 login)
  • An admin wallet funded with native gas on every chain you'll support (the arborter signs net-settlement settleBatch and withdrawal-voucher transactions from this account; trading itself is off-chain)
  • On Tron, TRX in the TEE signer's Tron account (see Tron chains) — Tron transactions pay for energy by burning TRX

2. Bring up the stack

The infra/ repo contains three stack flavors:

StackPurpose
localAnvil / solana-test-validator on your laptop
googlecloudConfidential VM deploy behind your own domain
testing-suiteIntegration-test scaffolding

Local development is just-driven:

git clone https://github.com/aspensprotocol/infra
cd infra
just local-up                                # bring up the local stack
just test-up evm-evm-local                   # deploy + configure a scenario end-to-end

Available scenarios live under infra/scenarios/*.toml (evm-evm-local, evm-evm-testnet-minimal, evm-evm-testnet, solana-evm-local, evm-evm-solana-local, evm-evm-solana-testnet, base-arb-op-solana-local).

For a CVM deploy, see infra/stacks/googlecloud/README.md — it covers domain templating, traefik / certbot setup, secret rotation, and image bumps.

3. Configure with aspens-admin

Install the admin CLI:

cargo install --locked --git https://github.com/aspensprotocol/sdk aspens-admin

Configure the environment:

# .env
ASPENS_MARKET_STACK_URL=http://localhost:50051
ADMIN_PRIVKEY=<64-char hex, no 0x prefix>

Initialize the admin (fresh stack only)

aspens-admin init-admin --address 0xYourAdminAddress

This is a one-time call. On any subsequent stack, log in to obtain a JWT:

aspens-admin login                   # uses ADMIN_PRIVKEY for EIP-712 signature

Save the returned JWT to .env as ASPENS_JWT (or pass --jwt per command).

Add chains

aspens-admin set-chain \
  --architecture EVM \
  --canonical-name "Base Sepolia" \
  --network base-sepolia \
  --chain-id 84532 \
  --rpc-url https://your-base-sepolia-rpc \
  --factory-address 0xYourMidribFactoryAddress \
  --explorer-url https://sepolia.basescan.org

The arborter mints the per-chain instance signer key inside the TEE when you call set-chain — secp256k1 for --architecture EVM, Ed25519 for --architecture Solana. For --architecture Tron the signer reuses its EVM key (see Tron chains). The architecture string is normalized to lowercase server-side, so casing on the CLI doesn't matter.

On a fresh chain registration the arborter derives the instance signer address and returns it automatically. If you re-run set-chain for a chain whose signer keys already exist (e.g. after a database reset), that auto-derivation is skipped and the SDK later fails with invalid instance_signer_address. Supply it explicitly with --instance-signer-address in that case — read the value back with aspens-cli signer-public-key --chain-network <network>.

Tron chains

Register a Tron chain with --architecture tron. The chain's RPC URL is the node's HTTP base (for example a TronGrid endpoint); the arborter derives the JSON-RPC and wallet paths from it. TronGrid's API key goes in as a header, so add the endpoint with auth scheme Header and header name TRON-PRO-API-KEY in the admin console's RPC endpoints dialog. Unlike Solana endpoints, which authenticate through the URL only, Tron endpoints may use header auth.

Addresses inside the venue are 20-byte 0x hex, exactly as on EVM. The T... form is a display form only: convert it to hex before passing it to aspens-admin or anything else that talks to the venue.

Funding the signer. The TEE signer's Tron account is the same 20-byte account as its EVM one, shown in T... form on Tron. It must hold TRX, because Tron charges energy and the signer has none staked, so it burns TRX to pay for settlements and fee sweeps. Withdrawals are different: the user's own wallet sends and pays for the withdrawal transaction, using a voucher the venue signs, so they cost the signer no TRX.

  • A settle of 50 deltas burned about 165 TRX on Nile with no staked energy.
  • The arborter caps each settle at a fee limit of 477.246 TRX.
  • The deployer's funding gate requires 4,772.46 TRX in the account before it will proceed.

A broadcast rejected for balance, or a transaction that runs out of energy, fails with an error naming the signer's address; top the account up with TRX.

Deploying the instance. The admin wallet cannot deploy a Tron instance, and the admin console's Deploy button is disabled for Tron chains. Deploy Tron instances with the deployer.

Add tokens

aspens-admin set-token \
  --network base-sepolia \
  --name "USD Coin" \
  --symbol USDC \
  --address 0xYourUsdcAddress \
  --decimals 6

Deploy trade contracts

aspens-admin deploy-contract base-sepolia --fees 100   # 100 bps = 1%

For EVM chains, the admin's wallet signs and broadcasts MidribFactory.createInstance. Tron instances are deployed with the deployer, not with this command. For Solana chains, the arborter signs and submits create_instance server-side using the factory's owner key (only the factory owner can call it under the has_one = owner constraint).

Fees (two tiers)

Fees are two-tier and taker-pays (charged on the received leg at settlement):

  • Maintenance fee — the Aspens cut, baked into the instance at deploy time via deploy-contract --fees <bps> (immutable for that instance).
  • Operator fee — the stack operator's own cut, set (and re-set) after deploy:
aspens-admin set-operator-fee \
  --chain-network base-sepolia \
  --recipient 0xYourFeeRecipient \
  --bps 12

The arborter submits the on-chain setOperatorFee as the instance's operator_admin; rotate that key with rotate-operator-admin.

Create markets

aspens-admin set-market \
  --base-network base-sepolia \
  --quote-network op-sepolia \
  --base-symbol USDC \
  --quote-symbol USDT \
  --base-address 0xUsdc... \
  --quote-address 0xUsdt... \
  --base-decimals 6 \
  --quote-decimals 6 \
  --pair-decimals 6

pair-decimals is the precision the matching engine works in internally — it can differ from either token's native decimals. See decimals.md in the SDK for conversion examples.

Verify

aspens-admin status               # connection check
aspens-admin version              # server build info
aspens-admin balances             # owner / signer / contract balances per chain

4. Reference: aspens-admin commands

CommandNotes
init-admin --address <addr>One-time, fresh stack only
login [--chain-id N]Sign EIP-712 with ADMIN_PRIVKEY
update-admin <addr>Rotate admin
set-chain ... / delete-chain <network>Chain registration
set-token ... / delete-token ...Token registration
set-market ... / delete-market <id>Market creation
deploy-contract <network> --fees <bps>Deploy MidribV3 instance (baked maintenance fee)
set-operator-fee --chain-network <n> --recipient <addr> --bps <n>Set the operator-fee tier
rotate-operator-admin ...Rotate the instance's operator_admin key
set-trade-contract --address ...Register an externally-deployed instance
delete-trade-contract <network>Unregister
version / status / admin-public-keyRead-only info
balancesOwner / signer / contract balances per chain

5. Maintenance

  • Image bumps: edit the @sha256: digest in infra/stacks/.../docker-compose.yml, then redeploy.
  • Database backups: back up the Postgres volume (tee_db) and the TigerBeetle data volume (tee_tigerbeetle-data) as a pair — they are one logical datastore (Postgres holds the registry + history that names the TigerBeetle balances), so restoring one without the other strands state.
  • Logs: docker stack ps <stack> for placement, docker service logs <service> for tails.
  • Hard reset: scale services to 0, drop tee_db and tee_tigerbeetle-data together (the registry and the ledger are a pair), then scale back up. Drop tee_arborter-keys as well only when rotating the signer (that forces a contract redeploy).

The CVM stack README captures the full operational catalog (signer rotation, GHCR token refresh, certbot retry, traefik tls.yml re-render, etc.).

6. Going live

Before opening the stack to traders:

  1. Fund the admin wallet on every chain (the arborter signs net settleBatch and withdrawal-voucher transactions from this address; insufficient native balance will hang settlement). On Tron, fund the signer's Tron account with TRX instead.
  2. Verify every chain, token, and market with aspens-admin status.
  3. Run a paper trade end-to-end with aspens-cli:
aspens-cli deposit  base-sepolia USDC 100
aspens-cli buy-limit USDC/USDT 10 0.99
aspens-cli stream-trades USDC/USDT --historical

Resources