02 · Architecture

Ballast has three actors that sign in three different trust domains. Keeping them straight is the key to understanding the system.

Diagram 1: System architecture

flowchart TB
    subgraph principal["Principal domain (wallet-signed)"]
        treasury["Treasury / owner wallet"]
        dash["Dashboard (web/, dapp-kit)"]
    end

    subgraph agentdomain["Agent domain (agent-server-signed)"]
        agent["Agent service (agent/)"]
        sdk["@ballast/sdk"]
    end

    subgraph tee["TEE domain (TEE-signed)"]
        enclave["AWS Nitro enclave (enclave/)"]
    end

    subgraph chain["Sui testnet"]
        subgraph pkg["Ballast Move package (v3)"]
            identity["identity"]
            capability["capability"]
            attestation["attestation"]
            trading["trading"]
        end
        deepbook["DeepBook v3 pool + BalanceManager"]
        enclaveobj["Enclave (BallastEnclave) — registered TEE key"]
    end

    treasury -->|"connect"| dash
    dash -->|"mint / issue / fund / revoke (wallet-signed)"| identity
    dash -->|"issue / revoke (wallet-signed)"| capability
    dash -->|"trigger run"| agent
    agent --> sdk
    agent -->|"sign trade decision"| enclave
    enclave -->|"Ed25519 sig + timestamp"| agent
    sdk -->|"execute_trade[_attested] (agent-signed PTB)"| trading
    trading -->|"enforce()"| capability
    trading -->|"verify_nautilus()"| attestation
    attestation -.->|"checks sig vs proven key"| enclaveobj
    trading -->|"place_market_order"| deepbook
    trading -->|"record_ok + TradeExecuted"| identity

Reading it: the principal never gives the agent a key. It gives it a BallastCap. The agent builds and signs the trade PTB, but the decision is signed inside the enclave, and the chain re-checks both the capability rules (enforce) and the enclave signature (verify_nautilus) before DeepBook is reached. The mock path skips the enclave and uses attestation::verify (the stub) instead. Same shape, one fewer guarantee.

Diagram 2: Move module architecture

The package is four small modules. trading is the composition root: it ties an identity, a capability, an attestation, and a DeepBook pool together into one gated call.

classDiagram
    class identity {
        <<module>>
        +mint(label, clock)
        +record_ok(rep_delta) package
        +reputation() u64
    }
    class AgentIdentity {
        UID id
        address owner
        String label
        u64 reputation
        u64 actions_ok
        u64 actions_blocked
        u64 created_at_ms
    }
    class capability {
        <<module>>
        +issue(identity, spend_limit, max_lev_bps, markets, expiry)
        +enforce(cap, market, amount, lev_bps, clock)
        +revoke(cap, ctx)
    }
    class BallastCap {
        UID id
        ID agent
        address owner
        u64 spend_limit
        u64 spent
        u64 max_leverage_bps
        vector~ID~ allowed_markets
        u64 expiry_ms
        bool revoked
    }
    class attestation {
        <<module>>
        +verify(payload, sig) bool_STUB
        +verify_nautilus(enclave, ts, agent, market, amount, is_bid, sig) bool
        +init_attestation(name, pcr0, pcr1, pcr2)
    }
    class TradeAttestation {
        address agent
        address market
        u64 amount
        bool is_bid
    }
    class trading {
        <<module>>
        +execute_trade() mockPath
        +execute_trade_attested() teePath
        +authorize(cap, market, amount, lev, payload, sig)
        +record_success() package
    }
    class TradeExecuted {
        <<event>>
        ID agent
        ID cap
        ID pool
        u64 amount
        bool is_bid
        u64 leverage_bps
        u64 client_order_id
        u64 reputation
    }

    identity --> AgentIdentity : defines
    capability --> BallastCap : defines
    attestation --> TradeAttestation : signs (BCS)
    trading --> TradeExecuted : emits
    trading ..> capability : enforce()
    trading ..> attestation : verify() / verify_nautilus()
    trading ..> identity : record_ok()

How execute_trade composes the modules

  1. assert!(capability::agent(cap) == object::id(identity)) confirms this cap was issued for this identity (ECapIdentityMismatch, code 2).
  2. market = object::id(pool) derives the market from the actual pool object, so it can’t be spoofed in an argument.
  3. authorize(...) runs capability::enforce(...) (the five rules) then attestation::verify(...) (stub). On the attested path it is attestation::verify_nautilus(...) before enforce instead.
  4. balance_manager.generate_proof_as_owner(ctx) and pool.place_market_order(...) place the real DeepBook fill.
  5. record_success(...) calls identity::record_ok (reputation) and emits TradeExecuted.

Because steps 1 to 3 use assert!, any failure aborts the entire Programmable Transaction Block. The spent increment and any DeepBook settlement chained after it roll back atomically. The chain refuses to start the trade, so Ballast never has to undo one.