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
| Service | Binary | Repo | Port | Purpose |
|---|---|---|---|---|
| ATS | ats | liquidityio/ats | 8080 | Order matching, settlement, on-chain recording |
| BD | bd | liquidityio/bd | 8090 | Compliance, KYC/KYB, multi-provider execution (Alpaca + 16 venues) |
| TA | ta | liquidityio/ta | 8100 | Transfer 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
| Method | Path | Description |
|---|---|---|
POST | /v1/orders | Place a new order |
GET | /v1/orders | List orders (scoped to org) |
GET | /v1/orders/{id} | Get order by ID |
DELETE | /v1/orders/{id} | Cancel order |
GET | /v1/trades | List trades (scoped to org) |
GET | /v1/settlements | List settlements |
GET | /v1/settlements/{id} | Get settlement status |
GET | /v1/assets | List tradable assets |
GET | /v1/assets/{id}/orderbook | Order 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 REVERSEDBD (Broker-Dealer)
The BD handles compliance, investor verification, and multi-venue execution routing. It wraps 16 execution venues behind a unified API.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v1/investors/{id}/compliance | Full compliance status |
POST | /v1/investors/{id}/kyc | Initiate KYC verification |
POST | /v1/investors/{id}/accreditation | Verify accreditation |
GET | /v1/investors/{id}/accreditation | Accreditation status |
POST | /v1/compliance/pre-trade | Pre-trade compliance check |
POST | /v1/compliance/post-trade | Post-trade surveillance |
GET | /v1/reporting/oats | FINRA OATS report data |
GET | /v1/reporting/cat | CAT report data |
POST | /v1/execution/route | Smart order routing (Broker) |
Execution Venues
The BD integrates with 16 execution venues via liquidityio/broker:
| Venue | Asset Classes | Protocol |
|---|---|---|
| Alpaca | US equities, crypto | REST |
| Interactive Brokers | Global equities, options, futures | FIX 4.4 |
| BitGo | Crypto custody + trading | REST |
| Binance | Crypto spot + derivatives | WebSocket |
| Kraken | Crypto spot | REST |
| Gemini | Crypto spot | REST + FIX |
| Coinbase | Crypto spot | REST |
| SFOX | Crypto aggregation | REST |
| FalconX | Crypto institutional | REST |
| Fireblocks | Crypto custody | REST |
| Circle | USDC, stablecoin rails | REST |
| Tradier | US equities, options | REST |
| Polygon.io | Market data only | WebSocket |
| CurrencyCloud | FX | REST |
| LMAX | FX + crypto | FIX 4.4 |
| Finix | Payment processing | REST |
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
| Method | Path | Description |
|---|---|---|
POST | /v1/transfers | Record a transfer instruction |
GET | /v1/transfers/{id} | Get transfer status |
GET | /v1/holdings | List holdings (scoped to org) |
GET | /v1/holdings/{investor_id} | Holdings for an investor |
POST | /v1/dividends | Declare a dividend |
GET | /v1/dividends | List declared dividends |
POST | /v1/corporate-actions | Record 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:
| Feature | Description |
|---|---|
| Shareholder registry | Definitive record of ownership |
| Transfer restrictions | Rule 144 holding periods, lock-ups, ROFR |
| Dividends | Declaration, record date, payment distribution |
| Corporate actions | Stock splits, mergers, conversions |
| Statements | Investor account statements, tax forms |
| FIFO lot tracking | Cost 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:8080Every 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"
}
}
}| Status | Meaning | Caller Action |
|---|---|---|
200 | Success | Process response |
400 | Bad request | Fix request payload |
403 | Forbidden (wrong org, insufficient role) | Do not retry |
404 | Resource not found | Do not retry |
409 | Conflict (duplicate, stale state) | Fetch current state, retry |
502 | Downstream service unavailable | Retry with backoff |
Database Schema
Each service owns its own database on the shared PostgreSQL cluster.
| Service | Database | Key Tables |
|---|---|---|
| ATS | ats | orders, trades, settlements, pending_settlements, assets |
| BD | bd | investors, compliance_checks, accreditations, execution_routes |
| TA | ta | transfers, 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