Liquidity Docs

ATS / BD / TA Interoperability

How the Alternative Trading System, Broker-Dealer, and Transfer Agent services work together — authentication, settlement flow, inter-service communication

Liquidity.io is built on three pillars: an Alternative Trading System (ATS), a Broker-Dealer (BD), and a Transfer Agent (TA). Each is a standalone Go binary. They communicate via REST with IAM JWTs for authentication and use organization_id from the IAM owner claim for tenant isolation.

Service Overview

ServiceBinaryRepoPortPurpose
ATSatsliquidityio/ats8080Order matching, settlement, on-chain recording
BDbdliquidityio/bd8090Compliance, KYC/KYB, multi-provider execution (Alpaca + 16 venues)
TAtaliquidityio/ta8100Transfer agent, cap table management (uses liquidityio/captable)

All three are Go services that import github.com/liquidityio/iam/sdk for JWT validation and share the same PostgreSQL cluster (separate databases per service).

Authentication

Every inter-service request carries an IAM JWT in the Authorization header. Behind the Gateway, services read the propagated X-IAM-* headers instead.

// ATS calling BD for compliance check
func (a *ATS) checkCompliance(ctx context.Context, orgID, investorID string) error {
    req, _ := http.NewRequestWithContext(ctx, "GET",
        fmt.Sprintf("http://bd.backend.svc.cluster.local:8090/v1/investors/%s/compliance", investorID),
        nil,
    )
    req.Header.Set("Authorization", "Bearer "+a.serviceToken)
    req.Header.Set("X-IAM-Org", orgID)

    resp, err := a.httpClient.Do(req)
    if err != nil {
        return fmt.Errorf("bd: compliance check failed: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("bd: compliance check returned %d", resp.StatusCode)
    }
    return nil
}

Service Tokens

Each service obtains its own IAM token via client credentials grant at startup and refreshes it before expiry.

func (s *Service) refreshToken(ctx context.Context) error {
    token, err := s.iamClient.ClientCredentials(ctx, sdk.ClientCredentialsRequest{
        ClientID:     s.config.IAMClientID,
        ClientSecret: s.config.IAMClientSecret,
        Scope:        "internal",
    })
    if err != nil {
        return fmt.Errorf("iam: token refresh failed: %w", err)
    }
    s.serviceToken = token.AccessToken
    return nil
}

Tenant Isolation

All three services enforce tenant isolation using organization_id from the IAM JWT owner claim. Every database query is scoped.

// In ATS order handler
func (h *Handler) ListOrders(w http.ResponseWriter, r *http.Request) {
    orgID := r.Header.Get("X-IAM-Org")
    if orgID == "" {
        http.Error(w, "missing organization", http.StatusForbidden)
        return
    }

    orders, err := h.store.ListOrders(r.Context(), orgID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(orders)
}

// In the store layer — organization_id is always in the WHERE clause
func (s *Store) ListOrders(ctx context.Context, orgID string) ([]Order, error) {
    rows, err := s.db.QueryContext(ctx,
        `SELECT id, asset_id, side, quantity, price, status, created_at
         FROM orders
         WHERE organization_id = $1
         ORDER BY created_at DESC`,
        orgID,
    )
    // ...
}

Settlement Flow

The settlement flow is the core integration point between ATS, BD, and TA. PostgreSQL is the source of truth — the chain records settlements but never blocks trading.

1. Investor places order


2. ATS: Pre-trade compliance ──────► BD: KYC/AML/accreditation check
        │                                    │
        │ ◄──── compliance: approved ────────┘

3. ATS: Match order (DEX engine)


4. ATS: Record trade in PostgreSQL (source of truth)

        ├──► 5a. ATS: Queue settlement intent (pending_settlements table)

        └──► 5b. ATS: Notify BD (webhook: trade.executed)

                       └──► BD: Post-trade surveillance
                            BD: Regulatory reporting (OATS, CAT)


6. Settlement cron (30s interval):

        ├──► MPC: Sign transaction (CGGMP21 threshold signature)

        ├──► Chain: Record on SettlementRegistry contract
        │         │
        │         ▼
        │    BD: Approve settlement (verify compliance)
        │         │
        │         ▼
        │    TA: Record transfer on cap table
        │         │
        │         ▼
        │    Chain: Mint/burn SecurityToken (ERC-20)

        └──► ATS: Update settlement status (PENDING → SETTLED)

Chain-Resilient Design

The ATS never blocks on chain availability. If the chain is down, trades continue normally — they are recorded in PostgreSQL. The settlement cron catches up when the chain recovers.

// Settlement cron — runs every 30 seconds
func (s *Settler) ProcessPending(ctx context.Context) error {
    settlements, err := s.store.ListPendingSettlements(ctx)
    if err != nil {
        return fmt.Errorf("list pending: %w", err)
    }

    for _, stl := range settlements {
        if err := s.settle(ctx, stl); err != nil {
            // Log and continue — don't block other settlements
            s.logger.Error("settlement failed",
                "settlement_id", stl.ID,
                "error", err,
            )
            continue
        }
    }
    return nil
}

func (s *Settler) settle(ctx context.Context, stl Settlement) error {
    // 1. Sign via MPC
    sig, err := s.mpc.Sign(ctx, stl.TxData)
    if err != nil {
        return fmt.Errorf("mpc sign: %w", err)
    }

    // 2. Submit to chain
    txHash, err := s.chain.SubmitSettlement(ctx, stl, sig)
    if err != nil {
        // Chain down — leave as pending, retry next cron cycle
        return fmt.Errorf("chain submit: %w", err)
    }

    // 3. Notify TA to record transfer
    if err := s.ta.RecordTransfer(ctx, stl); err != nil {
        return fmt.Errorf("ta record: %w", err)
    }

    // 4. Mark settled
    return s.store.MarkSettled(ctx, stl.ID, txHash)
}

ATS (Alternative Trading System)

The ATS is the order matching and settlement engine. It owns the order lifecycle from placement through settlement.

Endpoints

MethodPathDescription
POST/v1/ordersPlace a new order
GET/v1/ordersList orders (scoped to org)
GET/v1/orders/{id}Get order by ID
DELETE/v1/orders/{id}Cancel order
GET/v1/tradesList trades (scoped to org)
GET/v1/settlementsList settlements
GET/v1/settlements/{id}Get settlement status
GET/v1/assetsList tradable assets
GET/v1/assets/{id}/orderbookOrder book depth

Order Lifecycle

NEW → PENDING_COMPLIANCE → OPEN → PARTIALLY_FILLED → FILLED → SETTLING → SETTLED

NEW → PENDING_COMPLIANCE → REJECTED (compliance failure)      SETTLEMENT_FAILED

NEW → PENDING_COMPLIANCE → OPEN → CANCELLED                    REVERSED

BD (Broker-Dealer)

The BD handles compliance, investor verification, and multi-venue execution routing. It wraps 16 execution venues behind a unified API.

Endpoints

MethodPathDescription
GET/v1/investors/{id}/complianceFull compliance status
POST/v1/investors/{id}/kycInitiate KYC verification
POST/v1/investors/{id}/accreditationVerify accreditation
GET/v1/investors/{id}/accreditationAccreditation status
POST/v1/compliance/pre-tradePre-trade compliance check
POST/v1/compliance/post-tradePost-trade surveillance
GET/v1/reporting/oatsFINRA OATS report data
GET/v1/reporting/catCAT report data
POST/v1/execution/routeSmart order routing (Broker)

Execution Venues

The BD integrates with 16 execution venues via liquidityio/broker:

VenueAsset ClassesProtocol
AlpacaUS equities, cryptoREST
Interactive BrokersGlobal equities, options, futuresFIX 4.4
BitGoCrypto custody + tradingREST
BinanceCrypto spot + derivativesWebSocket
KrakenCrypto spotREST
GeminiCrypto spotREST + FIX
CoinbaseCrypto spotREST
SFOXCrypto aggregationREST
FalconXCrypto institutionalREST
FireblocksCrypto custodyREST
CircleUSDC, stablecoin railsREST
TradierUS equities, optionsREST
Polygon.ioMarket data onlyWebSocket
CurrencyCloudFXREST
LMAXFX + cryptoFIX 4.4
FinixPayment processingREST

Smart Order Routing

The BD routes orders to the best venue based on price, liquidity, and execution quality (FINRA 5310 / MiFID II best execution).

// BD routes an order to the optimal venue
route, err := broker.Route(ctx, broker.RouteRequest{
    OrgID:    orgID,
    Symbol:   "AAPL",
    Side:     "buy",
    Quantity: 100,
    Strategy: broker.BestExecution, // FINRA 5310 compliant
})
// route.Venue = "alpaca"
// route.Price = 187.50
// route.Fills = [{qty: 100, price: 187.50}]

TA (Transfer Agent)

The TA records ownership transfers on the cap table and manages the shareholder registry. It uses liquidityio/captable for the underlying cap table engine.

Endpoints

MethodPathDescription
POST/v1/transfersRecord a transfer instruction
GET/v1/transfers/{id}Get transfer status
GET/v1/holdingsList holdings (scoped to org)
GET/v1/holdings/{investor_id}Holdings for an investor
POST/v1/dividendsDeclare a dividend
GET/v1/dividendsList declared dividends
POST/v1/corporate-actionsRecord a corporate action
GET/v1/shareholders/{asset_id}Shareholder registry
GET/v1/statements/{investor_id}Generate statement

Transfer Recording

When a settlement completes, the ATS calls the TA to record the transfer.

// TA records a transfer from seller to buyer
func (ta *TransferAgent) RecordTransfer(ctx context.Context, req TransferRequest) (*Transfer, error) {
    // 1. Validate the transfer instruction
    if err := ta.validate(ctx, req); err != nil {
        return nil, fmt.Errorf("validation failed: %w", err)
    }

    // 2. Check transfer restrictions (Rule 144, lock-up, etc.)
    if err := ta.checkRestrictions(ctx, req); err != nil {
        return nil, fmt.Errorf("restriction check failed: %w", err)
    }

    // 3. Record on cap table
    transfer, err := ta.captable.Transfer(ctx, captable.TransferRequest{
        AssetID:        req.AssetID,
        FromInvestorID: req.SellerID,
        ToInvestorID:   req.BuyerID,
        Quantity:       req.Quantity,
        PricePerShare:  req.PricePerShare,
        OrganizationID: req.OrganizationID,
        SettlementID:   req.SettlementID,
    })
    if err != nil {
        return nil, fmt.Errorf("captable transfer: %w", err)
    }

    // 4. Update shareholder registry
    if err := ta.updateRegistry(ctx, transfer); err != nil {
        return nil, fmt.Errorf("registry update: %w", err)
    }

    return transfer, nil
}

Cap Table Integration

The TA uses liquidityio/captable for:

FeatureDescription
Shareholder registryDefinitive record of ownership
Transfer restrictionsRule 144 holding periods, lock-ups, ROFR
DividendsDeclaration, record date, payment distribution
Corporate actionsStock splits, mergers, conversions
StatementsInvestor account statements, tax forms
FIFO lot trackingCost basis tracking for tax reporting

Inter-Service Communication

All three services run in the same Kubernetes namespace and communicate via K8s DNS.

ATS  → http://bd.backend.svc.cluster.local:8090
ATS  → http://ta.backend.svc.cluster.local:8100
BD   → http://ats.backend.svc.cluster.local:8080
TA   → http://ats.backend.svc.cluster.local:8080

Every request includes:

  • Authorization: Bearer <service-jwt> (IAM client credentials token)
  • X-IAM-Org: <organization_id> (tenant scope)
  • X-Request-Id: <uuid> (distributed tracing)

Error Handling

Inter-service errors use standard HTTP status codes. Services return structured error responses.

{
  "error": {
    "code": "COMPLIANCE_REJECTED",
    "message": "Investor accreditation expired",
    "details": {
      "investor_id": "inv_a1b2c3",
      "check": "accreditation",
      "expired_at": "2026-01-15T00:00:00Z"
    }
  }
}
StatusMeaningCaller Action
200SuccessProcess response
400Bad requestFix request payload
403Forbidden (wrong org, insufficient role)Do not retry
404Resource not foundDo not retry
409Conflict (duplicate, stale state)Fetch current state, retry
502Downstream service unavailableRetry with backoff

Database Schema

Each service owns its own database on the shared PostgreSQL cluster.

ServiceDatabaseKey Tables
ATSatsorders, trades, settlements, pending_settlements, assets
BDbdinvestors, compliance_checks, accreditations, execution_routes
TAtatransfers, holdings, dividends, corporate_actions, shareholder_registry

All tables include organization_id TEXT NOT NULL and a corresponding index.

-- Example: ATS orders table
CREATE TABLE orders (
    id              TEXT PRIMARY KEY,
    organization_id TEXT NOT NULL,
    investor_id     TEXT NOT NULL,
    asset_id        TEXT NOT NULL,
    side            TEXT NOT NULL CHECK (side IN ('buy', 'sell')),
    order_type      TEXT NOT NULL CHECK (order_type IN ('market', 'limit', 'stop', 'stop_limit')),
    quantity        NUMERIC NOT NULL,
    price           NUMERIC,
    status          TEXT NOT NULL DEFAULT 'new',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_orders_org ON orders (organization_id);
CREATE INDEX idx_orders_org_status ON orders (organization_id, status);
CREATE INDEX idx_orders_org_investor ON orders (organization_id, investor_id);

Deployment

All three services are deployed as Kubernetes Deployments in the backend namespace.

# ATS
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ats
  namespace: backend
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: ats
          image: ghcr.io/liquidityio/ats:next
          ports:
            - containerPort: 8080
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: ats-db
                  key: url
            - name: IAM_CLIENT_ID
              valueFrom:
                secretKeyRef:
                  name: ats-iam
                  key: client_id
            - name: IAM_CLIENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: ats-iam
                  key: client_secret
            - name: IAM_URL
              value: https://iam.liquidity.io

On this page