Securities Compliance

Built-in regulatory compliance for operating LX as a registered ATS, MTF, or RMO across US, UK, and Singapore jurisdictions

Securities Compliance

LX includes a native compliance layer that enables operators to run a regulated securities venue — whether a US Alternative Trading System (ATS), UK Multilateral Trading Facility (MTF), or Singapore Recognised Market Operator (RMO) — without modifying the core matching engine.

Architecture

The compliance layer sits between the API and the matching engine as two interface hooks:

  Client Request
       |
       v
  ┌─────────────────────┐
  │  EligibilityChecker  │  Pre-trade: KYC, accreditation, jurisdiction,
  │  (your impl)         │  holding period, max holders, ROFR
  └──────────┬──────────┘
             v
  ┌─────────────────────┐
  │  AdvancedOrderBook   │  Matching engine (unchanged)
  │  Price-time priority │
  └──────────┬──────────┘
             v
  ┌─────────────────────┐
  │  SettlementHandler   │  Post-trade: transfer agent, cap table,
  │  (your impl)         │  on-chain token transfer, escrow
  └─────────────────────┘

The matching engine itself is completely asset-agnostic. All regulatory logic lives in the operator-provided implementations of EligibilityChecker and SettlementHandler.

Quick Start

1. Define Your Market

import "github.com/luxfi/dex/pkg/dex"

market := lx.NewSecuritiesMarket(lx.MarketConfig{
    Symbol:     "ACME-USD",
    AssetClass: lx.AssetClassEquity,
    Exemption:  lx.ExemptionRegD506c,
    ShareClass: "Series A Preferred",

    // Trading constraints
    MinNotional: 25000.0,  // $25K minimum trade value
    MinSize:     1.0,      // Whole shares only
    TickSize:    0.01,
    LotSize:     1.0,

    // Transfer restrictions
    Restrictions: &lx.TransferRestriction{
        HoldingPeriod:     365 * 24 * time.Hour, // 12-month Rule 144
        MaxHolders:        2000,                  // Section 12(g) limit
        MinInvestorStatus: lx.InvestorAccredited, // Accredited only
        JurisdictionWhitelist: []string{"US", "CA"},
    },

    // Plug in your compliance backend
    EligibilityChecker: &myEligibilityChecker{},
    SettlementHandler:  &mySettlementHandler{},
})

2. Implement the Eligibility Checker

The EligibilityChecker interface has a single method:

type EligibilityChecker interface {
    CanTrade(userID string, symbol string, side lx.Side, size float64, price float64) error
}

Return nil to allow the trade, or a *lx.ComplianceError to reject it.

LX provides StandardEligibilityChecker that handles common patterns out of the box:

checker := &lx.StandardEligibilityChecker{
    // Connect to your KYC/investor database
    GetProfile: func(userID string) (*lx.InvestorProfile, error) {
        investor, err := db.GetInvestor(userID)
        if err != nil {
            return nil, err
        }
        return &lx.InvestorProfile{
            UserID:       investor.ID,
            Status:       mapStatus(investor.AccreditationLevel),
            Jurisdiction: investor.CountryCode, // ISO 3166-1 alpha-2
            KYCVerified:  investor.KYCComplete,
            AMLCleared:   investor.AMLCleared,
            Holdings:     getHoldings(investor.ID),
        }, nil
    },

    // Connect to your cap table
    GetHolderCount: func(symbol string) (int, error) {
        return capTable.CountHolders(symbol)
    },

    Restrictions: market.Config.Restrictions,
}

3. Implement the Settlement Handler

type SettlementHandler interface {
    OnTrade(trade *lx.Trade, market *lx.MarketConfig) error
}

This is called after each match. Example implementations:

Off-chain (transfer agent):

func (h *cartaHandler) OnTrade(trade *lx.Trade, mc *lx.MarketConfig) error {
    return h.client.RecordTransfer(carta.Transfer{
        SecurityID: mc.Symbol,
        FromHolder: trade.Seller,
        ToHolder:   trade.Buyer,
        Shares:     trade.Size,
        PricePerShare: trade.Price,
    })
}

On-chain (ERC-1400 security token):

func (h *onChainHandler) OnTrade(trade *lx.Trade, mc *lx.MarketConfig) error {
    tx, err := h.token.TransferByPartition(
        []byte(mc.ShareClass),
        common.HexToAddress(trade.Seller),
        common.HexToAddress(trade.Buyer),
        big.NewInt(int64(trade.Size)),
        nil,
    )
    return err
}

4. Place Orders

Orders go through the compliance layer automatically:

order := &lx.AdvancedOrder{
    ID:     1,
    UserID: "investor-123",
    Symbol: "ACME-USD",
    Side:   lx.Buy,
    Type:   lx.Limit,
    Price:  50.00,
    Size:   500.0, // 500 shares at $50 = $25,000 notional
}

trades, err := market.AddOrder(order)
if err != nil {
    // err is *lx.ComplianceError with Code, Message, Detail
    var ce *lx.ComplianceError
    if errors.As(err, &ce) {
        switch ce.Code {
        case lx.ErrNotAccredited:
            // Investor not accredited
        case lx.ErrJurisdiction:
            // Blocked jurisdiction
        case lx.ErrMinNotional:
            // Below $25K minimum
        }
    }
}

Supported Jurisdictions

United States (SEC)

ExemptionConstantDescriptionInvestor Requirement
Reg D 506(b)ExemptionRegD506bPrivate placement, up to 35 non-accreditedAccredited (mostly)
Reg D 506(c)ExemptionRegD506cPrivate placement, general solicitation OKAccredited only
Reg A Tier 1ExemptionRegA1Mini-IPO up to $20M/yearRetail OK
Reg A Tier 2ExemptionRegA2Mini-IPO up to $75M/yearRetail with limits
Reg CFExemptionRegCFCrowdfunding up to $5M/yearRetail with limits
Reg SExemptionRegSOffshore offeringNon-US only
Rule 144ExemptionRule144Resale of restricted securitiesHolding period required
Section 4(a)(7)ExemptionSection4a7Private resaleAccredited only

Key US constraints enforced:

  • Accredited investor verification (SEC Rule 506(c))
  • Maximum holder limits (Section 12(g): 2,000 total / 500 non-accredited)
  • Holding periods (Rule 144: 6 or 12 months)
  • Blue sky law jurisdiction filtering

United Kingdom (FCA)

ExemptionConstantDescriptionInvestor Requirement
Private PlacementExemptionFCAPrivateNo prospectus requiredProfessional / Sophisticated
Prospectus ExemptExemptionFCAProspectusQualified investors onlyProfessional only

Key UK constraints:

  • FCA client categorisation: Retail, Elective Professional, Per Se Professional
  • Use InvestorSophisticated for certified sophisticated investors
  • Use InvestorProfessional for professional clients
  • Jurisdiction whitelist: ["GB", "GG", "JE", "IM"] for UK + Crown Dependencies

Singapore (MAS)

ExemptionConstantDescriptionInvestor Requirement
Small OffersExemptionMASSIPUp to S$5M in 12 months, max 50 personsRetail OK
Private PlacementExemptionMASPrivateMax 50 persons in 12 monthsAny
InstitutionalExemptionMASInstitutionalInstitutional investors onlyInstitutional

Key Singapore constraints:

  • MAS accredited investor maps to InvestorProfessional
  • Use MaxHolders: 50 for private placement and small offer exemptions
  • Jurisdiction whitelist: ["SG"] or broader APAC as needed

Investor Classification

Investor statuses are ordered by privilege level (higher = more access):

StatusConstantUS EquivalentUK EquivalentSG Equivalent
RetailInvestorRetailNon-accreditedRetail clientRetail
AccreditedInvestorAccreditedSEC accredited----
SophisticatedInvestorSophisticated--Certified sophisticated--
ProfessionalInvestorProfessional--Professional clientMAS accredited
Qualified PurchaserInvestorQualifiedPurchaser$5M+ investments----
Qualified ClientInvestorQualifiedClient$1.1M+ net worth----
InstitutionalInvestorInstitutionalBank, fund, etc.Per se professionalInstitutional

The MinInvestorStatus on TransferRestriction uses >= comparison, so setting it to InvestorAccredited allows accredited, sophisticated, professional, and institutional investors.

Transfer Restrictions

type TransferRestriction struct {
    HoldingPeriod         time.Duration  // Minimum hold before resale
    MaxHolders            int            // Maximum beneficial owners (0 = unlimited)
    MinInvestorStatus     InvestorStatus // Minimum investor classification
    JurisdictionWhitelist []string       // Allowed jurisdictions (empty = all)
    JurisdictionBlacklist []string       // Blocked jurisdictions
    ROFRRequired          bool           // Right of first refusal
    BoardApprovalRequired bool           // Board/GP approval for transfers
}

Common Configurations

US Reg D 506(c) — Startup equity:

&lx.TransferRestriction{
    MinInvestorStatus:     lx.InvestorAccredited,
    MaxHolders:            2000,
    HoldingPeriod:         365 * 24 * time.Hour, // 12 months
    JurisdictionWhitelist: []string{"US"},
    ROFRRequired:          true, // Company ROFR
}

UK FCA Private Placement:

&lx.TransferRestriction{
    MinInvestorStatus:     lx.InvestorSophisticated,
    JurisdictionWhitelist: []string{"GB", "GG", "JE", "IM"},
}

Singapore MAS Private Placement:

&lx.TransferRestriction{
    MaxHolders:            50,
    JurisdictionWhitelist: []string{"SG"},
}

Public equity (Reg A Tier 2 — minimal restrictions):

&lx.TransferRestriction{
    JurisdictionBlacklist: []string{"KP", "IR", "CU", "SY"}, // OFAC
}

Market Configuration

SingleMatchMode

For private securities markets where each order should match at most once (similar to traditional block trading), enable SingleMatchMode:

config := lx.MarketConfig{
    // ...
    SingleMatchMode: true, // Cancel remaining qty after first fill
}

This uses IOC (Immediate-or-Cancel) semantics internally — the order matches against available liquidity once, and any unfilled remainder is automatically cancelled.

Asset Classes

ClassConstantExamples
CryptoAssetClassCryptoBTC, ETH, tokens
EquityAssetClassEquityCommon stock, preferred stock
DebtAssetClassDebtBonds, convertible notes
FundAssetClassFundLP interests, fund shares
SAFEAssetClassSAFESimple Agreement for Future Equity
CommodityAssetClassCommodityGold, carbon credits
DerivativeAssetClassDerivativeOptions, futures

Compliance Error Handling

All compliance rejections return *lx.ComplianceError with structured error codes:

CodeConstantMeaning
1ErrNotAccreditedInvestor does not meet accreditation requirement
2ErrJurisdictionInvestor jurisdiction blocked
3ErrHoldingPeriodHolding period has not elapsed
4ErrMaxHoldersTrade would exceed maximum holder count
5ErrKYCRequiredKYC verification not completed
6ErrAMLRequiredAML clearance not completed
7ErrMinNotionalOrder value below minimum trade size
8ErrMinSizeOrder quantity below minimum
9ErrMaxSizeOrder quantity exceeds maximum
10ErrROFRPendingRight of first refusal review pending
11ErrBoardApprovalBoard approval required for this transfer
12ErrTransferRestrictedGeneric transfer restriction

Running as a Regulated Venue

US ATS Deployment

  1. Register with SEC as ATS (Form ATS / ATS-N)
  2. Register with FINRA as broker-dealer
  3. Configure EligibilityChecker with accredited investor verification
  4. Configure SettlementHandler with your transfer agent (Carta, Securitize, etc.)
  5. Set MarketConfig per security with appropriate Reg D/A/CF exemption
  6. Enable trade surveillance and regulatory reporting

UK MTF Deployment

  1. Obtain FCA authorization as MTF operator
  2. Configure EligibilityChecker with FCA client categorisation
  3. Set MinInvestorStatus to InvestorSophisticated or InvestorProfessional
  4. Jurisdiction whitelist to UK + Crown Dependencies
  5. Configure SettlementHandler with CREST or your settlement system

Singapore RMO Deployment

  1. Obtain MAS recognition as RMO
  2. Configure EligibilityChecker with MAS investor categorisation
  3. Set MaxHolders: 50 for private placement exemptions
  4. Jurisdiction whitelist to SG or APAC as permitted
  5. Configure SettlementHandler with CDP or your settlement system

Integration with Existing Infrastructure

The compliance layer is designed as a clean interface boundary. LX does not mandate any specific KYC provider, transfer agent, or custody solution. You bring your own:

ComponentInterfaceYou Provide
Investor verificationEligibilityCheckerKYC/AML provider (Jumio, Onfido, Alloy, etc.)
Cap table managementGetHolderCount callbackCarta, Pulley, Shareworks, or on-chain registry
SettlementSettlementHandlerTransfer agent, blockchain, or internal ledger
Custody(separate)MPC wallet, HSM, or qualified custodian

Example: Full Private Securities Market

package main

import (
    "log"
    "time"

    "github.com/luxfi/dex/pkg/dex"
)

func main() {
    // Configure a Reg D 506(c) market for startup equity
    market := lx.NewSecuritiesMarket(lx.MarketConfig{
        Symbol:      "STARTUP-USD",
        AssetClass:  lx.AssetClassEquity,
        Exemption:   lx.ExemptionRegD506c,
        ShareClass:  "Class B Common",
        MinNotional: 10000.0,
        MinSize:     1.0,
        TickSize:    0.01,
        LotSize:     1.0,

        Restrictions: &lx.TransferRestriction{
            MinInvestorStatus:     lx.InvestorAccredited,
            MaxHolders:            500,
            HoldingPeriod:         365 * 24 * time.Hour,
            JurisdictionWhitelist: []string{"US"},
            ROFRRequired:          true,
        },

        EligibilityChecker: newMyChecker(),
        SettlementHandler:  newMySettlement(),
    })

    // Place a sell order (existing shareholder)
    sell := &lx.AdvancedOrder{
        ID: 1, UserID: "seller-001", Symbol: "STARTUP-USD",
        Side: lx.Sell, Type: lx.Limit, Price: 25.00, Size: 1000.0,
    }
    if _, err := market.AddOrder(sell); err != nil {
        log.Printf("Sell rejected: %v", err)
        return
    }

    // Place a buy order (accredited investor)
    buy := &lx.AdvancedOrder{
        ID: 2, UserID: "buyer-001", Symbol: "STARTUP-USD",
        Side: lx.Buy, Type: lx.Limit, Price: 25.00, Size: 400.0,
    }
    trades, err := market.AddOrder(buy)
    if err != nil {
        log.Printf("Buy rejected: %v", err)
        return
    }

    for _, t := range trades {
        log.Printf("Trade: %d shares @ $%.2f", int(t.Size), t.Price)
    }
}

API Reference

See the Go package documentation:

go doc github.com/luxfi/dex/pkg/dex SecuritiesMarket
go doc github.com/luxfi/dex/pkg/dex MarketConfig
go doc github.com/luxfi/dex/pkg/dex StandardEligibilityChecker
go doc github.com/luxfi/dex/pkg/dex TransferRestriction
go doc github.com/luxfi/dex/pkg/dex ComplianceError