Developers

Flo Documentation

Everything you need to go from zero to production. SDK for on-chain primitives, REST API for off-chain data, sandbox parity on both.

01Introduction#overview

What Flo is

Flo is one SDK for tokenized capital markets. One call in TypeScript (Node.js) or Python mints, redeems, supplies, withdraws, borrows against, repays, or bridges tokenized exposure across equities, ETFs, treasuries, bonds, commodities, FX, and private credit. Every token is 1:1 backed by real underlying held by Flo Capital SPC (Cayman) in segregated brokerage accounts at SEC-registered broker-dealers (Interactive Brokers and Alpaca Securities) and issued as a blockchain-based structured note by Flo Global Markets Ltd. (BVI), the issuer entity.

Wallet balance is the unit of account. Every Flo user has one credited Flo wallet balance that all primitives draw from or return to. Capital enters via deposit on any supported source chain and leaves via withdraw on any supported destination chain.

#What ships as an SDK, what ships as REST

On-chain primitive pairs, mint/redeem, supply/withdraw, borrow/repay, plus positions and bridge, ship as typed SDKs in Python and Node.js. Off-chain state, developer-revenue balances, webhook subscriptions, token metadata, KYC records, reconciliation reports, lives only in Flo's database and is exposed over a thin REST API. Same auth, same idempotency guarantees, same sandbox parity.

#Key facts

  • Chains (live): Base, Arbitrum, Ethereum.
  • Settlement currencies: per-chain and per-direction (pay-in vs pay-out), configured by Flo. Fetch the live matrix from /v1/chains rather than hard-coding the set.
  • Prime brokers: Interactive Brokers (primary) and Alpaca Securities (secondary) behind a single routing layer.
  • Legal wrapper: Swiss-law security tokens issued by Flo Global Markets Ltd. (BVI) under an FMA-approved Liechtenstein base prospectus. The BVI issuer is the sole investor in Flo Capital SPC (Cayman), a bankruptcy-remote Segregated Portfolio Company with an independent director that holds the underlying. SEC-registered broker-dealer custody (IBKR + Alpaca, in segregated brokerage accounts in the SPC's name); independent corporate trustee acts as security agent.
  • Latency: p50 < 1s from mint call to on-chain confirmation on L2 routes.
  • Jurisdictions: live outside the United States. US persons are not served at Flo's layer; see Environments and Security.
02Introduction#architecture

Architecture at a glance

Flo sits between your application and global capital markets. Your code calls one SDK; Flo handles tokenization, on-chain settlement, broker execution at Interactive Brokers and Alpaca Securities, NAV updates, and the reconciliation against underlying.

#The stack, top to bottom

  • Your app calls the Flo SDK (TypeScript or Python) or REST.
  • Flo SDK validates the request, applies your fee mark-up policy, and submits to the Flo protocol.
  • Flo protocol on Arbitrum issues tokens, executes mints and redeems, tracks positions and NAV. Arbitrum is only the settlement chain.
  • You don't need to bridge your deposits from other chains to Arbitrum. You can deposit on any supported chain of your choice and Flo takes care of minting your balance on Arbitrum.
  • Custody and execution happen at Interactive Brokers and Alpaca Securities, in segregated accounts in the name of Flo Capital SPC (Cayman). Flo Global Markets Ltd. (BVI) is the issuer.

#A round-trip mint, end to end

  1. Partner calls flo.mint with an asset symbol, notional, and settlement instructions.
  2. Flo locks the notional on-chain, returns an order_id, and routes the order to the broker.
  3. Broker fills the order at the venue.
  4. Flo emits a webhook with the fill and mints the token to the partner's settlement wallet on Arbitrum chain.

The full state machine is documented under Settlement model.

03Introduction#for-whom

Who Flo is for, and who it isn't

Flo is a B2B SDK and API. You are reading these docs because you are a builder integrating Flo into a product that serves end users somewhere. If you want to explore the full potential of Flo SDKs, you can go to GM Markets to see a sample implementation with the Flo SDK.

#Built for

  • Crypto exchanges adding equities, ETFs, fixed income, FX, commodities, or private credit.
  • Crypto wallets that want to offer yield-bearing assets or in-wallet investing.
  • Neobanks and fintechs adding an investing tab.
  • Wealthtech platforms looking for tokenized market access with stablecoin settlement.
  • Onchain applications composing tokenized assets into yield strategies, vaults, or structured products.

#Not for

  • US persons.Flo's offering is non-US only. As a partner, your terms of service must geofence US end users. See Limits, non-US restriction.
  • Retail end-users directly. Flo has no consumer signup, no consumer support, no consumer pricing page. Always integrate via a partner product.

#Who handles what

  • Flo: token issuance, broker execution, custody at IB and Alpaca, smart contracts, on-chain settlement, NAV, reconciliation, regulatory wrapper (FMA-approved base prospectus).
  • You (partner): end-user support, UI, pricing to the end user.
04Quickstart#hello

Hello, Flo, mint and redeem in one script

Copy this file, set two env vars, run it. You get a real on-chain mint and a real on-chain redeem against sandbox in under two minutes. No prior reading required.

#1. Install the SDK

pip install flo-sdk

#2. Set two env vars

bashexport FLO_API_KEY="sk_test_..."        # from /dashboard/developers/keys
export FLO_WALLET="0xYourEvmWallet..."    # any EVM address you control

#3. Run the script

import os, time, flo

client = flo.Client(api_key=os.environ["FLO_API_KEY"], env="sandbox")
WALLET = os.environ["FLO_WALLET"]

# 1. Mint 10 shares of AAPL, paid in USDF on Base.
mint = client.mint(
    asset="AAPL",
    quantity=10,
    settlement={"currency": "USDF", "chain": 42161, "wallet": WALLET},
)
print("mint  :", mint.id, mint.status, mint.settlement["create_order_tx_hash"])

# 2. Wait for the broker leg to settle. In production, subscribe to the
#    mint.settled webhook (or poll client.mints.retrieve(mint.id)) instead
#    of sleeping.
time.sleep(120)

# 3. Redeem the same 10 shares back to USDF on Base.
redeem = client.redeem(
    asset="AAPL",
    chain=42161,
    quantity=10,
    source_wallet=WALLET,
    payout={"currency": "USDF", "chain": 42161, "wallet": WALLET},
)
print("redeem:", redeem.id, redeem.status)

Both SDK calls return a structured object. The script prints a line from each:

json// mint = client.mint(...)  →
{
  "id": "mnt_0a4921pQ9mR3nV",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "AAPL",
  "quantity": "10.000000",
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0xYourEvmWallet...",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

// redeem = client.redeem(...)  →  (returns once the lock + create-order tx confirms)
{
  "id": "rdm_7c2f55kT1xW8nQ",
  "object": "redeem",
  "status": "active",
  "order_id": "0x2b9d40af71c6...",
  "asset": "AAPL",
  "quantity": "10.000000",
  "payout": { "currency": "USDF", "chain": 42161, "wallet": "0xYourEvmWallet..." },
  "created_at": "2026-05-15T14:35:04Z"
}
That is the entire round trip. Sandbox runs on the Arbitrum Sepolia test network with $1M of synthetic USD seeded to your wallet, so the script costs you nothing. Swap sk_test_… for sk_live_… and env: "live" after partner KYB and the same eight lines run against a real prime-broker fill.
The two-minute sleep is for copy-paste convenience only. Real integrations subscribe to mint.settled and redeem.settled webhooks, or poll client.mints.retrieve(id) / client.redeems.retrieve(id). Settlement timing depends on broker hours and the recovery-window class of the asset.
05Quickstart#quickstart

Quickstart, your first mint in 10 minutes

Zero-to-mint on sandbox in under ten minutes. You need a wallet address (EVM), a terminal, and one of Python 3.10+ or Node 18+.

#1. Grab a sandbox key

In the dashboard, head to Developers → API keys, click Create key, pick environment Sandbox, scope Full access, and copy the sk_test_…secret. Sandbox keys don't require KYC.

#2. Install the SDK

pip install flo-sdk

#3. Fund the Flo wallet

Trading on Flo draws from a credited Flo wallet balance, not from a raw stablecoin transfer attached to each mint. Deposit USDC or USDT from any supported source chain; the credit lands on Arbitrum after the CCIP message executes. In sandbox, deposit on the Arbitrum Sepolia chain for instant credit.

deposit = client.wallet.deposit(
    amount_usd=1000,
    source={"currency": "USDC", "chain": 8453, "wallet": "0x4a8B...3c9D"},
)
# Wait for the wallet.deposit.delivered webhook (or poll), then proceed.
json{
  "id": "wdp_5kR2mN8pQ1",
  "object": "wallet_deposit",
  "status": "queued",
  "amount_usd": "1000.00",
  "source": { "currency": "USDC", "chain": 8453, "wallet": "0x4a8B...3c9D" },
  "credit": { "chain": 42161, "wallet": "0x4a8B...3c9D" },
  "ccip_message_id": "0x9f21...c30a",
  "created_at": "2026-05-15T14:33:02Z"
}

#4. Mint 10 shares of AAPL

import flo

client = flo.Client(api_key="sk_test_...", env="sandbox")

position = client.mint(
    asset="AAPL",
    quantity=10,
    settlement={"currency": "USDF", "chain": 42161},
)
print(position.id, position.tx_hash)
json{
  "id": "mnt_0a4921pQ9mR3nV",
  "object": "mint",
  "status": "active",
  "asset": "AAPL",
  "quantity": "10.000000",
  "tx_hash": "0xa3b1...9e4c",
  "settlement": { "currency": "USDF", "chain": 42161 },
  "created_at": "2026-05-15T14:33:02Z"
}
Sandbox tokens live on an isolated test network. The mint confirms onchain, but no real shares are held and no real balances move. Go to Sandbox for the full test matrix.

#5. Ship to live

Complete KYC in the dashboard. Once approved, the Live toggle unlocks in the topbar. Swap sk_test_… for sk_live_… and env: "live", and the same call runs against a real prime-broker fill.

06Quickstart#sdks

SDKs, Python and Node.js

Flo ships two first-party SDKs. Both wrap every on-chain primitive, handle retries, pagination, and idempotency, and expose typed models generated from the same OpenAPI spec that backs the REST surface.

Need an SDK in a language we do not ship yet? Tell us what you are building. Join our Telegram group or email engineering@flo.finance.

#Installation

pip install flo-sdk

#Initializing the client

import flo

client = flo.Client(
    api_key="sk_live_...",
    env="live",            # "sandbox" or "live"
    timeout=10.0,          # seconds; per-request, clamped to server limit
    max_retries=3,         # on 429 / 5xx with full jitter
)

#Versioning

SDKs follow semver. Breaking changes land on a major. The SDK version is pinned in pyproject.toml / package.json; runtime version is also echoed on the User-Agent header for server-side analytics.

#Type generation

Every resource (Mint, Redeem, Supply, Withdraw, Borrow, Repay, Bridge, Position) is typed end-to-end. In TypeScript, errors narrow by code:

typescripttry {
  await flo.mint({ asset: "AAPL", quantity: 10, settlement: { currency: "USDF", chain: 42161, wallet: "0x..." } });
} catch (err) {
  if (err instanceof Flo.InsufficientFunds) {
    // type-narrowed: err.required_usd, err.available_usd, err.shortfall_usd
  } else if (err instanceof Flo.MarketClosedNonFractional) {
    // type-narrowed: err.next_session_at
  } else if (err instanceof Flo.RateLimited) {
    // retry_after_seconds
  } else {
    throw err;
  }
}
07Quickstart#sandbox

Sandbox, test without partner KYB

Sandbox is a full-fidelity copy of the production surface running on the Arbitrum Sepolia test network. Same endpoints, same response shapes, same webhook events. Every Flo account gets unlimited sandbox access without partner KYC/KYB.

#Base URLs

  • Production: https://api.flo.finance
  • Sandbox: https://sandbox.flo.finance

#Sandbox-only conveniences

  • Time travel: the X-Flo-Clock header lets you advance sandbox-wall-clock up to 30 days forward on a single request. Useful for testing yield accrual, corporate actions, and webhook replay.
  • Error injection: set X-Flo-Error: ltv_exceeded (or any documented error code) on a mutating request to deterministically trigger that failure.
  • Idempotent resets: POST /v1/sandbox/reset wipes your sandbox state back to a clean slate. No production analog.
  • Seeded assets: sandbox carries the same 8,000-ticker universe with synthetic price feeds. Every ticker has a ~$100 nominal baseline to simplify test arithmetic.

#Sandbox funding

New sandbox tenants spin up with $1,000,000 of synthetic USD at their sandbox settlement address. Drain it by minting, refill any time with POST /v1/sandbox/faucet.

#Helper examples

# Sandbox helpers are only on the sandbox client.
client = flo.Client(api_key=SANDBOX_KEY, env="sandbox")

# Time travel, request-scoped header:
pos = client.positions.get("AAPL", wallet="0x...", headers={"X-Flo-Clock": "+7d"})

# Error injection:
try:
    client.mint(
        asset="AAPL",
        quantity=10,
        settlement={"currency": "USDF", "chain": 42161, "wallet": "0x..."},
        headers={"X-Flo-Error": "insufficient_funds"},
    )
except flo.InsufficientFunds as e:
    print(e.available_usd, e.required_usd)

# Reset + faucet:
client.sandbox.reset()
client.sandbox.faucet(currency="USDF", amount=500_000)

#Helper responses

The sandbox returns the same response shapes as production. The error-injection call fails deterministically, so your handler runs against a real error body.

json// GET /v1/positions/AAPL  ·  X-Flo-Clock: +7d  →  clock advanced 7 days
{
  "object": "position",
  "token": "AAPL",
  "wallet": "0x...",
  "quantity": "10.000000",
  "nav": { "value": "1.000000", "unit": "share", "as_of": "2026-05-22T14:33:02Z", "stale": false },
  "yield": { "gross_apy_bps": 0, "net_apy_bps": 0 },
  "attestation_url": "https://attest.flo.finance/sandbox/ATT-AAPL.pdf"
}

// POST /v1/mint  ·  X-Flo-Error: insufficient_funds  →  402, deterministic failure
{
  "error": {
    "type": "insufficient_funds",
    "message": "Settlement wallet holds less USDF than the mint requires.",
    "available_usd": "0.00",
    "required_usd": "1000.00",
    "request_id": "req_sbx_7Hk2p"
  }
}

// POST /v1/sandbox/reset  →  tenant state wiped to a clean slate
{ "object": "sandbox.reset", "status": "ok", "reset_at": "2026-05-15T14:33:02Z" }

// POST /v1/sandbox/faucet  →  synthetic USDF credited
{
  "object": "sandbox.faucet",
  "currency": "USDF",
  "amount": "500000.00",
  "new_balance_usd": "1000000.00",
  "credited_at": "2026-05-15T14:33:02Z"
}
08Quickstart#authentication

Authentication

Every call, SDK or REST, carries a Bearer token in the Authorization header. The SDK sets this automatically from your api_key; for raw REST, attach it yourself.

import os, uuid, httpx

r = httpx.get(
    "https://api.flo.finance/v1/tokens",
    headers={
        "Authorization": f"Bearer {os.environ['FLO_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=10.0,
)
r.raise_for_status()
tokens = r.json()

#Response

A valid key returns 200 with the catalog payload. A missing, malformed, or revoked key returns 401 and never reaches the resource.

json// 200 OK
{
  "object": "list",
  "data": [
    { "object": "token", "ticker": "AAPL", "name": "Apple Inc.", "supply": "18421.084523", "nav": { "value": "1.000000", "unit": "share" } },
    { "object": "token", "ticker": "SPY", "name": "SPDR S&P 500 ETF Trust", "supply": "9842.551200", "nav": { "value": "1.000000", "unit": "share" } }
  ],
  "has_more": true
}

// 401 Unauthorized: bad or missing Bearer token
{
  "error": {
    "type": "unauthorized",
    "message": "API key is missing, malformed, or revoked.",
    "request_id": "req_4Tg9w2"
  }
}

#Key shapes

  • sk_live_…, production. Requires partner org-level KYC/KYB approval (not end-user KYC).
  • sk_test_…, sandbox. No partner KYC/KYB needed. Unlimited, safe to commit to git in test-fixture files.

#Scopes

  • full, mint, redeem, borrow, bridge, and every read. Required for mutating operations.
  • read, GET only. Use for dashboards, BI jobs, and log shippers.

#Rotation

Rotate from Developers → API keys. Rotation is atomic, the old key continues to work for five minutes while your deployment rolls forward. Revocation is immediate. We may monitor GitHub, npm, and pastebin for leaked prefixes and auto-revoke on detection.

#IP allowlisting

Available on request for partners with elevated security requirements. Attach an IPv4 CIDR or IPv6 prefix to a key; requests from outside the allowlist return 403 ip_not_allowed without consuming rate-limit budget.

09Quickstart#environments

Environments, Sandbox vs Live

Every Flo account has two environments. Sandbox is always available. Live unlocks only after partner org-level KYC/KYB is approved (not end-user KYC, Flo never gates end-user wallets). Keys, webhooks, IDs, and settlement balances are fully separate between the two, sandbox state can never leak into live and vice-versa.

FieldTypeDescription
Base URLstringhttps://api.flo.finance vs https://sandbox.flo.finance
Key prefixstringsk_live_ vs sk_test_
SettlementbooleanReal prime-broker fills vs synthetic test network
Partner KYC/KYB requiredbooleanYes for live · No for sandbox. Org-level only; end-user wallets are not gated by Flo.
Rate limitsdefault + headroom1,000 RPS default in live · effectively unlimited in sandbox. See Rate limits.
WebhooksseparateSubscribe per-environment; events don't cross over
The dashboard topbar toggle enforces this server-side too. A sk_test_… key will return 401 key_lacks_live_scope against api.flo.finance, and vice-versa.
10Quickstart#faq

FAQ

Short answers to the questions we hear most. Everything here is linked to the deeper treatment in the rest of the doc, jump straight there when the one-liner isn't enough.

#What does Flo actually do?

One SDK call mints, redeems, supplies, withdraws, borrows against, repays, or bridges tokenized exposure to 8,000+ public-market instruments across stocks, treasury yield, commodities, FX, ETFs, fixed income, and private credit. Every token is 1:1 backed by real shares held by Flo Capital SPC (Cayman) in segregated brokerage accounts at SEC-registered broker-dealers (Interactive Brokers and Alpaca Securities) and issued as blockchain-based structured notes by Flo Global Markets Ltd. (BVI). See What Flo is.

#SDK or REST?

On-chain primitive pairs, mint/redeem, supply/withdraw, borrow/repay, plus positions and bridge, ship as SDKs in Python and Node.js. Off-chain state (developer-revenue balances, webhook subscriptions, token metadata, customers, reconciliation) is exposed over a thin REST API. Same auth, same idempotency guarantees, same sandbox parity.

#Do I need KYC to start?

No, sandbox is always open with sk_test_… keys and $1M of synthetic USD. Live mode unlocks once your org-level KYB is approved in the dashboard. See Environments.

#Does Flo run KYC on my end users?

No. Flo is not a KYC or KYB vendor, you keep your own vendor relationship (Jumio, Onfido, Persona, Sumsub, Middesk, Alloy, Veriff, or in-house). Posting the resulting attestation to Flo's free attestation vaultis completely optional. If you do, we'll tag it to the customer's wallet so auditors have one place to look.

#Which chains are live, and how are token contracts deployed?

Issuance lives on one chain. Token contracts are deployed only on Arbitrum, the canonical issuance chain, and Flo preemptively deploys the top 100 tickers there at launch, so for those assets minting works immediately with no setup. For long-tail assets, the factory is permissionless: any wallet can call client.tokens.predeploy(asset) and pay the one-time deploy gas. CREATE2 keeps the address deterministic, so the contract is shared by every partner and every user once it's live. Base, Ethereum, and the other supported chains handle stablecoin deposits and withdrawals and hold bridged tokens; they are not issuance chains. See the Mint section for the full mechanic.

#What settlement currencies are supported?

Trades settle in USDF, the Flo wallet-balance currency. Every mint, redeem, supply, borrow, and repay is denominated in USDF drawn from or returned to the Flo wallet balance. USDC and USDT are the stablecoins used to fund the wallet (deposit) and cash out (withdraw); the accepted set is configured per chain. Fetch /v1/chains and read pay_in_currencies and pay_out_currencies for the chain you want to use.

#How does pricing work?

Flo charges you nothing per transaction. You set the price your user pays on every mint, redeem, and borrow, and you keep the markup. The developer revenue primitive ledgers it to a wallet you control and settles to you in USDF.

#Is the US supported?

No. The United States is on the restricted-jurisdictions list. The interface and API are not offered to US persons. See Security.

#Are Flo tokens permissionless?

Primary minting and redemption are gated. Secondary transfers are permissionless under EU/EEA law.

#How does settlement work?

Every primitive (mint, redeem, supply, withdraw, borrow, repay) is asynchronous. The SDK call submits one on-chain tx that atomically pulls or burns the user's assets and creates an on-chain order_id. The terminal action (live token mint, stablecoin payout, lending-vault share mint, loan disbursement) happens in a separate tx via settle(order_id) after the broker / pool leg confirms. There is no inline settlement path. Subscribe to the *.settled webhook for the terminal state.

#What happens if the broker leg fails?

Flo's keeper calls cancel(order_id) on chain. The order is nullified and the held stablecoin (or burned tokens, or locked collateral, depending on primitive) is returned to the source wallet in the same tx. No live tokens are ever minted without the broker leg confirming.

#Can I recover my stablecoin if Flo's keeper goes down?

Yes. Each order has a per-asset recovery window (default 4h for sync-fillable assets, 48h for async-fillable). After the window, the source wallet can call orderContract.user_cancel(order_id)directly to nullify the order and recover the held assets. No trust assumption on Flo's keeper running.

#Is there a reorg or double-spend risk?

No, by construction. The create-order tx does transferFrom and createOrder atomically. settle(order_id)reads the order's on-chain status before doing anything; if the order isn't ACTIVE on canonical chain state, settle reverts. Because settle depends on storage written by the create-order tx, no reorg can leave Flo with a settled mint and a missing payment. The double-spend window is closed at the contract level.

#What happens if Flo shuts down?

Tokens remain redeemable through the BVI issuer (Flo Global Markets Ltd.) and the Cayman SPC (Flo Capital SPC), with an independent corporate trustee acting as security agent. Holder claims are against the issuer and the SPC, not any Flo operating entity. See Issuer + SPC structure.

#How do I test webhooks locally?

Three options: the flo webhooks listen CLI tunnel, the dashboard's Send test delivery button, or ngrok + a dedicated subscription. Full walkthrough at Webhooks.

#Where do I report a security issue?

security@flo.finance, up to $250K for critical findings under the Flo-operated bug bounty program. Details in Support.

Token catalogue overview

Flo issues a single catalogue of wrapped tokens that spans seven asset classes: equities, ETFs, treasuries, bonds, commodities, FX, and private credit. Each token is a claim on an underlying instrument held by Flo Capital SPC (Cayman) at one of two SEC-registered broker-dealers, Interactive Brokers and Alpaca Securities, under a base prospectus approved by the Liechtenstein Financial Market Authority.

The complete, browseable catalogue lives at /tokens. Each entry has a dedicated reference page with the wrapped symbol, underlying ticker, ISIN where applicable, custodian, supported chains, distribution treatment, attestation cadence, and a one-call mint snippet.

One issuer, one prospectus, one Cayman SPC, one broker-dealer chain, and one set of smart contracts back the entire catalogue. Adding a class never means a new prospectus, a new SPC, or a new audit pass.

Wrapper brand convention

Every Flo-issued token uses a lowercase f suffix on the underlying ticker. Examples:AAPLf, SPYf, BILL3Mf, GOLDf, EURUSDf.

The convention is asset-first by design. The underlying ticker stays at the head of the symbol so a user scanning a wallet, an exchange listing, or a block explorer recognises the asset before the wrapper. Search-as-you-type for “AAPL” surfaces AAPLf immediately, which would not be true for a prefix form.

Always derive the wrapped symbol with the wrap() helper in lib/constants/tokens.ts rather than hand-writing it. Partner UIs may relabel Flo tokens to fit their own brand; the on-chain canonical symbol is always the f-suffix form.

How a Flo token resolves to underlying

Every Flo-issued token follows the same four-step resolution path from on-chain claim to off-chain underlying.

  1. Issuance. Flo Global Markets Ltd. (BVI) issues the token under the Liechtenstein FMA-approved base prospectus, EEA-passported across 30 states.
  2. Custody. The underlying instrument is held by Flo Capital SPC (Cayman) in a segregated brokerage account at Interactive Brokers or Alpaca Securities, both SEC-registered broker-dealers.
  3. Settlement. The wrapped token is issued and settles on Arbitrum, the canonical issuance chain. From there it can be bridged to other supported chains through Chainlink CCIP.
  4. Attestation. Reserves are attested at the cadence shown on the per-token reference page by an independent auditor and published on the Flo transparency surface.

Reading a token reference page

Every page under /tokens/[symbol] carries the same six sections so an integrator can scan any two tokens against each other in seconds.

  • Hero. Wrapped symbol, underlying instrument name, asset class, short pitch.
  • Key facts. Wrapped symbol, underlying ticker, ISIN where applicable, custodian, chains, distribution treatment, attestation cadence, issuer, asset SPC, prospectus reference.
  • How it resolves. The four-step issuance, custody, settlement, and attestation flow specific to that token.
  • Mint snippet. A working POST /v1/mint example using the underlying ticker and the first supported chain.
  • Venues. Live partners that distribute the token, plus a placeholder for partners in build.
  • Audits and prospectus. Cross-links to the audit reports and the base prospectus on the transparency surface.
  • Risks. Market risk of the underlying plus structural risks common to issuer-wrapped tokens.

Per-token reference index

The complete, filterable catalogue lives at /tokens. Open any card for the full reference page. The index is the authoritative list; this documentation page is a pointer.

Catalogue contents are derived directly from the five market roster files in db/markets/*. Every instrument exposed under /markets is present at /tokens as a Flo-issued wrapped token, and every wrapped token at /tokens links back to its underlying market reference. The two surfaces are mirror images by construction; they cannot drift.

Adding a brand-new instrument: append a row to the appropriate file in db/markets/*. The token entry, the per-token page, and the sitemap entry materialise on the next build. To add curated detail (ISIN, hand-written short pitch) for a head-of-distribution name, extend the OVERRIDES map in db/tokens/catalogue.ts.

Settlement model, asynchronous by design

Every Flo primitive that touches the broker leg is asynchronous. You submit a call; Flo returns anorder_id immediately. Settlement happens when the broker leg clears. The timing of settlement depends on the asset class.

#The state machine

  • submitted, accepted by Flo, locked on-chain, pending broker.
  • committed, broker fill received, on-chain mint queued.
  • settled, token minted to your settlement wallet on Arbitrum, NAV updated.
  • cancelled, the order was cancelled before fill; locked notional and any fee are refunded.
  • failed, broker or downstream rejection; locked notional and any fee are refunded with a failure code.

#Per-asset-class timing

Settlement timing follows the underlying market. Flo never claims to compress the broker leg; it does give you 24/7 on-ramp and off-ramp in stablecoins on both sides of the broker.

  • US equities: typically T+1.
  • Non-US equities and ETFs: typically T+2.
  • Treasuries and bonds: typically T+1.
  • FX: 24/5 during the FX session; weekend orders queue for Monday open.
  • Commodities: per the underlying contract.
  • Private credit: monthly redemption windows, NAV per the fund cadence.

Subscribe to webhooks (REST webhooks) to be notified on every state transition.

17Concepts#networks

Networks, settlement, bridging, capital flows

Arbitrum is the only chain where issuance, NAV, and trade execution happen. Every Flo token is born on Arbitrum. From there, tokens can be bridged to and held on other chains, and stablecoin capital can be deposited and withdrawn from other chains. Every cross-chain hop runs over Chainlink CCIP. This split keeps the trade engine on one chain while letting users hold and fund from wherever they already are.

#Settlement chain

  • Arbitrum. Token issuance, NAV updates, trade execution. All Flo wrapped tokens (AAPLf, SPYf, BILL3Mf, pCREDf, etc.) are ERC-20s issued on Arbitrum.

#Where tokens can live

Every token is minted on Arbitrum. To hold it on another chain, move it there afterward with bridge. Token-bridge routes are currently a subset of the stablecoin deposit chains below; see the bridge route matrix for the live set.

#Stablecoin deposit and withdrawal chains

Stablecoin capital flows in and out from more chains than tokens bridge across. The live set is in Supported chains and stablecoins; today it covers Base, Ethereum, Arbitrum, Polygon, and Optimism. The canonical machine-readable source is GET /v1/chains, fetch from there at request time rather than hard-coding.

See SDK · Bridge for moving tokens, SDK · Deposit and SDK · Withdraw for the on-ramp and off-ramp flows, and Security · Chainlink CCIP for the trust model.

Permissionlessness, KYB at Flo, KYC at the partner

Flo's token contracts have no on-chain allowlist. There is no per-end-user gating at the token layer. Mint, redeem, supply, withdraw, borrow, and repay are permissionless on-chain operations.

The compliance perimeter lives one layer up. Flo performs KYB on you, the partner. You perform KYC on your end users and ensure they sit outside the US. Flo does not see your end users.

#Implications

  • You can call any Flo primitive from any wallet address. There is no Flo-side address-level access control.
  • Geofencing US end users is your obligation under your partner agreement with Flo.
  • Once a token is minted, it is a standard ERC-20. It can be held, transferred, or composed into other on-chain systems freely.

Wallet balance, the unit of account for trading

Every Flo user has one credited Flo wallet balance that all on-chain primitives read from or write to. Mint, supply, and borrow draw notional from this balance. Redeem, supply-withdraw, and repay return into it. The balance is denominated in USD; deposits and withdrawals on the user side happen in USDC or USDT on any supported chain.

Where the balance lives.Flo wallet balances are held on Arbitrum. The user can deposit from any supported source chain and withdraw to any supported destination chain. The source-to-Arbitrum hop is a Chainlink CCIP message, not a token bridge; Flo does not move the user's stablecoin between chains.

#End-to-end flow

  1. User sends USDC or USDT to the Flo deposit contract on a supported source chain.
  2. A Chainlink CCIP message dispatches from the source chain to Arbitrum. Once it executes, the user's Flo wallet balance is credited 1:1.
  3. Asynchronously, Flo off-ramps the collected stablecoin to fiat and parks it in the prime-broker account that funds trades. This treasury leg is decoupled from the user-facing credit.
  4. Trade. Every primitive draws notional from, or returns notional to, the wallet balance.
  5. On withdraw, the wallet balance debits on Arbitrum. Flo on-ramps fiat from the broker account into the user's chosen stablecoin and pays out on the chosen destination chain.

#Key facts

  • Denomination: USD, accurate to six decimals on reconciliation reports.
  • Inbound assets: USDC and USDT. The accepted set per chain comes from Supported chains and stablecoins and from the live Chains catalog.
  • Deposit credit timing: source-chain finality plus CCIP execution. No fixed ETA. See Deposit for the honest framing.
  • Withdraw timing: up to 24 hours, liquidity-bound. No fixed ETA. See Withdraw.
  • Costs. Source-chain gas plus CCIP message fee on deposit, on-ramp and off-ramp fees plus destination-chain gas on withdraw. Flo does not skim.
  • Not the bridge primitive. Bridge moves tokenized assets between chains via CCIP. Wallet deposit and withdraw move stablecoin capital in and out of Flo and are separate.
20SDK guides · Mint and redeem#mint

Mint, tokenize any asset

mintis notional-in: the partner sends a USD amount and an acceptable slippage; the SDK derives the minimum quantity that must be filled and submits the order. The notional is drawn from the user's Flo wallet balance, not from a raw stablecoin transfer attached to the call. Fund the wallet first via wallet.deposit. The call is asynchronous via an on-chain order_id model and runs in two on-chain steps. Reorg-safe by construction: settlereads the order's on-chain status before minting, so no live token can ever land without the wallet balance having been debited in canonical chain state.

Two on-chain steps. Step 1. Partner submits the SDK call. The contract atomically debits notional_usd from the user's Flo wallet balance and creates an order at order_id. Webhook: mint.created (or mint.queuedif the broker leg can't be opened immediately). Step 2.Once the broker leg confirms with Interactive Brokers or Alpaca Securities, Flo's keeper calls settle(order_id). The live Flo token is minted to settlement.wallet in the same transaction. Webhook: mint.settled. The broker leg itself is off-chain and consumes no gas.
# Notional-in. The SDK derives min_quantity from the live quote and slippage_bps.
position = client.mint(
    asset="AAPL",
    notional_usd=1000,
    slippage_bps=50,  # 0.50 percent. Default if omitted.
    settlement={
        "currency": "USDF",
        "chain": 42161,  # Arbitrum. EIP-155 integer ID; see /docs#rest-chains.
        "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    },
    gas={"sponsor": "developer"},  # see /docs#gas
    developer_fee={"amount_bps": 25, "amount_flat_usd": 0.50, "destination": "0x9f2c..."},
    idempotency_key="mint_2026_04_21_0001",
)

#Fee math

From the submitted notional_usd, the SDK first deducts the partner's developer_fee (bps and flat components are additive). Then, if gas.sponsor is user_notional, the gas budget is deducted as well. The remainder is the amount that buys the asset:

textnet_for_purchase = notional_usd
                   - developer_fee_usd
                   - gas_usd                   # only if gas.sponsor = "user_notional"

min_quantity     = net_for_purchase / (quote * (1 + slippage_bps / 10_000))

If the broker leg cannot fill at or above min_quantity, the order does not fail. It stays in active (pending) until the price moves back into the acceptable band, until the partner cancels via orders.cancel (see Cancellation), or until the user recovery window elapses. See the canonical fee math for the same statement referenced from every order section.

#Parameters

FieldTypeDescription
assetrequiredstringTicker, e.g. AAPL, NVDA, SPY, TLT. See /v1/tokens for the full list.
notional_usdrequirednumberUSDF amount the user is paying in. Developer fee, plus gas if gas.sponsor is user_notional, are deducted from this amount before the asset is bought.
slippage_bpsintegerMaximum acceptable price drift, in basis points. Default 50 (0.50 percent). Sets min_quantity via the formula above. If the broker can't fill at or above min_quantity, the order stays active until fillable or cancelled. Below the dust floor at 10⁻⁶ shares the call returns slippage_too_tight.
settlement.currencyrequiredstringAlways USDF, the Flo wallet-balance currency. Mint draws notional from the user's Flo wallet balance in USDF; fund it first via wallet.deposit. The external stablecoins (USDC, USDT) apply only at the deposit and withdraw edges.
settlement.chainrequiredinteger (EIP-155 chain ID)Issuance chain for the minted token. Always Arbitrum, 42161: Flo tokens are issued only on Arbitrum. Pass the integer EIP-155 chain ID; string slugs are not accepted. To hold the token on another chain, bridge it after minting. See Chains catalog.
settlement.walletrequiredaddressDestination EVM address. Token contracts are permissionless: no on-chain wallet allowlist. Whatever onboarding posture the partner runs at their own layer is the partner's call; Flo doesn't gate the mint on it.
gasobjectGas-fee handling. { sponsor: "developer" | "user_notional", max_gas_usd?: number }. Default developer. See Gas for the full contract.
developer_feeobjectOptional partner markup. Provide amount_bps, amount_flat_usd, or both (additive). See Developer fees.
idempotency_keystringDedup window 24 h. SDK auto-generates a UUIDv4 if omitted.
Quantity-in legacy mode. Partners already integrated against the original quantity-in shape can keep using client.mints.create_by_quantity(...) (Python) / flo.mints.createByQuantity(...) (Node). Same lifecycle, same response shape; the SDK pins notional_usd from the live quote on submission. Stable, not deprecated.

#Precision and granularity

Two different precision floors apply, and they aren't the same number:

  • On-chain token representation: every Flo ERC-20 uses decimals = 18, so a balance can be expressed down to 10⁻¹⁸ of a share at the contract level.
  • Mint / redeem fill granularity: capped at 10⁻⁶ shares because that's the broker fill size at Interactive Brokers and Alpaca. Quantities passed to mint or redeembelow 10⁻⁶ are rejected client-side; the broker leg can't be filled smaller.

Practically: write user balances and NAV math against the 18-decimal contract value, but quote 10⁻⁶ as the smallest amount your user can buy or sell. Reconciliation reports diff to 0.000000 (six decimals) for the same reason.

#Response

The response returns the moment the create-order tx confirms (Step 1). Settlement arrives via webhook (mint.settled) or by polling client.mints.retrieve(id). Fields with null populate on settle.

json{
  "id": "mnt_0a4921pQ9mR3nV",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "AAPL",
  "notional_usd": "1000.00",
  "slippage_bps": 50,
  "developer_fee_usd": "2.50",
  "gas_usd": "0.00",
  "net_for_purchase_usd": "997.50",
  "quote_at_submission": "184.21",
  "min_quantity": "5.388291",
  "executed_quantity": null,
  "executed_price": null,
  "contract": {
    "address": "0x7c1f...aE09"
  },
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "gas": { "sponsor": "developer", "max_gas_usd": null },
  "created_at": "2026-04-21T14:33:02Z"
}

#Chain activation

Flo runs a uniformly permissionless ERC-20 factory contract on Arbitrum, the canonical issuance chain. Every deploy is paid by msg.sender directly to the chain. There is no Flo-funded relayer pathway. The two tiers below differ only in who ends up being msg.sender.

Top 100 tickers, preinstalled. Flo preemptively deploys the most-traded tickers on Arbitrum at launch from a Flo-controlled wallet, paying gas like any other caller. For these assets, partners and users do nothing. Minting works immediately. The current preinstalled list is queryable at /v1/tokens?preinstalled=true.

Long-tail assets, on-demand deploy. For anything outside the top 100, the contract deploys on demand on Arbitrum. Any wallet can call client.tokens.predeploy(asset), paying the one-time deploy gas directly to Arbitrum. CREATE2 keeps the address deterministic. Whether you, another partner, or any wallet on chain triggers the deploy, the resulting contract sits at the same address, and every partner and every user mints to that shared contract afterward.

  • predeploy is idempotent. Calling it on an already-deployed pair returns the existing address as a no-op. Safe to run from CI as a precondition step.
  • No allowlist on who can deploy. CREATE2 plus Flo-controlled bytecode means the contract is canonical regardless of deployer; the deployer is just paying gas to bring the canonical contract on chain.
  • Anti-abuse by economic gravity. Every deploy is paid by msg.sender, including the top 100 done from Flo's own wallet. Flo never funds a relayer that an attacker could trigger to burn protocol gas on speculative tickers. Speculative deploys cost the deployer.
  • Typical deploy gas on Arbitrum: ~180k gas.
  • A tokens.deployed webhook fires the first time each contract lands on Arbitrum.
# Long-tail asset only. Top 100 are already deployed.
# Anyone can call this. There is no Flo allowlist. msg.sender pays gas.
tx = client.tokens.predeploy(asset="OBSCURE_TICKER")  # deploys on Arbitrum
signed = wallet.sign_transaction(tx)
receipt = client.tokens.broadcast(signed)
print(receipt.contract_address)  # deterministic CREATE2 address
json// receipt = client.tokens.broadcast(signed)
{
  "object": "token_deploy_receipt",
  "asset": "OBSCURE_TICKER",
  "symbol": "OBSCURE_TICKERf",
  "chain": 42161,
  "contract_address": "0x7c1f9b40Ee2Aa3c5d9F1b07cC2390a4471eaE09",
  "create2": true,
  "already_deployed": false,
  "status": "deployed",
  "tx_hash": "0xa3b1...9e4c",
  "gas_used": 179842,
  "deployed_at": "2026-05-15T14:33:02Z"
}

predeploy is idempotent: calling it on an already-deployed pair returns the same contract_address with already_deployed: true and gas_used: 0.

#Order lifecycle

Every mint is asynchronous and is governed by an on-chain order_id. The state of the order is contract storage. Settlement reads it before minting the live token, which is what closes the reorg / double-spend window structurally rather than statistically.

FieldTypeDescription
queuedintermediateCreate-order tx confirmed; stablecoin held in the order contract. The broker leg can't open right now (out-of-session, or insufficient liquidity). The order auto-promotes to active when the venue is fillable. Response carries queue_reason and an estimated_fillable_at (next-session open, or null if liquidity-bound). Webhook: mint.queued.
activeintermediateBroker leg open; waiting on a fill at or above min_quantity. Stablecoin held in the order contract. Webhook: mint.created.
settledterminalBroker leg confirmed. Flo's keeper called settle(order_id) on chain; the order is nullified and the live Flo token is minted to settlement.wallet in the same tx. executed_quantity and executed_price populate. Webhook: mint.settled.
cancelledterminalPartner cancelled via orders.cancel, broker rejected the fill, or the user-recovery window elapsed. The order is nullified and the stablecoin is refunded to the source wallet, including the previously-deducted developer_fee_usd (reversed). Webhook: mint.cancelled with cancel_reason.

Every state carries forward the original mint.id (mnt_…) and the on-chain order_id, so you can poll client.mints.retrieve(id) at any time, or subscribe to the webhook lane for push updates. Any developer_fee accrual is reversed automatically on cancelled.

#Order recovery (user-side)

If Flo's keeper does not settle or cancel within the per-asset recovery window, the source wallet can call orderContract.user_cancel(order_id)directly to nullify the order and pull the stablecoin back. This is an on-chain escape hatch. No trust assumption that Flo's keeper is running. Default windows: 4 hours for sync-fillable assets (US equities core hours), 48 hours for async-fillable assets (after-hours, fixed income, private credit). Per-asset values exposed at /v1/tokens/{asset} under recovery_window_seconds.

#Why this is reorg-safe

The create-order transaction does transferFrom(user → Flo) and createOrder(order_id, ...) atomically. settle(order_id)reads the order's on-chain status before minting; if the order is not ACTIVE in canonical chain state, settle reverts. A chain reorg that drops the create-order block also drops every block built on it (including the settle block), so both txs go back to the mempool together. If the create-order tx is replaced and never re-mines, the order never exists on canonical chain and the settle tx will keep reverting; no live token is ever minted without the stablecoin having been received. The double-spend window is closed at the contract level, not by waiting for finality.

#Pricing

Flo charges nothing on mint and nothing on redeem. The partner sets the price their user pays. Pass a developer_fee on the mint call and keep the markup. The fee is deducted at the front of the fee math (before the asset purchase) and is reversed on cancelled. See developer revenue for the canonical math and Gas for the two gas-sponsorship modes.

#Errors

  • asset_not_found, ticker not in the universe or delisted mid-session.
  • insufficient_funds, source wallet balance doesn't cover notional_usd, including the deducted developer_fee and any gas when sponsor is user_notional.
  • slippage_too_tight, the fee-and-slippage math would set min_quantity below the 10⁻⁶ dust floor. Loosen slippage_bps or raise notional_usd.
  • gas_budget_insufficient, gas.sponsor is developerand the attached gas budget can't cover the on-chain step. Top up and retry.
  • gas_exceeds_max, gas.sponsor is user_notional and the deducted gas would exceed gas.max_gas_usd. Raise the cap or switch to developer.
  • order_already_settled / order_already_cancelled, the on-chain order is no longer ACTIVE. Idempotent; safe to ignore.
  • order_not_found, the create-order tx was reorged out and never re-mined. Resubmit the mint.
  • user_cancel_too_early, user_cancel called before the recovery window elapsed. Body includes recovery_at (ISO).
  • jurisdiction_restricted, the API key's configured jurisdiction (set during partner KYB) falls in the restricted list for this asset class. Enforced at the SDK access layer, not at the token contract. See Security.
21SDK guides · Mint and redeem#redeem

Redeem, lock tokens, burn on settlement, credit wallet balance

redeemis the inverse of mint: quantity-in, notional-out. The partner sends a token quantity and an acceptable slippage on the payout; the SDK derives the minimum USD notional the user will accept and submits the order. Net proceeds are credited to the user's Flo wallet balance on settlement; cash-out to a destination chain happens via wallet.withdraw as a separate call. The call is asynchronous via the same on-chain order_id model as mint, same two on-chain steps, same webhook lifecycle.

Two on-chain steps. Step 1. Partner submits the SDK call. The contract atomically locks quantity tokens under order_id. Tokens are not burned yet. Webhook: redeem.created (or redeem.queued if the broker leg can't open immediately). Step 2.Once Flo Capital SPC offsets the underlying with the prime broker, Flo's keeper calls settle(order_id). The locked tokens are burned and the net USD proceeds, net of developer_feeand gas, are credited to the user's Flo wallet balance in the same tx. Webhook: redeem.settled.
Wallet credit is asynchronous. It does not land in the same block as the redeem call. There is no inline-from-pool fast path. Subscribe to redeem.settled (or poll client.redeems.retrieve(id)) for the terminal state instead of blocking on the call's return. Cash-out to a chain happens through wallet.withdrawas a separate call. The recovery window applies here too: if the keeper neither settles nor cancels within the asset's window, the user can call orderContract.user_cancel(order_id) to release the locked tokens back to the source wallet. No re-mint is needed because the tokens were never burned.
# Quantity-in. The SDK derives min_notional_usd from the live quote and slippage_bps.
# Net proceeds credit to the user's Flo wallet balance; cash-out is via wallet.withdraw.
result = client.redeem(
    asset="AAPL",
    chain=42161,  # Arbitrum. EIP-155 integer ID; see /docs#rest-chains.
    quantity=10,
    slippage_bps=50,  # 0.50 percent on the payout. Default if omitted.
    source_wallet="0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    gas={"sponsor": "developer"},  # see /docs#gas
    developer_fee={"amount_bps": 25, "amount_flat_usd": 0.50, "destination": "0x9f2c..."},
    idempotency_key="rdm_2026_04_21_0001",
)
# result.id is a redeem ID (rdm_...); result.status is "active".
# Watch for "settled" via the redeem.settled webhook.

#Fee math

The broker leg sells quantityat the executed price. From the gross proceeds, the SDK first deducts the partner's developer_fee, then the gas budget if gas.sponsor is user_notional. The remainder is what credits the user's Flo wallet balance:

textgross_proceeds_usd = quantity * executed_price
net_credit_usd     = gross_proceeds_usd
                      - developer_fee_usd
                      - gas_usd                        # only if gas.sponsor = "user_notional"

min_notional_usd   = (quantity * quote * (1 - slippage_bps / 10_000))
                      - developer_fee_usd
                      - gas_usd                        # same condition

If the broker offset would land below min_notional_usd, the order does not fail. It stays in active (pending) until the price moves back into the acceptable band, until the partner cancels via orders.cancel, or until the user recovery window elapses. Same canonical fee math as mint, mirrored. See developer revenue and Gas.

Redeem is token-based, not position-based. The partner doesn't reference a mint ID: tokens are fungible ERC-20 holdings, so identifying the source by (asset, chain, source_wallet) is sufficient. A user can redeem tokens received via transfer or bridge, not just tokens they minted themselves.

#Parameters

FieldTypeDescription
assetrequiredstringTicker of the Flo token to redeem (e.g. AAPL, NVDA, SPY). The (asset, chain) pair deterministically resolves to the ERC-20 contract.
chainrequiredinteger (EIP-155 chain ID)Chain the tokens are redeemed from. Always Arbitrum, 42161: redemption executes on Arbitrum. If the holder's tokens are on another chain, bridge them to Arbitrum first. Pass the integer chain ID; string slugs are not accepted. See Chains catalog.
quantityrequirednumberToken quantity to redeem. Fractional supported down to 10⁻⁶ shares (broker fill granularity). May be less than the holder's balance: partial redemption is supported. The order contract locks this quantity at submission; the burn happens later, in settle.
slippage_bpsintegerMaximum acceptable slippage on the payout, in basis points. Default 50 (0.50 percent). Sets min_notional_usd via the formula above. If gross proceeds net of fees and gas would land below, the order stays active until fillable or cancelled.
source_walletrequiredaddressEVM address that holds the tokens. The wallet signs the lock-and-create-order tx and pays gas. Must hold ≥ quantity at time of submission.
gasobjectGas-fee handling. Same shape as mint. See Gas.
developer_feeobjectOptional partner markup, same shape as on mint. Deducted from gross proceeds before the wallet credit. See developer revenue.
idempotency_keystringDedup window 24 h. SDK auto-generates a UUIDv4 if omitted.

#Response

The response returns the moment the lock-and-create-order tx confirms (Step 1). Settlement arrives via redeem.settled. Fields with null populate on settle.

json{
  "id": "rdm_8b1024kQ7tR9pX",
  "object": "redeem",
  "status": "active",
  "order_id": "0x9c1e44b2af72...",
  "asset": "AAPL",
  "quantity": "10.000000",
  "slippage_bps": 50,
  "quote_at_submission": "184.21",
  "min_notional_usd": "1830.94",
  "developer_fee_usd": "4.61",
  "gas_usd": "0.00",
  "executed_price": null,
  "gross_proceeds_usd": null,
  "net_payout_usd": null,
  "source_wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
  "payout": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x9f2c...e41a",
    "tx_hash": null
  },
  "gas": { "sponsor": "developer", "max_gas_usd": null },
  "created_at": "2026-04-21T14:33:02Z"
}

#Order lifecycle

FieldTypeDescription
queuedintermediateLock-and-create-order tx confirmed; tokens locked. Broker offset can't open right now (out-of-session, or insufficient liquidity). Auto-promotes to active when fillable. Carries queue_reason and estimated_fillable_at. Webhook: redeem.queued.
activeintermediateTokens locked, broker offset open; waiting on a fill that meets min_notional_usd after fees and gas. Webhook: redeem.created.
settledterminalBroker leg confirmed. Flo's keeper called settle(order_id); the locked tokens were burned and net_payout_usd landed at payout.tx_hash in the same tx. Webhook: redeem.settled.
cancelledterminalPartner cancelled, broker rejected the offset, or the user-recovery window elapsed. Locked tokens released back to the source wallet (no re-mint needed; tokens were never burned). Any developer_fee_usd accrual is reversed. Webhook: redeem.cancelled with cancel_reason.

For mainstream asset classes during core hours, active → settledtypically completes in well under an hour. Outside core hours, after-hours, and program-redemption assets defer to broker-cycle timing (T+1 for US equities, typically T+2 for other markets, monthly or quarterly for private credit). The settle-or-cancel decision is made by Flo's keeper based on broker leg outcome; if the keeper is offline, the user's user_cancel escape hatch applies the same way as on mint.

#Pricing

Flo charges nothing on either side of the trade: zero fees on mint, zero fees on redeem. The partner sets the price the user pays on both legs. Pass a developer_fee on the redeem call (same shape as on mint) and keep the markup. The fee is taken from gross proceeds before the payout lands. See developer revenue.

22SDK guides · Mint and redeem#limit

Limit orders, mint or redeem at a worst-acceptable price

Limit orders are the same two on-chain steps as market orders, with one difference: Step 1 confirms but the order sits in queued until the mark crosses the partner's limit_price. When the limit is hit the order auto-promotes to active, the broker leg opens, and Step 2 settles when the fill confirms. Same fee math, same slippage semantics, same cancellation path.

Two on-chain steps. Step 1. Partner submits the SDK call. The contract atomically pulls notional_usd (mint) or locks quantity (redeem) and parks the order at order_id. Webhook: mint.queued / redeem.queued. Step 2. When the mark crosses limit_price, the order promotes to active and the broker leg runs. On confirmation, Flo's keeper calls settle(order_id); the live token is minted (or the locked tokens are burned and the payout lands) in the same tx. Webhook: mint.settled / redeem.settled.

#Mint at a limit

# Mint AAPL only if the live quote is at or below 180 USDF per share.
order = client.mint.limit(
    asset="AAPL",
    notional_usd=1000,
    limit_price=180,
    slippage_bps=50,
    good_till="gtc",  # "gtc" | "day" | ISO timestamp
    settlement={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11"},
    gas={"sponsor": "developer"},
    developer_fee={"amount_bps": 25, "destination": "0x9f2c..."},
    idempotency_key="mlim_2026_04_21_0001",
)

#Redeem at a limit

# Redeem 10 AAPL only if the live quote is at or above 200 USDF per share.
order = client.redeem.limit(
    asset="AAPL",
    chain=42161,
    quantity=10,
    limit_price=200,
    slippage_bps=50,
    good_till="day",
    source_wallet="0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    payout={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11"},
    gas={"sponsor": "developer"},
    developer_fee={"amount_bps": 25, "destination": "0x9f2c..."},
    idempotency_key="rlim_2026_04_21_0001",
)

#Response

A limit order returns in queued with queue_reason: "limit_unmet"; it promotes to active only when the mark crosses limit_price.

json// order = client.mint.limit(...)  →  parked until the mark falls to limit_price
{
  "id": "mnt_8f21aQ4mL9",
  "object": "mint",
  "status": "queued",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "AAPL",
  "notional_usd": "1000.00",
  "limit_price": "180.00",
  "slippage_bps": 50,
  "good_till": "gtc",
  "queue_reason": "limit_unmet",
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

// order = client.redeem.limit(...)  →  tokens locked, parked until the mark rises to limit_price
{
  "id": "rdm_3c77bT1xW8",
  "object": "redeem",
  "status": "queued",
  "order_id": "0x2b9d40af71c6...",
  "asset": "AAPL",
  "quantity": "10.000000",
  "limit_price": "200.00",
  "slippage_bps": 50,
  "good_till": "day",
  "queue_reason": "limit_unmet",
  "payout": { "currency": "USDF", "chain": 42161, "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11" },
  "created_at": "2026-05-15T14:33:02Z"
}

#Parameters

FieldTypeDescription
limit_pricerequirednumberThe worst acceptable execution price. For mint.limit this is the maximum (buy at or below). For redeem.limit this is the minimum (sell at or above). Independent of slippage_bps, which still bounds the executed price relative to the live quote at fill time.
good_tillstringOrder expiry. gtc (good-til-cancelled, default), day (cancels at the asset's next session close), or an ISO 8601 timestamp. On expiry the order moves to cancelled with cancel_reason: "good_till_expired".
(everything else)-Same shape as the corresponding market order. See Mint or Redeem for the full parameter set, including gas, developer_fee, settlement / payout, and idempotency_key.

#Order lifecycle

FieldTypeDescription
queuedintermediateStablecoin pulled (mint) or tokens locked (redeem). Waiting on mark to cross limit_price. Webhook: mint.queued / redeem.queued with queue_reason: "limit_unmet".
activeintermediateMark crossed limit_price; broker leg open. Same semantics as a market order from here.
settledterminalBroker leg confirmed and settle(order_id) ran. Same as market.
cancelledterminalPartner cancelled, good_till expired, or recovery window elapsed. Returns limit_price_unreachable on good_till expiry without a fill.
23SDK guides · Mint and redeem#triggers

Triggers, stop-loss, take-profit, OCO

Triggers exit a position when the mark crosses a threshold the partner sets at arm time. They are modeled on the redeem path: tokens are locked under the order contract at arm time, so a triggered fill never fails for lack of inventory. When the trigger fires, the order behaves identically to a market redeem: same fee math, same slippage band, same two-step settlement.

Two on-chain steps. Step 1. Partner arms the trigger. The contract atomically locks quantity tokens under order_id in the armed state. Webhook: order.armed. Step 2. When the trigger condition is met, the order transitions to triggered → active and the broker leg opens. On fill, Flo's keeper calls settle(order_id); tokens burn, payout lands. Webhooks: order.triggered then redeem.settled.

#Stop-loss

# Exit 10 AAPL if the mark falls to or below 170 USDF.
sl = client.triggers.stop_loss(
    asset="AAPL",
    chain=42161,
    quantity=10,
    trigger_price=170,
    slippage_bps=100,  # wider on a stop, market often moves through
    source_wallet="0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    payout={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11"},
    gas={"sponsor": "developer"},
    developer_fee={"amount_bps": 25, "destination": "0x9f2c..."},
    idempotency_key="sl_2026_04_21_0001",
)

#Take-profit

# Exit 10 AAPL if the mark rises to or above 220 USDF.
tp = client.triggers.take_profit(
    asset="AAPL",
    chain=42161,
    quantity=10,
    trigger_price=220,
    slippage_bps=50,
    source_wallet="0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    payout={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11"},
    gas={"sponsor": "developer"},
    developer_fee={"amount_bps": 25, "destination": "0x9f2c..."},
    idempotency_key="tp_2026_04_21_0001",
)

#OCO (one-cancels-other)

A pair of trigger prices on the same locked quantity. Whichever side fires first cancels the other atomically on chain (the locked tokens cannot be double-spent because they sit under a single order_id).

# Bracket 10 AAPL: exit on a stop at 170 OR a target at 220, whichever hits first.
oco = client.triggers.oco(
    asset="AAPL",
    chain=42161,
    quantity=10,
    stop_loss_trigger_price=170,
    take_profit_trigger_price=220,
    slippage_bps=75,
    source_wallet="0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    payout={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11"},
    gas={"sponsor": "developer"},
    developer_fee={"amount_bps": 25, "destination": "0x9f2c..."},
    idempotency_key="oco_2026_04_21_0001",
)

#Response

A trigger returns in armed with the locked quantity held under one order_id. It does not enter the broker leg until the mark crosses trigger_price.

json// sl = client.triggers.stop_loss(...)  →  tokens locked, watching the mark
{
  "id": "ord_sl_5kR2mN8p",
  "object": "order",
  "kind": "stop_loss",
  "status": "armed",
  "order_id": "0x91c4...5af0",
  "asset": "AAPL",
  "quantity": "10.000000",
  "trigger_price": "170.00",
  "slippage_bps": 100,
  "payout": { "currency": "USDF", "chain": 42161, "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11" },
  "created_at": "2026-05-15T14:33:02Z"
}

// tp = client.triggers.take_profit(...)  →  identical shape; kind "take_profit", trigger_price "220.00"

// oco = client.triggers.oco(...)  →  one order_id, both legs armed; first to fire cancels the other
{
  "id": "ord_oco_7c2f55kT",
  "object": "order",
  "kind": "oco",
  "status": "armed",
  "order_id": "0x6d18...e3b9",
  "asset": "AAPL",
  "quantity": "10.000000",
  "slippage_bps": 75,
  "legs": {
    "stop_loss":   { "trigger_price": "170.00" },
    "take_profit": { "trigger_price": "220.00" }
  },
  "payout": { "currency": "USDF", "chain": 42161, "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11" },
  "created_at": "2026-05-15T14:33:02Z"
}

#Parameters

FieldTypeDescription
trigger_pricerequirednumberThe threshold the mark must cross to fire the order. For stop_loss, mark must fall to or below; for take_profit, mark must rise to or above. Validated against the live mark at arm time, returns trigger_price_invalid if a stop is above mark or a take-profit is below mark.
stop_loss_trigger_price / take_profit_trigger_pricerequirednumberOCO only. Both required. Same validation as the single-sided variants.
slippage_bpsintegerSlippage band on execution after the trigger fires. Default 50; partners typically widen on stops because triggered exits often run through the threshold. Same fee-math semantics as redeem.
(everything else)-Same shape as redeem, including gas, developer_fee, payout, and idempotency_key.

#Order lifecycle

FieldTypeDescription
armedintermediateTokens locked under order_id. Watching the mark. Webhook: order.armed.
triggeredintermediateMark crossed trigger_price. The broker leg opens immediately. For OCO, the other side is cancelled atomically in the same on-chain step. Webhook: order.triggered.
activeintermediateBroker leg open; behaves identically to a market redeem from here.
settledterminalTokens burned, payout landed at payout.tx_hash. Webhook: redeem.settled.
cancelledterminalPartner cancelled before the trigger fired, OCO sibling fired first, or recovery window elapsed. Locked tokens released to the source wallet. Webhook: order.cancelled.
24SDK guides · Mint and redeem#cancel

Cancellation, free off the partner's side

A single endpoint cancels any open order: market mint, market redeem, limit, stop-loss, take-profit, or OCO. Works in queued, armed, and active states (the latter only while the broker leg has not yet confirmed). Refunds the user in full: the stablecoin pulled in Step 1 (mint side) or the tokens locked in Step 1 (redeem and trigger side). Any developer_fee accrual on the order is reversed.

No cancellation fee.Flo charges nothing on cancel and the partner cannot configure a markup on it. The on-chain cancel transaction itself consumes gas, billed to whoever submits it (Flo's keeper when the partner calls orders.cancel within the recovery window, or the user wallet when user_cancel is used past the window). That is the only economic cost of a cancellation.
# Works for any order_id: mint, redeem, limit, SL, TP, OCO.
result = client.orders.cancel(order_id="0x7f3a91c2e4b8...")
# result.status is "cancelled" or, on a race, "broker_leg_in_flight" with executed_quantity populated.

#Response

json// Clean cancel: user refunded in full, developer_fee accrual reversed
{
  "object": "order.cancel",
  "order_id": "0x7f3a91c2e4b8...",
  "status": "cancelled",
  "cancel_reason": "partner_cancel",
  "refund": { "kind": "tokens_unlocked", "quantity": "10.000000" },
  "developer_fee_reversed_usd": "2.50",
  "cancelled_at": "2026-05-15T14:33:02Z"
}

// Race: the broker leg partial-filled before the cancel landed
{
  "object": "order.cancel",
  "order_id": "0x7f3a91c2e4b8...",
  "status": "broker_leg_in_flight",
  "executed_quantity": "3.500000",
  "executed_price": "184.20",
  "remainder_cancelled_quantity": "6.500000"
}

#Parameters

FieldTypeDescription
order_idrequiredstringThe on-chain order_id from the original SDK call response.
idempotency_keystringDedup window 24 h. Cancel is idempotent: re-submitting on an already-cancelled order returns the same response.

#Errors

  • order_already_settled, the order moved to settled before the cancel landed. Idempotent: safe to ignore.
  • order_already_cancelled, idempotent: safe to ignore.
  • broker_leg_in_flight, the cancel arrived after the broker partial-filled. Response body includes executed_quantity and executed_price so the partner can settle the partial; the unfilled remainder is cancelled and refunded.
  • order_not_found, the order_id is not a Flo order or was reorged out.
25SDK guides · Mint and redeem#gas

Gas, sponsor it or deduct from notional

Every order accepts a gasobject. Two modes, partner's choice per call. Off-chain activity (quote lookup, retrieve, list, fee balance, webhooks) consumes no gas regardless of mode.

What costs gas. Two on-chain steps per order: the create-order tx in Step 1 and the settle-or-cancel tx in Step 2. The broker leg between them is off-chain and is never sponsored. Anything read-only in the SDK never consumes gas.

#Shape

typescripttype GasOptions = {
  sponsor: "developer" | "user_notional"
  // Only used when sponsor = "user_notional". Bounds the deduction.
  // Returns gas_exceeds_max if the live gas estimate would exceed it.
  max_gas_usd?: number
}

#sponsor: "developer" (default)

The partner sends a gas budget along with the SDK call. Flo's keeper executes Step 1 and Step 2 against it. The user pays nothing toward gas; the user's notional_usd goes entirely to fees and the asset purchase. If the budget is exhausted before Step 2 confirms, the call returns gas_budget_insufficient before any on-chain action and the order does not enter the contract. There is no separate fee settlement for gas: the partner is invoiced inline against the existing developer_fee balance shown in developer revenue.

#sponsor: "user_notional"

Gas is deducted inline from the order: from notional_usd on a mint, from gross proceeds on a redeem. The deduction sits between developer_fee and the asset purchase / payout, per the canonical fee math. max_gas_usd, if set, caps the deduction; if the live gas estimate would exceed it, the call returns gas_exceeds_max and the order does not enter the contract.

#Examples

# Sponsor the gas yourself. Default mode.
client.mint(
    asset="AAPL",
    notional_usd=1000,
    settlement={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B..."},
    gas={"sponsor": "developer"},
)

# Pass it to the user, capped at $0.50.
client.mint(
    asset="AAPL",
    notional_usd=1000,
    settlement={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B..."},
    gas={"sponsor": "user_notional", "max_gas_usd": 0.50},
)

#Response

json// gas: { sponsor: "developer" }  →  the user's notional is untouched by gas
{
  "id": "mnt_0a4921pQ9mR3nV",
  "object": "mint",
  "status": "active",
  "asset": "AAPL",
  "notional_usd": "1000.00",
  "gas": { "sponsor": "developer", "gas_usd": "0.00", "max_gas_usd": null },
  "net_for_purchase_usd": "1000.00",
  "created_at": "2026-05-15T14:33:02Z"
}

// gas: { sponsor: "user_notional", max_gas_usd: 0.50 }  →  gas deducted from notional, under the cap
{
  "id": "mnt_4d8821wQ2rT6",
  "object": "mint",
  "status": "active",
  "asset": "AAPL",
  "notional_usd": "1000.00",
  "gas": { "sponsor": "user_notional", "gas_usd": "0.31", "max_gas_usd": "0.50" },
  "net_for_purchase_usd": "999.69",
  "created_at": "2026-05-15T14:33:02Z"
}
26SDK guides · Mint and redeem#positions

Positions, live NAV and yield

Read any tokenized position by token ticker and holder wallet. Returns live NAV expressed in the asset's own accounting unit, accrued yield, and the broker attestation URL for audit trails. Always free.

NAV is denominated in nav.unit, the asset's natural unit, never in USDF. Flo will not invent a USDF oracle for tokens that don't have one. For a tokenized share you get the share price in the issuer's reporting currency; for a vault token you get underlying-units-per-token (which grows over time as yield accrues). Convert to display currency on your side using whatever reference price your stack already trusts.

Flo runs a total-return model. Cash dividends (net of any applicable issuer-country withholding tax), bond coupons, money-market accruals, scrip dividends, and stock splits are paid out to token holders as an increase in nav.value. Token supply changes only on mint and redeem. yield.gross_apy_bps is therefore a NAV-growth rate covering price plus reinvested distributions.

pos = client.positions.get("AAPL", wallet="0x4a8B...")
# or list every position your key can read
for p in client.positions.list(limit=100):
    print(p.token, p.quantity, p.nav.value, p.nav.unit)
json// client.positions.get("AAPL", wallet="0x4a8B...")
{
  "object": "position",
  "token": "AAPL",
  "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
  "quantity": "10.000000",
  "nav": { "value": "1.000000", "unit": "share", "as_of": "2026-05-15T14:33:00Z", "stale": false },
  "yield": { "gross_apy_bps": 0, "net_apy_bps": 0 },
  "attestation_url": "https://attest.flo.finance/ATT-2026-05-15-AAPL.pdf"
}

#Response fields

FieldTypeDescription
tokenstringTicker.
quantitydecimalFractional holding.
nav.valuedecimalNAV per Flo token, denominated in nav.unit. Multiply by quantity for the position value in that unit.
nav.unitstringThe asset's accounting unit. Examples: "share" for a tokenized equity (always 1, since 1 token = 1 share), "USDF" for a yield-accruing wrapper (grows over time), or the underlying ticker for vault tokens. Never assume USDF unless this field says so.
nav.as_ofiso8601Timestamp of the NAV tick.
nav.stalebooleanTrue when the NAV tick is older than the venue's stale threshold (usually 120 s).
yield.gross_apy_bpsintegerTrailing-30-day gross yield annualized, in basis points.
yield.net_apy_bpsintegerNet of any management and performance fees levied by the underlying issuer or strategy. Use this when displaying yield to the end user.
attestation_urlstringCurrent SPC / broker attestation report for this ticker.
27SDK guides · Supply#supply-overview

Supply overview

supply draws notional from the user's Flo wallet balance in USDF and supplies it to the Flo lending pool. The pool funds Borrow against tokenized collateral. Suppliers receive shares in a tokenized lending vault issued by Flo Global Markets Ltd. (BVI), with assets held by Flo Capital SPC (Cayman); the legal substrate for the pool sits under the FMA Base Prospectus. The vault is a regulated lending-vault security under the FMA Base Prospectus; it is not a bank deposit and is not a registered investment fund. Yield accrues from t0 and reflects in NAV continuously.

The supplier rate is computed as borrow_apy × utilization × (1 − reserveFactor), where utilization is the share of supplied capital currently borrowed. The borrow APR follows a two-kink utilization curve: gentle below 75%, steeper between 75% and 90%, and sharply rising above 90% to incentivize repayment and pull new supply in.

#Curve regions

FieldTypeDescription
0–75%NormalMost operating time. Both sides have headroom; rates are stable.
75–90%TightFirst kink. Rate climbs more steeply to discourage marginal borrows and pull new supply.
90–100%StressSecond kink. Rate spikes hard. Borrowers are pressured to repay; suppliers earn outsized yield. New withdrawals beyond available liquidity queue.
28SDK guides · Supply#supply-supply

Supply, deposit stablecoin and earn yield

Notional supplied is debited from the user's Flo wallet balance. Fund the wallet first via wallet.deposit.

supply is asynchronous and uses the same order_id model as mint and redeem. The call submits one on-chain tx that atomically debits the wallet balance and creates a supply order. Lending-vault shares are minted to settlement.wallet in a separate tx, after the pool intake leg confirms via settle(order_id). Yield accrues on the shares once they land. Redeem the shares via Withdraw.

# wallet-balance debit + create-order in one on-chain tx; lending-vault shares mint asynchronously.
receipt = client.supply(
    asset="USDF",
    amount=1_000_000,
    settlement={"chain": 42161, "wallet": "0x4a8B70..."},
    idempotency_key="sup_2026_05_07_0001",
)
# receipt.status is "active"; watch for "settled" via webhook supply.settled.
json{
  "id": "sup_2f81kR4mL9",
  "object": "supply",
  "status": "active",
  "order_id": "0x5c10...92af",
  "asset": "USDF",
  "amount_usd": "1000000.00",
  "share_token": "fUSDF-Pool",
  "vault_shares": null,
  "settlement": {
    "chain": 42161,
    "wallet": "0x4a8B70...",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

vault_shares is null until the pool intake leg confirms; it populates on the supply.settled webhook.

FieldTypeDescription
assetrequiredstringUSDF, drawn from the Flo wallet balance.
amountrequirednumberNotional amount in stablecoin units.
settlement.chainrequiredinteger (EIP-155 chain ID)Origin chain for the supplied stablecoin and destination chain for the lending-vault shares. Always the integer chain ID; string slugs are not accepted. See Chains catalog.
settlement.walletrequiredaddressEVM address that receives lending-vault shares.
idempotency_keystringDedup window 24 h. The on-chain order_id is the canonical idempotency mechanism; this header guards retries on the create-order tx itself.

#Order lifecycle

FieldTypeDescription
activeintermediateStablecoin pulled, supply order created on chain. Webhook: supply.created.
settledterminalLending-vault shares minted to settlement.wallet; yield accrual active. Webhook: supply.settled.
cancelledterminalSupply order cancelled and stablecoin refunded (or recovered via user_cancel after the recovery window). Webhook: supply.cancelled.

#Other webhooks

The supply vault doesn't emit discrete yield-accrual events. Yield is reflected continuously in the NAV of the vault share token. To track yield onchain, subscribe to:

  • supply.nav_updated: periodic NAV push for the supply vault share token (configurable cadence, default 1 hour). The payload carries the current nav, the share token, the chain, and a recorded_at timestamp. This is not an accrual event; it's a relay of the live NAV. Compute yield client-side from the NAV at deposit:
json// supply.nav_updated payload
{
  "type": "supply.nav_updated",
  "data": {
    "token": "fUSDF-Pool",
    "chain": 42161,
    "nav": "1.0427189",
    "nav_unit": "USDF",
    "recorded_at": "2026-05-08T14:00:00Z"
  }
}

Save the NAV from the supply.settled payload at deposit time as your baseline, then derive yield off any later supply.nav_updated:

javascript// yield in stablecoin terms
const yield = (nav_now - nav_at_deposit) * shares_held;

// or annualised, if you want an APY display
const days = (Date.now() - depositTs) / 86_400_000;
const apy = Math.pow(nav_now / nav_at_deposit, 365 / days) - 1;
29SDK guides · Supply#supply-withdraw

Withdraw, lock lending-vault shares, burn on settlement, credit wallet balance

Net proceeds are credited to the user's Flo wallet balance; cash-out to a destination chain happens via wallet.withdraw as a separate call.

withdraw is asynchronous and uses the same order_id model. The call submits one on-chain tx that atomically locks the lending-vault shares under the order contract and creates a withdraw order. Shares are not burned at submission. They are held by the contract pending settlement. When pool liquidity covers the order, settle(order_id)burns the locked shares and credits the user's Flo wallet balance with the net proceeds in the same tx. If utilization is at the cap when settle runs, the order remains active and clears as borrowers repay or new supply arrives. If the keeper neither settles nor cancels within the recovery window, user_cancel(order_id) releases the locked vault shares back to the user (no re-mint; the shares were never burned).

# Lock lending-vault shares + create withdraw order in one on-chain tx.
# Shares are burned later, when settle(order_id) runs against pool liquidity.
receipt = client.withdraw(
    shares=950_000,
    payout={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B70..."},
)
# receipt.status is "active"; watch for "settled" via webhook withdraw.settled.
json{
  "id": "wd_7c39mN2pQ1",
  "object": "withdraw",
  "status": "active",
  "order_id": "0x8f44...c1d0",
  "shares": "950000.000000",
  "share_token": "fUSDF-Pool",
  "net_proceeds_usd": null,
  "payout": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B70...",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

net_proceeds_usd is null until settle burns the locked shares at the settlement NAV; it populates on the withdraw.settled webhook.

FieldTypeDescription
sharesrequirednumberLending-vault share amount to redeem. Up to 100% of held shares. The order contract locks this amount at submission; the burn happens later, in settle.
payout.currencyrequiredstringAlways USDF. Net proceeds are credited to the user's Flo wallet balance in USDF; cash-out to an external stablecoin happens via wallet.withdraw.
payout.chainrequiredinteger (EIP-155 chain ID)Destination chain. May differ from where the shares live; cross-chain via Bridge. Always the integer chain ID; string slugs are not accepted.
payout.walletrequiredaddressEVM address for stablecoin payout.

#Order lifecycle

FieldTypeDescription
activeintermediateLock-and-create-order tx confirmed on chain. Lending-vault shares are locked under the order contract (not yet burned); awaiting pool liquidity at the time of settle. Webhook: withdraw.created.
settledterminalPool liquidity covered the order. The locked shares were burned and the stablecoin payout landed on chain, in the same settle tx. Webhook: withdraw.settled.
cancelledterminalOrder cancelled (keeper failure or user_cancel after window). The locked lending-vault shares were released back to the source wallet (no re-mint; the shares were never burned). Webhook: withdraw.cancelled.
30SDK guides · Supply#supply-rates

Rates, curve parameters and live state

Live curve parameters and the current pool state are exposed at GET /v1/supply/rates. The endpoint is unauthenticated and updates every block.

json{
  "utilization": "0.7842",
  "borrow_apy": "0.0720",
  "supply_apy": "0.0507",
  "reserve_factor": "0.10",
  "curve": {
    "base": "0.02",
    "slope1": "0.06",
    "slope2": "0.14",
    "slope3": "0.80",
    "kink1": "0.75",
    "kink2": "0.90"
  },
  "pool": {
    "total_supply_usd": "15234210.55",
    "total_borrow_usd": "11942581.20",
    "available_liquidity_usd": "3291629.35",
    "queued_withdrawals_usd": "0.00"
  },
  "as_of": "2026-05-07T14:33:02Z"
}
31SDK guides · Borrow#borrow-overview

Borrow overview

Post any Flo token as collateral and receive USDF credited to the Flo wallet balance, asynchronously, via the same order_id model used everywhere else. The call submits one on-chain tx that atomically locks the collateral and creates a borrow order; the USDF disbursement is delivered in a separate tx via settle(order_id) once the pool-side leg confirms. APR is a floating rateset by the pool's utilization curve, with kinks at 75% and 90% utilization. See Rates for live curve parameters.

#Three-margin model

Every borrow position is governed by three asset-class-specific LTV thresholds:

  • Initial margin: the maximum LTV the position can be opened at. The borrow call rejects with ltv_exceeded (422) if the requested borrow would push LTV past this threshold.
  • Maintenance margin: the maximum LTV the position must stay below after opening. Crossing it does not liquidate the position; it triggers the borrow.maintenance_breach webhook (continuously, while the position stays above maintenance) so the borrower can top up collateral, partial-repay, or close.
  • Liquidation margin: the LTV at which keepers auto-close the position. Triggered on a 30-second TWAP (not a single tick). Cap is enforced; there is no defined grace window. Borrowers are responsible for monitoring borrow.maintenance_breach and borrow.liquidation_imminent webhooks and acting before the liquidation threshold is crossed.

#Margins by asset class

Each row reads initial / maintenance / liquidation as LTV percentages. See Liquidation mechanics for the full state machine.

FieldTypeDescription
Large-cap equities70% / 75% / 78%AAPL, NVDA, TSLA, MSFT, S&P 500 constituents
Blue-chip ETFs75% / 79% / 82%SPY, QQQ, VTI, VOO, broad-market index ETFs

Lendable collateral at launch is restricted to large-cap equities and blue-chip ETFs. Government bonds, corporate credit, and private credit aren't accepted as collateral today. Additional asset classes are onboarded as their oracle coverage and pool depth meet the borrow protocol's reliability thresholds.

Borrows are subject to available pool liquidity.The borrow disbursement comes from the same lending pool that powers Supply / Withdraw. Even when your collateral and LTV are both fine, a borrow can be denied or queued if the pool doesn't have free stablecoin to pay out:
  • Below the utilization cap: the pool has free stablecoin. The borrow order moves active → settled in the usual window (typically a few blocks to a few minutes).
  • At the utilization cap (≈ 100%): the order remains active in a deterministic FIFO queue and clears as borrowers repay or new supply arrives. Position in queue is exposed on the borrow object and via the webhook stream, symmetric to Withdraw queue mechanics.
  • Recovery window elapsed: if the keeper hasn't settled within the asset's window, the borrow can be cancelled by either side and the locked collateral is returned to the source wallet (borrow.cancelled, reason pool_capacity_unavailable).
Two-kink utilization curve (75% normal, 90% stress) is the steady-state pressure release: as utilization rises, borrow APR climbs sharply and supply yield rises with it, pulling new supply in and pushing marginal borrows to repay. The queue is the safety valve, not the steady state.
32SDK guides · Borrow#borrow-open

Borrow

Borrowed proceeds are credited to the user's Flo wallet balance; cash-out to a destination chain happens via wallet.withdraw as a separate call.

Top-level SDK call. borrowopens a new position the first time it's called for a given (source_wallet, collateral.token, borrow.currency) tuple, and adds to the same position on subsequent calls. To top up collateral, call borrowagain with just collateral and no new debt. That improves the position's LTV and health factor without changing principal. To grow debt, pass more borrow.amount; subject to the same initial-margin LTV check on the combined position.

# Open a new borrow position.
loan = client.borrow(
    collateral={"token": "AAPL", "quantity": 500},
    borrow={
        "amount": 42000,
        "currency": "USDF",
        "chain": 42161,
        "wallet": "0x4a8B...3c9D",
    },
)
# loan.id is the brw_... id; reuse it via the same tuple to top up.

# Top-up: same tuple, more collateral, no new debt.
client.borrow(
    collateral={"token": "AAPL", "quantity": 100},
    borrow={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B...3c9D"},
)

#Response

Returns once the lock-collateral + create-order tx confirms. The borrow stablecoin disbursement arrives via webhook borrow.settled; until then the position is active.

json{
  "id": "brw_3hQ8mL2pV6wY",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "collateral": { "token": "AAPL", "quantity": "500.000000", "borrow_oracle_price_usd": "187.42" },
  "borrow":     { "amount": "42000.000000", "currency": "USDF", "create_order_tx_hash": "0xa3b1...9e4c" },
  "ltv_bps": 4482,
  "initial_ltv_bps": 7000,
  "maintenance_ltv_bps": 7500,
  "liquidation_ltv_bps": 7800,
  "apr_bps": 612,
  "health_factor": "1.56",
  "created_at": "2026-04-21T14:33:02Z"
}

#Order lifecycle

FieldTypeDescription
activeintermediateCollateral locked, borrow order created. Awaiting pool disbursement at settle. Webhook: borrow.created.
settledterminalStablecoin disbursed to borrow.wallet. Position is now interest-accruing. Webhook: borrow.settled.
cancelledterminalPool capacity unavailable / keeper failure / user_cancel after window. Collateral is unlocked back to the source wallet. Webhook: borrow.cancelled.
33SDK guides · Borrow#borrow-repay

Repay

Repay debits the user's Flo wallet balance. Fund the wallet first via wallet.deposit if the balance is short of the repay amount.

Top-level SDK call. repay handles partial repayment and full close in one primitive. There is no separate close. Pass an amount to pay down principal + accrued interest by that much; omit amount (or pass "all") to repay the full outstanding and release all collateral pro rata in the same settle tx.

Asynchronous, uses the same order_id model as the other primitives. Each call submits one on-chain tx that atomically debits the wallet balance and creates a repay order; the principal reduction and any collateral release happen in a separate tx via settle(order_id). Lifecycle: active → settled on success, active → cancelled on keeper failure or user_cancel. Webhooks: repay.created, repay.settled, repay.cancelled.

# Partial repay: pays down principal, position stays open.
client.repay("brw_3hQ8mL2pV6wY", amount=10_000)

# Full repay: closes the position and releases all collateral.
client.repay("brw_3hQ8mL2pV6wY")

#Response

json// Partial repay: client.repay("brw_3hQ8mL2pV6wY", amount=10_000)
{
  "id": "rpy_9k21mL4pQ7",
  "object": "repay",
  "status": "active",
  "order_id": "0x4e80...77a1",
  "borrow_id": "brw_3hQ8mL2pV6wY",
  "amount_usd": "10000.00",
  "closes_position": false,
  "created_at": "2026-05-15T14:33:02Z"
}

// Full repay: client.repay("brw_3hQ8mL2pV6wY")  ·  amount = outstanding principal + accrued interest
{
  "id": "rpy_2c55nT8wX1",
  "object": "repay",
  "status": "active",
  "order_id": "0x6f12...b3c4",
  "borrow_id": "brw_3hQ8mL2pV6wY",
  "amount_usd": "32184.50",
  "closes_position": true,
  "collateral_release": { "token": "AAPL", "quantity": "500.000000" },
  "created_at": "2026-05-15T14:33:02Z"
}
Interest accrues per block; you only pay for the blocks you held the loan. Flo charges no origination fee. Attach a developer_fee.amount_bps on the original borrow call to charge your user a markup you keep.
34SDK guides · Borrow#borrow-liquidations

Liquidation mechanics

#State machine

Every open borrow position is in one of three states based on its current LTV against the asset-class margin thresholds (see Borrow overview):

FieldTypeDescription
HealthyLTV ≤ maintenancePosition is below its maintenance margin. No alerts. Steady state.
In maintenance breachmaintenance &lt; LTV &lt; liquidationPosition has crossed the maintenance margin but is still below liquidation. borrow.maintenance_breach webhook fires on entry and re-fires every 30 minutes while the position remains in this band. borrow.liquidation_imminent fires when the position is within 100 bps of the liquidation margin and re-fires every 5 minutes. The borrower may top up collateral via borrow or partial-repay via repay to drop back to Healthy.
LiquidatableLTV ≥ liquidationThe 30-second TWAP has crossed the liquidation margin. Keepers auto-close. There is no grace window at this state. Each liquidation event closes up to 50% of the position to avoid total wipeouts on transient dislocations; if the resulting LTV is still above liquidation, further liquidations follow.

#Health factor

Every open borrow position carries a health_factor, a single dimensionless number benchmarked against the liquidation margin:

texthealth_factor = (collateral_value_usd × liquidation_ltv) / debt_usd

Where:
  collateral_value_usd = borrow_oracle_price_usd × collateral quantity
  liquidation_ltv       = asset-class liquidation margin (e.g. 0.78 for large-cap equities)
  debt_usd             = outstanding principal + accrued interest
Health factor uses borrow_oracle_price_usd, an on-chain oracle price maintained by the borrow protocol for its supported collateral set. This is a separate surface from the position NAV API (which is unit-honest and never USDF-denominated). Only assets with a reliable on-chain USD reference are eligible as borrow collateral; at launch this is large-cap equities and blue-chip ETFs.
  • health_factor > 1.0: the collateral covers the debt at the liquidation margin. Position may still be in maintenance breach (cross-check maintenance_ltv_bps).
  • health_factor == 1.0: at the liquidation margin. Any further adverse move triggers the 30-second TWAP breach.
  • health_factor < 1.0: past the liquidation margin. Keepers liquidate.

Worked example. 500 AAPL @ $187.42 posted as collateral, 42,000 USDF borrowed, large-cap liquidation margin of 78%:

textcollateral_value = 500 × 187.42         = 93,710.00 USDF
debt             = 42,000.00 USDF
LTV              = debt / collateral    = 0.4482  (44.82%)

Initial margin     (large-cap)          = 70%   ✓ open allowed
Maintenance margin (large-cap)          = 75%   ✓ no alert (44.82% ≪ 75%)
Liquidation margin (large-cap)          = 78%   ✓ healthy
health_factor    = (93,710 × 0.78) / 42,000     = 1.740

AAPL would need to drop roughly 32% for the LTV to cross 75% (maintenance, alerts begin) and 35% to cross 78% (liquidation). The borrower is responsible for monitoring borrow.maintenance_breach and borrow.liquidation_imminent and acting before the liquidation threshold is crossed. There is no grace window at the liquidation margin.

#Triggers

Liquidation triggers when the loan's LTV crosses the asset-class liquidation margin against a 30-second TWAP, not a single tick. This protects borrowers from flash-crash wipeouts at the keeper layer; it is not a grace period at the position layer.

#Liquidation penalty

Flat 5% on the closed notional across every account. The penalty splits 50/50: half accrues to the liquidation insurance fund (which absorbs shortfalls and protects the supplier pool from bad-debt drawdown), and half goes to the liquidator as incentive for keeping positions current. Neither half hits Flo's P&L.

35SDK guides · Borrow#borrow-rates

Live rate lookup

rates = client.borrow_rates()
for r in rates:
    print(r.asset_class, r.apr, r.initial_ltv, r.maintenance_ltv, r.liquidation_ltv)
json{
  "object": "list",
  "data": [
    {
      "asset_class": "large_cap_equity",
      "apr": "0.0612",
      "initial_ltv": "0.70",
      "maintenance_ltv": "0.75",
      "liquidation_ltv": "0.78"
    },
    {
      "asset_class": "blue_chip_etf",
      "apr": "0.0584",
      "initial_ltv": "0.72",
      "maintenance_ltv": "0.77",
      "liquidation_ltv": "0.80"
    }
  ],
  "as_of": "2026-05-15T14:33:02Z"
}

Rates are indicative, subject to market conditions. Flo publishes an effective-APR snapshot every 60 s; SDKs cache it for 5 s client-side to avoid hot-loop blowout.

36SDK guides · Capital flows#wallet-deposit

Deposit, credit your Flo wallet from any supported chain

wallet.depositcredits the user's Flo wallet from a USDC or USDT transfer on any supported source chain. The stablecoin stays on the source chain; a Chainlink CCIP message attests the deposit to Arbitrum, where the balance is credited 1:1. Trading runs against the credited balance, not against the source-chain stablecoin directly.

Two on-chain steps. Step 1. Partner submits the SDK call (or the end-user signs and sends directly to the deposit contract). The contract pulls amount in the chosen stablecoin from the source wallet and emits a CCIP message. Webhook: wallet.deposit.committed. Step 2.Once the CCIP message executes on Arbitrum, the user's wallet balance is credited. Webhook: wallet.deposit.delivered. The internal off-ramp of the collected stablecoin into fiat in the prime-broker account is asynchronous and not part of the user-facing lifecycle.
# Source-chain stablecoin in. The SDK signs and sends to the deposit contract,
# then waits for CCIP delivery on Arbitrum.
deposit = client.wallet.deposit(
    amount_usd=1000,
    source={
        "currency": "USDC",
        "chain": 8453,                 # Base. EIP-155 integer ID; see /docs#rest-chains.
        "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    },
    idempotency_key="dep_2026_04_21_0001",
)
# deposit.id is a deposit ID (dep_...); deposit.status is "committed".
# Watch for "delivered" via the wallet.deposit.delivered webhook.
json{
  "id": "dep_6kR2mN8pQ1",
  "object": "wallet_deposit",
  "status": "committed",
  "amount_usd": "1000.00",
  "source": {
    "currency": "USDC",
    "chain": 8453,
    "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    "tx_hash": "0xa3b1...9e4c"
  },
  "credit": { "chain": 42161 },
  "ccip_message_id": "0x9f21...c30a",
  "fees": { "ccip_usd": "0.18" },
  "created_at": "2026-05-15T14:33:02Z"
}

#Parameters

FieldTypeDescription
amount_usdrequirednumberUSD amount to credit to the Flo wallet, 1:1 with the stablecoin pulled from the source wallet.
source.currencyrequiredstringStablecoin sent in. USDC or USDT. Per-chain support comes from Supported chains and stablecoins.
source.chainrequiredinteger (EIP-155 chain ID)Source chain ID (e.g. 8453 for Base, 1 for Ethereum). Always the integer chain ID; string slugs are not accepted.
source.walletrequiredaddressEVM address the stablecoin is debited from. Token contracts are permissionless: no on-chain wallet allowlist. Partner-side onboarding posture is the partner's call.
idempotency_keystringDedup window 24 h. SDK auto-generates a UUIDv4 if omitted.

#Timing

End-to-end deposit time equals source-chain finality plus Chainlink CCIP execution on the source-to-Arbitrum lane. Flo does not set this number and does not quote a fixed ETA. For live, per-lane numbers, see CCIP execution latency.

#Fees

Costs on this leg flow directly to the source chain and to Chainlink CCIP. Flo does not skim.

  • Source-chain gas. Paid by the user (or sponsored by the partner via the standard gas contract) at the time of submission.
  • CCIP message fee. Paid in the source-chain native token or LINK, settled by the deposit contract on submit, and surfaced on the deposit response as fees.ccip_usd.

#Failure semantics

Deposit failure handling is governed by Chainlink CCIP, not by Flo. Once the source transaction confirms, the stablecoin pull is irreversible at the protocol level; there is no automatic on-chain refund path. Flo emits webhooks as the CCIP message moves through its lifecycle and offers operator-assisted recovery for stuck messages.

  • wallet.deposit.queued: source-chain submission accepted, awaiting source-chain finality.
  • wallet.deposit.committed: CCIP message committed by the Committing DON on the source.
  • wallet.deposit.delivered: CCIP message executed on Arbitrum; wallet balance credited.
  • wallet.deposit.requires_manual_execution: Smart Execution window elapsed without delivery; manual execution available via the CCIP Explorer.
  • wallet.deposit.lane_cursed: Risk Management Network has cursed the source-to-Arbitrum lane; in-flight messages stay queued until the curse is lifted. Source stablecoins remain pulled. Recovery is operator-assisted via support@flo.finance.
37SDK guides · Capital flows#wallet-withdraw

Withdraw, off-ramp from Flo wallet to a chosen chain

wallet.withdrawdebits the user's Flo wallet on Arbitrum and pays out USDC or USDT on a destination chain of the partner's choice. The fiat-to-stablecoin leg runs through Flo's on-ramp partner against the prime-broker account that backs the balance; final delivery is on-chain.

Withdrawal is asynchronous. Settlement can take up to 24 hours and is liquidity-bound. Flo does not quote a fixed ETA. Subscribe to wallet.withdraw.delivered (or poll client.wallet.withdrawals.retrieve(id)) for the terminal state rather than blocking on the call's return.
# Wallet balance debits on Arbitrum on submit. Payout lands on the destination chain
# after the on-ramp leg from broker fiat into the chosen stablecoin clears.
withdrawal = client.wallet.withdraw(
    amount_usd=1000,
    payout={
        "currency": "USDT",
        "chain": 42161,                # Arbitrum. EIP-155 integer ID; see /docs#rest-chains.
        "wallet": "0x9f2c70Aa19cC3d9d3c9b22Ac019bE40a7d11",
    },
    idempotency_key="wdl_2026_04_21_0001",
)
# withdrawal.id is a withdrawal ID (wdl_...); withdrawal.status is "queued".
# Watch for "delivered" via the wallet.withdraw.delivered webhook.
json{
  "id": "wdl_4d8821wQ2r",
  "object": "wallet_withdrawal",
  "status": "queued",
  "amount_usd": "1000.00",
  "payout": {
    "currency": "USDT",
    "chain": 42161,
    "wallet": "0x9f2c70Aa19cC3d9d3c9b22Ac019bE40a7d11"
  },
  "fees": { "on_ramp_usd": "1.50", "gas_usd": "0.02" },
  "debit_tx_hash": "0xa3b1...9e4c",
  "created_at": "2026-05-15T14:33:02Z"
}

#Parameters

FieldTypeDescription
amount_usdrequirednumberUSD amount to debit from the Flo wallet, 1:1 with the stablecoin paid out on the destination chain.
payout.currencyrequiredstringStablecoin paid out. USDC or USDT. Per-chain support comes from Supported chains and stablecoins.
payout.chainrequiredinteger (EIP-155 chain ID)Destination chain ID for the payout. Always the integer chain ID; string slugs are not accepted.
payout.walletrequiredaddressDestination EVM address. Token contracts are permissionless: no on-chain wallet allowlist.
idempotency_keystringDedup window 24 h. SDK auto-generates a UUIDv4 if omitted.

#Timing

End-to-end withdrawal time is bounded at 24 hours and depends on available on-ramp liquidity at the time of submission. Flo does not quote a fixed ETA. Lifecycle webhooks below give the only honest status signal; the call response is not a settlement guarantee.

#Fees

Costs on this leg flow directly to the on-ramp partner and to the destination chain. Flo does not skim.

  • On-ramp fee. Fee charged by the on-ramp partner to convert prime-broker fiat into the chosen stablecoin. Surfaced on the withdrawal response as fees.on_ramp_usd.
  • Destination-chain gas. Network gas to deliver the stablecoin to payout.wallet, paid by Flo at submission and surfaced as fees.gas_usd. Can be charged to the partner per the standard gas contract.

#Lifecycle

  • wallet.withdraw.queued: wallet balance debited on Arbitrum; on-ramp leg pending available liquidity.
  • wallet.withdraw.in_settlement: on-ramp leg cleared; stablecoin payout queued for destination-chain submission.
  • wallet.withdraw.delivered: payout tx confirmed on the destination chain.
  • wallet.withdraw.requires_manual_intervention: 24-hour window elapsed without delivery. Recovery is operator-assisted via support@flo.finance.
38SDK guides · Capital flows#bridge

Bridge, move tokens across chains

Bridge moves tokenized assets between chains. It is not the path for moving your stablecoin capital in or out of Flo, that lives in the capital-flow guides, deposit and withdraw. Both surfaces use CCIP messaging, but only Bridge moves the actual tokens.

Bridge runs on Chainlink CCIP. Every cross-chain message is validated by two independent networks: a Committing DON commits the message on the source chain and an Executing DON delivers it on the destination, while the Risk Management Network (RMN) runs in parallel and can curse a lane if it detects malicious activity. You pay only the network gas; Flo takes no fee on the message itself.

bdg = client.bridge(
    token="AAPL",
    quantity=50,
    from_chain=8453,    # Base
    to_chain=42161,     # Arbitrum
    destination="0x4a8B...3c9D",
)
json{
  "id": "brg_7c2f55kT1x",
  "object": "bridge",
  "status": "committed",
  "token": "AAPL",
  "quantity": "50.000000",
  "from_chain": 8453,
  "to_chain": 42161,
  "destination": "0x4a8B...3c9D",
  "ccip_message_id": "0x9f21...c30a",
  "fees": { "ccip_usd": "0.21" },
  "created_at": "2026-05-15T14:33:02Z"
}

#Parameters

FieldTypeDescription
tokenrequiredstringTicker of the Flo token to move.
quantityrequirednumberToken quantity to bridge (10⁻⁶ floor, same as mint/redeem).
from_chainrequiredinteger (EIP-155 chain ID)Source chain ID (e.g. 8453). Always the integer chain ID; string slugs are not accepted. See Chains catalog.
to_chainrequiredinteger (EIP-155 chain ID)Destination chain ID. Always the integer chain ID; string slugs are not accepted.
destinationrequiredaddressDestination wallet on the target chain. Defaults to the source holder if omitted.

#Route matrix

Live: Base (8453) ↔ Arbitrum (42161), Base ↔ Ethereum (1), Arbitrum ↔ Ethereum. All routes are bidirectional. Settlement time on each route is determined by Chainlink CCIP and the source chain's finality requirements, not by Flo. See CCIP execution latency for live, per-lane numbers.

Token-bridge routes are a subset of the deposit chains. Moving tokens across chains (this page) currently covers Base, Arbitrum, and Ethereum. Moving stablecoin capital in and out (deposit and withdraw) covers more chains, see Supported chains and stablecoins. The two surfaces expand independently.

#Failure semantics

Bridge failure handling is governed by Chainlink CCIP, not by Flo. The source burn is irreversible at the protocol level once the source transaction confirms; there is no automatic on-chain refund path. Flo emits webhooks as the CCIP message moves through its lifecycle and offers operator-assisted recovery for stuck messages.

  • Smart Execution. If destination execution fails initially (insufficient gas, receiver revert, transient issues), CCIP retries automatically within an 8-hour Smart Execution window. Most failures clear inside this window with no action required.
  • Manual execution.If the message is still unexecuted after the 8-hour window, it enters "Ready for manual execution." Anyone can connect a wallet on the CCIP Explorer, override the gas limit, and trigger execution. Flo emits bridge.requires_manual_execution when a message enters this state, and bridge.deliveredonce the destination mint lands. Subsequent messages on the same lane queue behind a stuck message until it's cleared. See the CCIP manual execution reference.
  • RMN curse. If the Risk Management Network curses a lane after detecting anomalous activity, in-flight messages on that lane stay queued until the curse is lifted. Source tokens remain burned during the pause; there is no protocol-level timeout that refunds them. Flo emits bridge.lane_cursed when this happens.
  • Operator-assisted recovery. If a message remains stuck past the operational tolerance for your account, contact support@flo.finance. Flo's ops team can either trigger manual execution on your behalf or, for lanes where recovery via CCIP is impossible, mint replacement tokens off-protocol after verifying the source burn. This is a manual workflow, not an automatic protocol mechanism.
39SDK guides · Capital flows#wallet-chains

Supported chains and stablecoins

Wallet deposit and withdraw accept different stablecoin and chain combinations depending on the live on-ramp / off-ramp coverage and the CCIP lane status. The set below is the wallet-scoped view. The canonical, machine-readable source is GET /v1/chains; fetch from there at request time rather than hard-coding.

#Deposit, source chains

FieldTypeDescription
Base (8453)USDC, USDTProduction. CCIP source-to-Arbitrum lane live.
Ethereum (1)USDC, USDTProduction. CCIP source-to-Arbitrum lane live.
Arbitrum (42161)USDC, USDTProduction. Same-chain credit; no CCIP hop.
Polygon (137)USDC, USDTProduction. CCIP source-to-Arbitrum lane live.
Optimism (10)USDC, USDTProduction. CCIP source-to-Arbitrum lane live.

#Withdraw, destination chains

FieldTypeDescription
Base (8453)USDC, USDTProduction. On-ramp coverage live.
Ethereum (1)USDC, USDTProduction. On-ramp coverage live.
Arbitrum (42161)USDC, USDTProduction. No CCIP hop required.
Polygon (137)USDC, USDTProduction. On-ramp coverage live.
Optimism (10)USDC, USDTProduction. On-ramp coverage live.
Adding chains. New source or destination chains land first in sandbox, then in live once both the CCIP lane (for deposits) and the on-ramp coverage (for withdrawals) clear operational gates. The Chains catalog always reflects the current live set; treat this page as a snapshot.
40Asset class guides#asset-stocks-etfs

Stocks and ETFs

Wrapped equities and ETFs follow the same mint and redeem primitives as every other Flo asset class. Settlement timing follows the underlying venue, typically T+1 in the US and T+2 elsewhere. Market hours and halts apply to the broker leg, not to the on-chain leg.

#Coverage

  • 5,000+ stocks across the US, EU, UK, Japan, India, Brazil.
  • 2,000+ ETFs.
  • $1 minimum per trade; native fractional support via Alpaca.
  • Wrapper convention: AAPLf, SPYf (see Wrapper convention).

#Mint

Equities and ETFs use the default notional-in mint. Pass the bare underlying ticker as asset; Flo mints the f-suffixed wrapped token (AAPLAAPLf).

position = client.mint(
    asset="AAPL",
    notional_usd=250,
    slippage_bps=50,
    settlement={"currency": "USDF", "chain": 42161, "wallet": user_wallet},
)
json{
  "id": "mnt_a1stk2Q9mR",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "AAPL",
  "symbol": "AAPLf",
  "notional_usd": "250.00",
  "executed_quantity": null,
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B...3c9D",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

#Market hours and out-of-hours orders

Market hours and trading halts apply to the broker leg, not the on-chain leg. An order placed while the underlying market is closed is accepted on-chain immediately, queues on Flo's in-house keeper, and releases to the broker at the next open. The token mints only after the broker leg fills. Settlement timing is typically T+1 for US listings and T+2 elsewhere; see Settlement model.

#Corporate actions

Splits, dividends, and other corporate actions are processed at the underlying. Cash dividends accrue to the position; stock splits adjust token quantity to preserve economic exposure. The position you read via positions always reflects the post-action state.

#Notes

  • Fractional fills are native; a $1 notional buys a fractional share.
  • ETF tokens behave identically to single-name equity tokens. The underlying basket is held at the broker; Flo does not pass through ETF holdings as separate tokens.
41Asset class guides#asset-treasuries

Treasuries

Tokenized Treasury bills and notes are total-return instruments. The token quantity a holder owns stays constant; the per-token NAV in the underlying currency rises as yield accrues. There is no rebase and no separate distribution event, the return shows up entirely as NAV appreciation.

#Mint

Default notional-in mint. The minted token is the f-suffixed wrapper (BILL3M BILL3Mf).

position = client.mint(
    asset="BILL3M",
    notional_usd=5000,
    settlement={"currency": "USDF", "chain": 42161, "wallet": user_wallet},
)
json{
  "id": "mnt_b2tsy4Q9mR",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "BILL3M",
  "symbol": "BILL3Mf",
  "notional_usd": "5000.00",
  "executed_quantity": null,
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B...3c9D",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

#Reading NAV and accrued yield

NAV is reported in the underlying unit, not in USDF. Read the live NAV and accrued yield from positions. Quantity held times current NAV gives the position value; the delta against the cost basis is the accrued return.

#Redeem

Redeem at any time. Unlike private credit, Treasuries have no scheduled redemption window, the order settles on the standard broker cadence (typically T+1). Proceeds pay out in the stablecoin and chain specified in payout.

42Asset class guides#asset-fx

FX

29+ currency pairs, wrapped as f-suffixed tokens (EURUSDEURUSDf). An FX token represents exposure to the pair; minting EURUSDf is a long-EUR, short-USD position held at the broker.

#Trading hours

The FX session runs 24/5. Orders submitted during the session route to the broker promptly. Orders placed over the weekend are accepted on-chain immediately and queue on Flo's keeper for the Monday open. The on-chain leg never waits, only the broker fill does.

#Mint

position = client.mint(
    asset="EURUSD",
    notional_usd=1000,
    settlement={"currency": "USDF", "chain": 42161, "wallet": user_wallet},
)
json{
  "id": "mnt_c3fx05Q9mR",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "EURUSD",
  "symbol": "EURUSDf",
  "notional_usd": "1000.00",
  "executed_quantity": null,
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B...3c9D",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

The pair's quote convention follows market standard (base/quote). Confirm the convention for any pair from its token reference page before wiring a UI.

43Asset class guides#asset-commodities

Commodities

30+ commodities, wrapped as f-suffixed tokens (GOLDGOLDf). Exposure is held at the broker layer through the standard instrument for that commodity. The token tracks the economic exposure; you do not take physical delivery.

#Mint

position = client.mint(
    asset="GOLD",
    notional_usd=2000,
    settlement={"currency": "USDF", "chain": 42161, "wallet": user_wallet},
)
json{
  "id": "mnt_d4cmd6Q9mR",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "GOLD",
  "symbol": "GOLDf",
  "notional_usd": "2000.00",
  "executed_quantity": null,
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B...3c9D",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

#Contract roll

For commodities whose underlying exposure is held through dated contracts, Flo handles the roll at the broker layer. The roll is reflected in NAV; the holder's token quantity is unaffected. Read the current value from positions.

#Trading hours

Settlement follows the underlying venue's hours. Out-of-hours orders queue on Flo's keeper and release at the next session, the same pattern as equities.

44Asset class guides#asset-bonds

Bonds

Wrapped corporate and sovereign bonds, f-suffixed like every other Flo token (NOTE10Y NOTE10Yf). Bonds are total-return instruments: coupon income and price movement both flow into NAV. There is no separate coupon payout, the holder sees the full return as NAV change.

#Mint

position = client.mint(
    asset="NOTE10Y",
    notional_usd=10000,
    settlement={"currency": "USDF", "chain": 42161, "wallet": user_wallet},
)
json{
  "id": "mnt_e5bnd7Q9mR",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "NOTE10Y",
  "symbol": "NOTE10Yf",
  "notional_usd": "10000.00",
  "executed_quantity": null,
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B...3c9D",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

#Bonds vs Treasuries

The NAV-accrual model is the same as Treasuries. The difference is price sensitivity: a bond's NAV moves with rates and, for corporates, with credit spread, so the position can mark down as well as up. Treasury bills held to a short maturity are far less sensitive. Settlement timing follows the underlying bond market, typically T+1.

45Asset class guides#private-credit

Private credit

Flo tokenizes private credit positions as blockchain-based structured notes issued by Flo Global Markets Ltd. (BVI). Underlying exposure to private credit funds and structured-credit instruments (ABS, MBS) is held by Flo Capital SPC (Cayman) and reflected 1:1 in the Flo token supply.

#Mint

The SDK surface for private credit mint is identical to public-market mint: permissionless at the token contract, asynchronous via the same order_id model, with stablecoin pulled and order created in one on-chain tx, and the live token minted via settle(order_id) after the broker leg confirms.

order = client.mint(
    asset="APOLLO_DDF",  # any private credit ticker in the Flo universe
    quantity=10,
    settlement={"currency": "USDF", "chain": 42161},
)
json{
  "id": "mnt_f6pcr8Q9mR",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "APOLLO_DDF",
  "quantity": "10.000000",
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

#Redeem

Redeem is also an SDK call. Same surface, same order_id model as public-market redeem. The only behavioural difference is the settlement window: private credit funds follow program-specific redemption windows (typically monthly or quarterly), so active → settled defers until the broker leg clears the next cycle. The order response includes the scheduled window; redeem.settled fires when stablecoin proceeds land.

order = client.redeem(
    asset="APOLLO_DDF",
    chain=42161,
    quantity=10,
    source_wallet="0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    payout={"currency": "USDF", "chain": 42161, "wallet": "0x4a8B70..."},
)
print(order.id, order.scheduled_window)
json{
  "id": "rdm_g7pcr9T1xW",
  "object": "redeem",
  "status": "active",
  "order_id": "0x2b9d40af71c6...",
  "asset": "APOLLO_DDF",
  "quantity": "10.000000",
  "scheduled_window": { "cycle": "monthly", "opens_at": "2026-06-01T00:00:00Z" },
  "payout": { "currency": "USDF", "chain": 42161, "wallet": "0x4a8B70..." },
  "created_at": "2026-05-15T14:33:02Z"
}

#Asset coverage

  • Private credit funds across managers including Apollo, Blackstone, and KKR. New programs added on partner request.
  • Structured-credit instruments: ABS and MBS tranches, sourced via the same SPC custody chain.
  • Status, NAV cadence, and program redemption windows: /markets/fixed-income.
46Developer revenue#dev-revenue-overview

Developer revenue, two modes

Flo never takes a cut of the developer markup. You set the price your end user pays. Flo offers two distinct ways to handle the markup: keep it yourself, or have Flo collect it on-chain and pay you out.

#Mode A, partner-collected

You bill the end user in your own product (account balance, subscription, in-app charge). You pass Flo only the net notional you actually want invested. developer_fee_bps is omitted or set to0. Flo never holds your markup.

  • Best for: partners with existing billing infrastructure, neobanks, fintechs.
  • Reconciliation: against your own ledger, not Flo's.

#Mode B, Flo-collected

You pass a developer_fee_bps on every call. Flo accrues the fee on-chain to a wallet you control during the mint or redeem. Flo settles to that wallet daily at 00:00 UTC in USDF, or on-demand via REST.

  • Best for: partners who do not want to build billing, crypto-native partners, wallets.
  • Reconciliation: every fee is visible on-chain, addressable by destination wallet.

#Can you mix modes?

Yes, per call. developer_fee_bps is a per-request field. You may default to one mode globally and override per product surface.

REST surface for fee balances and manual settlement lives at REST · Developer revenue endpoints.

47Developer revenue#dev-revenue-mode-a

Mode A, partner-collected markup

You charge your end user inside your product. You send Flo only the net notional you want invested. No payout from Flo to you, because there is nothing for Flo to collect.

#When to use Mode A

  • You already have an in-app balance, subscription, or charge flow.
  • You want the markup recognized in the same revenue line as the rest of your product P&L.
  • You prefer to keep fiat in your own banking stack and only send stablecoin to Flo for the trade.

#Code shape

Omit developer_fee_bps, or set it to 0 explicitly to make the intent unambiguous.

# User pays $105 in your product; you keep $5; you send Flo $100.
flo.mint(
    asset="AAPLf",
    notional_usd="100.00",
    # developer_fee_bps omitted - Mode A
    settlement={"currency": "USDF", "chain": 42161, "wallet": user_wallet},
)
json{
  "id": "mnt_h8mda1Q9mR",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "AAPLf",
  "notional_usd": "100.00",
  "developer_fee_usd": "0.00",
  "net_for_purchase_usd": "100.00",
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B...3c9D",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

#Reconciliation

Reconcile against your own ledger. Flo will report the notional and resulting position; the markup never touches Flo's books.

48Developer revenue#dev-revenue-mode-b

Mode B, Flo-collected fees with daily payout

You pass a developer_fee_bps on every call. Flo accrues the fee on-chain to your destination wallet during the mint or redeem. Daily settlement runs at 00:00 UTC in USDF; you can also settle on-demand via REST.

#When to use Mode B

  • You do not want to build or run billing infrastructure.
  • You want every fee on-chain and addressable by destination wallet.
  • You want settlement in USDF, not fiat.

#Code shape

# User pays you $100; you want a 25bps markup; Flo accrues $0.25 to your fee wallet.
flo.mint(
    asset="AAPLf",
    notional_usd="100.00",
    developer_fee_bps=25,
    fee_destination={"chain": 42161, "wallet": partner_fee_wallet},
    settlement={"currency": "USDF", "chain": 42161, "wallet": user_wallet},
)
json{
  "id": "mnt_i9mdb2Q9mR",
  "object": "mint",
  "status": "active",
  "order_id": "0x7f3a91c2e4b8...",
  "asset": "AAPLf",
  "notional_usd": "100.00",
  "developer_fee_bps": 25,
  "developer_fee_usd": "0.25",
  "net_for_purchase_usd": "99.75",
  "fee_accrual": {
    "destination": { "chain": 42161, "wallet": "0x9f2c...fee0" },
    "status": "accrued",
    "settles": "daily_00_00_utc"
  },
  "settlement": {
    "currency": "USDF",
    "chain": 42161,
    "wallet": "0x4a8B...3c9D",
    "create_order_tx_hash": "0xa3b1...9e4c"
  },
  "created_at": "2026-05-15T14:33:02Z"
}

#Fee math

The same statement applies to every order type (market mint, market redeem, limit, stop-loss, take-profit, OCO):

textfee_usd = notional_usd * developer_fee_bps / 10_000

net_for_purchase = notional_usd - fee_usd - gas_usd  # gas if gas.sponsor = "user_notional"

#Payout cadence

#Refunds on cancel

If an order is cancelled, developer_fee_usd is reversed and refunded to the user along with the locked notional. A fee.refunded webhook fires alongside themint.cancelled or redeem.cancelled event.

49Developer revenue#rest-fees

REST endpoints, fee balance and settle

REST surface for inspecting Mode B fee balances and triggering settlement on demand. For the conceptual difference between Mode A (partner-collected, no payout from Flo) and Mode B (Flo-collected, daily payout), see Developer revenue, two modes.

Balances ledger in real time. Automatic settlement runs daily at 00:00 UTC with a $10 minimum per destination wallet. There is no separate sweep for gas: see Gas.

#Balance and manual settle

# Balance by destination wallet
balances = client.fees.balance()
for b in balances:
    print(b.destination, b.accrued_usd, b.pending_usd, b.settled_usd)

# Manual settle, only sweeps destinations whose accrued balance >= minimum.
result = client.fees.settle(minimum_usd=10)
print(result.settled_count, result.total_usd)

#Response

json// GET /v1/fees/balance
{
  "object": "list",
  "data": [
    {
      "destination": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
      "chain": 8453,
      "accrued_usd": "184.20",
      "pending_usd": "0.00",
      "settled_usd": "12904.55"
    },
    {
      "destination": "0x9C3f11A2c0Ee48b1772Dd0A1b6e3F4905c22",
      "chain": 42161,
      "accrued_usd": "6.40",
      "pending_usd": "0.00",
      "settled_usd": "881.10"
    }
  ],
  "has_more": false
}

// POST /v1/fees/settle  ·  { "minimum_usd": 10 }  ·  0x9C3f… is below the minimum, so it is skipped
{
  "object": "fee.settlement",
  "settled_count": 1,
  "skipped_count": 1,
  "total_usd": "184.20",
  "settlements": [
    {
      "destination": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
      "chain": 8453,
      "amount_usd": "184.20",
      "tx_hash": "0xa3b1...9e4c"
    }
  ],
  "settled_at": "2026-05-15T14:33:02Z"
}
50REST API#rest-chains

Chains catalog

Every Flo primitive that takes a chain (mint, redeem, supply, withdraw, borrow, repay, bridge) requires an EIP-155 integer chain ID. String slugs are never accepted as input: not in the SDK, not in the REST API. Use this endpoint to discover the catalog and build chain pickers, validation tables, and status pages.

#List chains

bashcurl https://api.flo.finance/v1/chains \
  -H "Authorization: Bearer $FLO_API_KEY"

#Response

json{
  "object": "list",
  "data": [
    {
      "id": 8453,
      "slug": "base",
      "name": "Base",
      "native_currency": "ETH",
      "environment": "mainnet",
      "status": "live",
      "supports": ["deposit", "withdraw", "bridge"],
      "pay_in_currencies":  ["USDC"],
      "pay_out_currencies": ["USDC"],
      "explorer_url": "https://basescan.org",
      "rpc_hint": "https://mainnet.base.org"
    },
    {
      "id": 42161,
      "slug": "arbitrum",
      "name": "Arbitrum One",
      "native_currency": "ETH",
      "environment": "mainnet",
      "status": "live",
      "supports": ["mint", "redeem", "supply", "withdraw", "borrow", "repay", "deposit", "bridge"],
      "pay_in_currencies":  ["USDC", "USDT"],
      "pay_out_currencies": ["USDC", "USDT"],
      "explorer_url": "https://arbiscan.io"
    },
    {
      "id": 1,
      "slug": "ethereum",
      "name": "Ethereum",
      "native_currency": "ETH",
      "environment": "mainnet",
      "status": "live",
      "supports": ["deposit", "withdraw", "bridge"],
      "pay_in_currencies":  ["USDC", "USDT"],
      "pay_out_currencies": ["USDC", "USDT"],
      "explorer_url": "https://etherscan.io"
    }
  ],
  "has_more": false
}

#Response fields

FieldTypeDescription
idintegerEIP-155 chain ID. The only accepted identifier on input: every `chain` parameter across the SDK and REST API is this integer.
slugstringStable, lower-case, kebab-cased display string (base, arbitrum, ethereum) for human surfaces and URL routing in your client. Never accepted as input. Flo APIs always require the integer id.
namestringDisplay name for human surfaces (network selector, dashboards).
native_currencystringNative gas asset symbol on the chain (e.g. ETH, MATIC).
environment"mainnet" | "testnet"Mainnets carry real assets. Sandbox is keyed by environment regardless of chain.
status"live" | "paused" | "deprecated"Current ingestion status. `paused` means inbound mint/redeem are queued; `deprecated` means the chain is sunsetting and bridge-out only.
supportsstring[]Set of primitives currently routable on this chain. Use this to gate which UI surfaces show the chain.
pay_in_currenciesstring[]Stablecoins accepted for pay-in on this chain (deposit). Configurable per chain. Read it instead of hard-coding. Sending a currency outside this set returns currency_not_supported_on_chain (422).
pay_out_currenciesstring[]Stablecoins available for pay-out on this chain (withdraw). Can differ from pay_in_currencies. Outbound liquidity is provisioned per direction.
explorer_urlstringBlock explorer base URL for tx links.
rpc_hintstring?Public RPC suggestion for clients that need to broadcast directly. Optional; Flo never relies on this.
Chain IDs are the only accepted identifier. Every Flo input (SDK parameters, REST bodies, REST path params) requires the integer EIP-155 ID. The slug field is a display string only; passing it as a chain parameter returns 400 invalid_chain. Persist the integer ID in your data model.

#Pay-in vs pay-out per chain

The accepted stablecoins for deposit and withdraw are configurable per chain and per direction. Pay-in (the currency a user funds with) and pay-out (the currency they receive) are independent matrices. A chain can accept USDT for deposit while only paying out in USDC if outbound USDT liquidity isn't provisioned there yet.

Always read pay_in_currencies and pay_out_currencies from the catalog at app boot (or on a 1-hour cache) and gate your UI off the live answer rather than hard-coding USDC/USDT. New chains, new stablecoins, and per-chain activations land here first.

# Build a chain → accepted currencies map at startup.
chains = client.chains.list()
pay_in  = {c.id: c.pay_in_currencies  for c in chains}
pay_out = {c.id: c.pay_out_currencies for c in chains}

# Validate a mint before submitting:
chain_id = 8453
currency = "USDC"
if currency not in pay_in[chain_id]:
    raise ValueError(f"{currency} not accepted on chain {chain_id}")

#Currencies-only shortcut

If you only need the currency matrix for one chain, use the shortcut endpoint:

bashcurl https://api.flo.finance/v1/chains/8453/currencies \
  -H "Authorization: Bearer $FLO_API_KEY"
json{
  "chain_id": 8453,
  "pay_in":  ["USDC"],
  "pay_out": ["USDC"]
}

#Get a single chain

bashcurl https://api.flo.finance/v1/chains/8453 \
  -H "Authorization: Bearer $FLO_API_KEY"
json{
  "id": 8453,
  "slug": "base",
  "name": "Base",
  "native_currency": "ETH",
  "environment": "mainnet",
  "status": "live",
  "supports": ["deposit", "withdraw", "bridge"],
  "pay_in_currencies":  ["USDC"],
  "pay_out_currencies": ["USDC"],
  "explorer_url": "https://basescan.org",
  "rpc_hint": "https://mainnet.base.org"
}

Lookup is by integer chain ID only: /v1/chains/8453. Slug paths like /v1/chains/base return 400 invalid_chain. Unknown IDs return 404.

#Caching

The catalog is small (≤ 32 entries) and changes only when a new chain ships. Cache the list for 1 hour client-side; the response carries Cache-Control: public, max-age=3600.

51REST API#rest-webhooks

Webhooks, subscribe, replay, verify

Subscribe to any lifecycle event. Deliveries are HMAC-SHA256 signed against a per-endpoint secret and a request timestamp to prevent replay. Retries use exponential backoff over ~72 hours with up to 10 attempts. Replay history is retained for 2 years; the dashboard exposes the last 30 days inline and older deliveries on request.

#Create a subscription

sub = client.webhooks.create(
    url="https://api.your-app.com/flo-webhook",
    events=["mint.*", "redeem.*", "borrow.*", "bridge.*"],
    secret="whsec_replace_with_your_signing_secret",
)
print(sub.id, sub.url)

#Response

Flo stores a salted hash of your signing secret and never echoes it back; only the last four characters are returned for confirmation.

json{
  "object": "webhook",
  "id": "whsub_0d2a91",
  "url": "https://api.your-app.com/flo-webhook",
  "events": ["mint.*", "redeem.*", "borrow.*", "bridge.*"],
  "secret_last4": "cret",
  "status": "enabled",
  "created_at": "2026-05-15T14:33:02Z"
}

#Event families

Every primitive (mint, redeem, supply, withdraw, borrow, repay) follows the same async order_id lifecycle: createdsettled or createdcancelled.

  • mint.*, queued, created, settled, cancelled
  • redeem.*, queued, created, settled, cancelled
  • order.*, armed, triggered, cancelled. Trigger lifecycle for stop-loss, take-profit, and OCO. Settlement after a fired trigger emits a redeem.settled event under the same order_id.
  • supply.*, created, settled, cancelled, nav_updated
  • withdraw.*, created, settled, cancelled
  • borrow.*, created, settled, cancelled, maintenance_breach, liquidation_imminent, liquidated
  • repay.*, created, settled, cancelled
  • bridge.*, queued, committed, delivered, requires_manual_execution, lane_cursed
  • position.*, nav_updated
  • fee.*, accrued, settled, refunded
  • tokens.deployed, fires the first time a Flo token contract lands on Arbitrum.

#Delivery headers

FieldTypeDescription
X-Flo-SignaturestringHMAC-SHA256 hex digest of timestamp + "." + raw_body, signed with your endpoint secret.
X-Flo-Signature-TimestampintegerUnix seconds at which Flo signed the request. Reject deliveries whose timestamp is more than 300 seconds off from your server clock, this kills replay attacks.
X-Flo-Delivery-Idstringwh_.... Persist it in your handler to deduplicate at-least-once deliveries.
X-Flo-EventstringEvent type, e.g. mint.settled. Mirrors body.type.
X-Flo-Webhook-IdstringSubscription ID. Useful when a single handler serves multiple subscriptions.

#Signature verification (replay-safe)

import hmac, hashlib, time

SKEW_SECONDS = 300  # 5 minutes

def verify(body: bytes, secret: str, signature: str, timestamp: str) -> bool:
    # 1. Reject stale events to kill replay attacks.
    ts = int(timestamp)
    if abs(time.time() - ts) > SKEW_SECONDS:
        return False
    # 2. HMAC is over "${timestamp}.${raw_body}", never just the body.
    signed_payload = f"{timestamp}.".encode() + body
    expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Framework-agnostic usage
# verify(request.body, "whsec_...", request.headers["X-Flo-Signature"], request.headers["X-Flo-Signature-Timestamp"])
Always use the raw request body, don't JSON-parse then re-stringify before verifying. Express users: app.use("/flo-webhook", express.raw({ type: "application/json" })). Next.js route handlers: use await req.text() before JSON.parse.

#Retry schedule

When your endpoint returns a non-2xx or times out (10 s), Flo retries on this schedule with full jitter. After the 10th failed attempt (~72 hours later) the delivery is marked abandoned and appears in the dashboard as a failed delivery until you replay it manually.

FieldTypeDescription
Attempt 1t + 0sImmediate, counts as the first delivery.
Attempt 2t + 30s
Attempt 3t + 5m
Attempt 4t + 30m
Attempt 5t + 2h
Attempt 6t + 6h
Attempt 7t + 12h
Attempt 8t + 24h
Attempt 9t + 48h
Attempt 10t + 72hFinal. After this the delivery is abandoned.

Flo treats any 2xx status as a successful delivery, including 200, 201, 202, 204. A slow 200(> 10 s) still counts as timeout and triggers a retry. Respond fast, do heavy work async.

#Idempotent handler pattern

At-least-once means the same event can arrive twice. Persist X-Flo-Delivery-Id on first processing and drop duplicates.

seen: set[str] = set()  # use Redis/DB in prod

async def handle(req):
    delivery_id = req.headers["X-Flo-Delivery-Id"]
    if delivery_id in seen:
        return Response(status=200)  # idempotent drop
    if not verify(req.body, SECRET, req.headers["X-Flo-Signature"], req.headers["X-Flo-Signature-Timestamp"]):
        return Response(status=401)
    seen.add(delivery_id)
    await enqueue_for_processing(json.loads(req.body))
    return Response(status=200)

#Replay from the dashboard

Every delivery attempt is captured for up to 2 years; the dashboard surfaces the last 30 days inline. From Webhooks → deliveries you can:

  • View the raw request body, headers, response status, and latency of every attempt.
  • Replay a single delivery to the original endpoint or redirect it to a different URL (useful when rolling forward a fix).
  • Bulk-replay every abandoned delivery within a time range after a rollout fixes a handler bug.

#Testing webhooks locally

You have three options. In order of what we'd reach for:

  1. Flo CLI tunnel, flo webhooks listen --forward-to localhost:3000/flo-webhook. Installs a short-lived endpoint on Flo's side that streams deliveries to your machine over HTTPS. Prints the effective signing secret so your verifier works unchanged. Sandbox-only.
  2. Dashboard trigger, from Webhooks pick any event type and click Send test delivery. Fires a synthetic event through the full signing + retry stack so you can exercise your handler without a real mint.
  3. ngrok, ngrok http 3000, paste the public URL into the subscription URL, done. Works in sandbox and live; remember to use a separate subscription so live keys never touch your laptop.
52REST API#rest-tokens

Token catalog

The /v1/tokenssurface is a read-only catalog of every instrument Flo has tokenized: supply, NAV in the asset's own accounting unit (nav.value + nav.unit, never USDF), per-chain distribution, issuer identifier, audit URLs. Updated continuously, do not cache beyond 60 s.

# List the full universe (auto-paginates).
for token in client.tokens.list(limit=100):
    print(token.ticker, token.supply, token.nav.value, token.nav.unit)

# Retrieve one, full issuer record + latest attestation URL.
aapl = client.tokens.retrieve("AAPL")
print(aapl.issuer_id, aapl.attestation_url)

#Response

json// GET /v1/tokens: paginated catalog (list shape)
{
  "object": "list",
  "data": [
    {
      "object": "token",
      "ticker": "AAPL",
      "symbol": "AAPLf",
      "name": "Apple Inc.",
      "supply": "18421.084523",
      "nav": { "value": "1.000000", "unit": "share", "as_of": "2026-05-15T14:33:00Z" },
      "issuer_id": "iss_flo_global_markets",
      "chains": [8453, 42161, 1]
    }
  ],
  "has_more": true,
  "next_cursor": "tok_eyJpZCI6IkFBUEwifQ"
}

// GET /v1/tokens/AAPL: full issuer record + attestation URL
{
  "object": "token",
  "ticker": "AAPL",
  "symbol": "AAPLf",
  "name": "Apple Inc.",
  "supply": "18421.084523",
  "nav": { "value": "1.000000", "unit": "share", "as_of": "2026-05-15T14:33:00Z", "stale": false },
  "issuer_id": "iss_flo_global_markets",
  "distribution": [
    { "chain": 8453,  "supply": "12044.220100" },
    { "chain": 42161, "supply": "5102.864400" },
    { "chain": 1,     "supply": "1274.000023" }
  ],
  "attestation_url": "https://attest.flo.finance/ATT-2026-05-15-AAPL.pdf",
  "updated_at": "2026-05-15T14:33:00Z"
}
53REST API#rest-customers

Customers & attestations

Flo does not run KYC or KYB. You use any vendor you want, Jumio, Onfido, Persona, Sumsub, Middesk, Alloy, Veriff, or your own in-house stack, and post the resulting attestationto Flo. Flo normalizes it, tags it to the customer's wallet + external ID, tamper-hashes it, and indexes it against that customer's on-platform activity. Raw PII storage is opt-in (encrypted behind a customer-managed key), the default is hash-only. Included free.

#Submit an attestation

customer = client.customers.upsert(
    external_id="acme-user-1294",
    wallet="0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
    attestation={
        "kind": "kyc",
        "provider": "jumio",
        "vendor_reference": "JMO-2026-04-19-aX9k",
        "attested_at": "2026-04-19T10:22:00Z",
        "expires_at": "2028-04-19T10:22:00Z",
        "signals": ["document_verified", "liveness_verified", "sanctions_clear"],
        "attestation_hash": "0x91fe...a04c",
        "pii": "hash_only",
    },
)
print(customer.id, customer.attestation.id)

#Attestation schema

FieldTypeDescription
kindrequired"kyc" | "kyb"Individual (kyc) or business (kyb). Drives reporting + dashboard filtering.
providerrequiredenum | "custom"One of jumio, onfido, persona, sumsub, middesk, alloy, veriff, or custom. Unrecognized values must use custom + a provider name in provider_name.
vendor_referencerequiredstringStable ID returned by your vendor (Jumio txn ID, Onfido check ID, Middesk business ID). Used for audit back-trace.
attested_atrequiredISO-8601When the vendor signed off. Used to compute staleness.
expires_atISO-8601Optional. Flo fires customer.attestation_expiring 30 days out and customer.attestation_expired on the day.
signalsrequiredstring[]Any of document_verified, liveness_verified, sanctions_clear, pep_check_clear, accreditation_verified, ubo_verified, source_of_funds_verified, addresses_verified. Query-filterable.
attestation_hashrequiredhex (0x…)SHA-256 of the raw attestation payload. Lets auditors prove the record wasn't tampered with.
pii"hash_only" | "encrypted"Default hash_only, Flo stores metadata + hash, never the raw payload. Switch to encrypted and include encrypted_payload to store a ciphertext blob behind your KMS key.
encrypted_payloadbase64 ciphertextOptional. Present only when pii: "encrypted". Encrypted at-rest with the key referenced by kms_key_id.
kms_key_idstringOptional AWS KMS / GCP KMS / HashiCorp Vault key alias for the encrypted blob. Flo never holds the key, decryption is always customer-driven.

#Response

json{
  "id": "cus_k01m84",
  "object": "customer",
  "external_id": "acme-user-1294",
  "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
  "attestation": {
    "id": "att_0f9a21",
    "kind": "kyc",
    "provider": "jumio",
    "vendor_reference": "JMO-2026-04-19-aX9k",
    "attested_at": "2026-04-19T10:22:00Z",
    "expires_at": "2028-04-19T10:22:00Z",
    "signals": ["document_verified", "liveness_verified", "sanctions_clear"],
    "attestation_hash": "0x91fe...a04c",
    "pii": "hash_only",
    "stored_at": "2026-04-19T10:22:04Z"
  }
}

#List & query by signal

for c in client.customers.list(signal="accreditation_verified", min_notional=250_000):
    print(c.id, c.wallet, c.attestation.expires_at)

#Response

json{
  "object": "list",
  "data": [
    {
      "id": "cus_k01m84",
      "object": "customer",
      "external_id": "acme-user-1294",
      "wallet": "0x4a8B70fa12cC3d9d3c9b22Ac019bE40a7d11",
      "lifetime_notional_usd": "412903.55",
      "attestation": {
        "id": "att_0f9a21",
        "kind": "kyc",
        "provider": "jumio",
        "signals": ["document_verified", "liveness_verified", "sanctions_clear", "accreditation_verified"],
        "attested_at": "2026-04-19T10:22:00Z",
        "expires_at": "2028-04-19T10:22:00Z"
      }
    }
  ],
  "has_more": false,
  "next_cursor": null
}

#Query surface

The attestation index is designed to answer the questions auditors actually ask. A few examples you can GET directly:

  • GET /v1/customers?signal=accreditation_verified&min_notional=250000, every customer with active accreditation whose mint history tops $250k.
  • GET /v1/customers?attestation.expires_before=2026-06-01, every attestation that'll expire before your next audit cycle, so you can chase re-verifications early.
  • GET /v1/customers?provider=sumsub&attested_after=2026-01-01, provenance query for a specific vendor (useful when a vendor has a data-breach incident and you need exposure count).
Attestation storage is always free. Flo is not in the KYC business, we're the reconciliation layer that stitches your compliance records to your on-chain activity so your auditor has one place to look.

Reconciliation reports

Daily cash / position break reports between the prime-broker statements and onchain supply. The reconciliation job runs at 00:00 UTC; diffs above $100 automatically page on-call and pause mints for the affected ticker until resolved.

report = client.reconciliation.daily(date="2026-04-21")
print(report.status, report.summary.diff_usd)
for line in report.lines:
    if line.status == "break":
        print("BREAK:", line.token, line.chain, line.diff)

#Response schema

json{
  "object": "reconciliation_report",
  "date": "2026-04-21",
  "status": "matched",
  "generated_at": "2026-04-22T00:04:12Z",
  "summary": {
    "tokens_checked": 8124,
    "breaks": 0,
    "total_onchain_usd": "412,038,217.55",
    "total_broker_usd":  "412,038,217.55",
    "diff_usd":          "0.00"
  },
  "lines": [
    {
      "token": "AAPL",
      "chain": 8453,
      "onchain_supply":   "18421.084523",
      "broker_position":  "18421.084523",
      "diff":             "0.000000",
      "nav":              { "value": "1.000000", "unit": "share" },
      "broker_statement_value_usd": "3,452,307.61",
      "status": "matched",
      "attestation_url": "https://attest.flo.finance/ATT-2026-04-21-AAPL.pdf"
    }
  ],
  "pagination": { "has_more": false, "next_cursor": null }
}

#Field reference

FieldTypeDescription
dateISO dateUTC calendar day the report covers.
status"matched" | "break" | "pending"matched = every line matched; break = one or more diffs > $100; pending = job still running.
summary.breaksintegerCount of lines whose diff exceeded the $100 threshold.
summary.diff_usddecimalAggregate absolute-value USD difference across all lines. Should round to 0 on a clean day.
lines[].tokenstringTicker.
lines[].chaininteger (EIP-155 chain ID)Which chain this line is for. A ticker with distribution across 3 chains produces 3 lines.
lines[].onchain_supplydecimalSum of ERC-20 balances across all holder wallets at 00:00 UTC.
lines[].broker_positiondecimalBroker-of-record share count for the Flo Capital SPC vault at 00:00 UTC (T+0 cash-basis).
lines[].diffdecimalonchain_supply − broker_position. Positive = tokens over-issued. Negative = under-issued. Should be 0.000000.
lines[].status"matched" | "break"Per-line status. break auto-pauses mint for (token, chain) until resolved.
lines[].attestation_urlstringPDF attestation for that line from our independent third-party attestor.
If status === "break" your affected tickers will return mint_paused_reconciliation on any new mint until operations clear the break. Redeems are not paused, holders can always exit.
55Conventions#idempotency

Idempotency

Every mutating SDK call and REST endpoint accepts an Idempotency-Key. Flo dedupes against the key for 24 hours; retries after the first successful response return the cached result verbatim, including the original tx hash.

  • Keys must be <= 128 bytes, case-sensitive.
  • If the cached response is a 5xx, the next retry re-runs the request. We do not pin transient failures.
  • Cross-environment collisions are impossible, sandbox and live have independent idempotency stores.
  • SDKs generate a UUIDv4 automatically when you don't pass one; override only when you want app-side control.
56Conventions#rate-limits

Rate limits

Per-key requests-per-second:

  • Default: 1,000 RPS
  • Sandbox: effectively uncapped (soft 10,000 RPS)
  • Higher caps: available on request. Write in to enterprise@flo.finance

Responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. On a hard 429 we include Retry-After in seconds. SDKs back off automatically with full jitter up to max_retries.

#Burst allowance

Every account allows a 2× burst for the first 10 seconds of a cold window. After that, sustained traffic is shaped to the account cap. Read endpoints (GET) do not consume the mutating-budget quota.

57Conventions#pagination

Pagination

All list endpoints are cursor-paginated. Pass ?limit=100&starting_after=obj_…; responses return has_more and next_cursor. Max limit is 100.

# SDK auto-paginates
for position in client.positions.list():
    ...
json// One page of a cursor-paginated list response
{
  "object": "list",
  "data": [
    { "object": "position", "token": "AAPL", "quantity": "10.000000" },
    { "object": "position", "token": "SPY", "quantity": "4.250000" }
  ],
  "has_more": true,
  "next_cursor": "obj_eyJpZCI6InBvc18wMDk0In0"
}

Pass next_cursor back as starting_after to fetch the next page; the SDK iterators above do this for you until has_more is false.

58Conventions#errors

Errors, Problem-JSON

Every non-2xx returns a Problem-JSON envelope (RFC 7807 + Flo extensions). The envelope carries the standard fields plus error-specific context you can switch on without string-matching.

FieldTypeDescription
typeURICanonical URL for the error code. Dereference at docs.flo.finance/errors/<code>.
titlestringShort human-readable summary.
statusintegerHTTP status (echoed for offline log analysis).
detailstringLonger human-readable explanation with concrete values where applicable.
instancepathThe request path that triggered it.
request_idstringreq_…. Include this in support tickets, it indexes every log line end-to-end.
codestringMachine-readable code, switch on this, not on the HTTP status.
retryablebooleanWhether the same request with the same idempotency key may succeed on retry.

#Retry / idempotency matrix

Rule of thumb. 4xx with retryable: false is permanent, fix the request first. 5xx and 429 are transient and safe to retry with the same idempotency key. 409 idempotency_mismatch is the special case: use a fresh key, not a retry.

#Full error catalog

FieldTypeDescription
unauthorized401 · permanentMissing or malformed Bearer token.
forbidden403 · permanentKey is valid but lacks scope for this resource.
key_lacks_live_scope403 · permanentUsing a sk_test_ key against api.flo.finance or vice-versa. Switch env.
ip_not_allowed403 · permanentRequest origin outside the IP allowlist attached to this key.
kyc_required403 · permanentLive operation attempted before partner org-level KYC/KYB approval. Flo gates SDK access at the partner layer; end-user wallets are not KYC-gated by Flo. Response includes kyc_status and kyc_flow_url.
jurisdiction_restricted403 · permanentThe API key's configured jurisdiction list does not include this asset class. Enforced at the SDK access layer; token contracts have no on-chain wallet allowlist. Response includes country and category.
asset_not_found404 · permanentTicker not in the universe or delisted mid-session.
position_not_found404 · permanentNo position resolves for the given (asset, wallet) pair on this account. Returned by GET /v1/positions/{asset}?wallet=… when the holder has no balance.
idempotency_mismatch409 · permanentSame Idempotency-Key with different body. Use a new key.
broker_leg_in_flight409 · partialCancel arrived after the broker partial-filled. Body includes executed_quantity and executed_price so the partner can settle the partial; the unfilled remainder is cancelled and refunded.
limit_price_unreachable410 · permanentA limit order's good_till expired without the mark crossing limit_price. Order moved to cancelled; locked notional / tokens refunded.
trigger_price_invalid422 · permanentArming a trigger with an invalid threshold (stop_loss above mark, or take_profit below mark). Body includes mark and trigger_price.
slippage_too_tight422 · permanentThe fee-and-slippage math would set min_quantity below the 10⁻⁶ dust floor. Loosen slippage_bps or raise notional_usd.
gas_budget_insufficient402 · permanentgas.sponsor is developer and the attached gas budget can't cover the on-chain step. Top up and retry.
gas_exceeds_max422 · permanentgas.sponsor is user_notional and the live gas estimate would exceed gas.max_gas_usd. Raise the cap or switch to developer.
mint_paused_reconciliation409 · transientReconciliation break; retry after the break is resolved (status page updated).
rate_limited429 · retryableOver your account RPS cap. retry_after_seconds in body, Retry-After header mirrors it.
ltv_exceeded422 · permanentRequested borrow would push the position's LTV above the asset-class initial margin. Body: initial_ltv_bps, requested_ltv_bps. Reduce borrow.amount or post more collateral.
insufficient_funds422 · permanentSource wallet balance below required. required_usd includes the developer_fee deducted at the front of the fee math, plus gas_usd when gas.sponsor is user_notional. Body: required_usd, available_usd, shortfall_usd.
guardrail_triggered422 · retry-after-windowPer-block circuit breaker. Body: limit, limit_value, current_value, retry_after_seconds.
bridge_route_unsupported422 · permanentFlo doesn't support this (from_chain, to_chain) pair yet.
currency_not_supported_on_chain422 · permanentThe currency you sent isn't in the chain's accepted set for this direction. Body: chain_id, direction (pay_in | pay_out), currency, accepted (string[]). Re-fetch the Chains catalog and pick a supported currency.
internal_error500 · retryableFlo-side. Retry with the same idempotency key.
service_unavailable503 · retryableScheduled maintenance or degraded upstream. Retry with backoff.

#Example bodies

rate_limited (429), standard SDK catches this and auto-retries up to max_retries.

json{
  "type": "https://docs.flo.finance/errors/rate_limited",
  "title": "Too many requests",
  "status": 429,
  "detail": "Exceeded 1000 RPS on key sk_live_••a19c.",
  "instance": "/v1/mint",
  "request_id": "req_0b1204",
  "code": "rate_limited",
  "retryable": true,
  "retry_after_seconds": 2,
  "limit_rps": 1000,
  "observed_rps": 1184
}

insufficient_funds (422), permanent. Surface shortfall_usd to your user.

json{
  "type": "https://docs.flo.finance/errors/insufficient_funds",
  "title": "Insufficient USDF to settle this trade",
  "status": 422,
  "detail": "Need 1,842.05 USDF, available 1,200.00 USDF.",
  "instance": "/v1/mint",
  "request_id": "req_0b1205",
  "code": "insufficient_funds",
  "retryable": false,
  "required_usd":  "1842.05",
  "available_usd": "1200.00",
  "shortfall_usd":  "642.05"
}

guardrail_triggered (422), per-block circuit breaker. Body names the limit you hit so you can cool down or back off.

json{
  "type": "https://docs.flo.finance/errors/guardrail_triggered",
  "title": "Mint rejected by per-block guardrail",
  "status": 422,
  "detail": "Your address exceeded mint_max_notional_per_address_per_block at block 8102443.",
  "instance": "/v1/mint",
  "request_id": "req_0b1206",
  "code": "guardrail_triggered",
  "retryable": true,
  "limit": "mint_max_notional_per_address_per_block",
  "limit_value": "250000.00",
  "current_value": "268420.50",
  "retry_after_seconds": 12
}

idempotency_mismatch (409), same key, different body. Use a fresh idempotency key.

json{
  "type": "https://docs.flo.finance/errors/idempotency_mismatch",
  "title": "Idempotency-Key already used with a different request body",
  "status": 409,
  "detail": "Key 'mint_2026_04_21_0001' was first used with a different asset or quantity.",
  "instance": "/v1/mint",
  "request_id": "req_0b1208",
  "code": "idempotency_mismatch",
  "retryable": false,
  "original_request_id": "req_0af9d5",
  "action": "Retry with a different Idempotency-Key."
}

broker_leg_in_flight (409), the cancel raced a partial fill. Settle the partial from executed_quantity and executed_price; the unfilled remainder is cancelled and refunded automatically.

json{
  "type": "https://docs.flo.finance/errors/broker_leg_in_flight",
  "title": "Cancel arrived after broker partial fill",
  "status": 409,
  "detail": "Filled 4.000000 of 10.000000 AAPL at 184.21 before cancel landed.",
  "instance": "/v1/orders/cancel",
  "request_id": "req_0b1209",
  "code": "broker_leg_in_flight",
  "retryable": false,
  "order_id": "0x7f3a91c2e4b8...",
  "executed_quantity": "4.000000",
  "executed_price": "184.21",
  "remainder_status": "cancelled"
}

kyc_required (403), hand the user kyc_flow_url for a hosted flow, or call /v1/customers to create a link programmatically.

json{
  "type": "https://docs.flo.finance/errors/kyc_required",
  "title": "Live operation requires an approved KYC record",
  "status": 403,
  "detail": "Your organization has not completed KYC. Sandbox remains available.",
  "instance": "/v1/mint",
  "request_id": "req_0b120a",
  "code": "kyc_required",
  "retryable": false,
  "kyc_status": "not_started",
  "kyc_flow_url": "https://dashboard.flo.finance/kyc"
}
Type-narrow on code, not on HTTP status. Two 422s can need very different UX, ltv_exceeded needs a collateral suggestion, insufficient_funds needs a top-up CTA, guardrail_triggered is a backoff.
59Conventions#versioning

Versioning

The URL carries the current major version (/v1/…). Breaking changes cut a new major; we commit to supporting the previous major for 18 months after a new one ships. Pin your SDK version in production, we publish non-breaking additions weekly.

Opt into dated non-breaking variants via the Flo-Version header (ISO date). When a header is absent, your account is pinned to the version on the first successful live call; you can upgrade the pin from the dashboard.

60Security and trust#security-overview

Security posture

Flo enforces defense-in-depth across four boundaries: API edge, SDK guardrails, on-chain contracts, and prime-broker custody. Each boundary is independently auditable and independently pauseable.

  • SDK access edge: IP geofencing of API requests, partner-counterparty sanctions screening at KYB onboarding, partner-supplied KYC attestation storage (Flo does not run end-user KYC), per-key rate limiting, and the per-block guardrails below. End-user wallets are permissionless at the token-contract layer. There is no on-chain allowlist and Flo does not gate mint or redeem on end-user attributes.
  • Onchain: Chainlink CCIP with the Risk Management Network. 72-hour timelock on parameter changes. Pause is single-signer (any security council member); unpause requires a 5-of-9 quorum on the same Gnosis Safe that owns smart-contract upgrades and admin rights. One multi-sig, two thresholds.
  • Issuer + SPC: Flo Global Markets Ltd. (BVI) is the issuer entity. Flo Capital SPC (Cayman) is a bankruptcy-remote Segregated Portfolio Company with a trust-like shareholding arrangement and an independent director on the board; the BVI issuer is the sole investor in the SPC. An independent corporate trustee holds first-priority security interest.
  • Custody: segregated omnibus at IBKR + Alpaca; daily onchain supply ≤ custodied underlying, attested monthly.
61Security and trust#security-guardrails

Per-block guardrails

Every mint, redeem, supply, withdraw, borrow, repay, and bridge call passes through a set of conservative circuit breakers. Exact thresholds live on the Transparency page and are tuned per-asset; the categories below are the shape of what's enforced. A request that would breach any breaker is rejected with guardrail_triggered.

#Mint

  • mint_max_notional_per_address_per_block, per-wallet notional cap within a single block. Prevents fat-finger and MEV-style stuffing.
  • mint_max_total_per_block, platform-wide cap per block. Protects prime-broker fill capacity.
  • mint_min_per_tx, floor on gas-wasting dust (e.g. $10).
  • mint_daily_notional_per_asset, rolling-window cap per ticker; gates concentration.

#Redeem

  • redeem_max_per_block, mirrors the mint cap; prevents run-on-the-bank behavior under stress.
  • redeem_cool_down, optional per-wallet cooldown when the daily redemption ratio exceeds a tunable threshold; configurable on request via enterprise@flo.finance.

#Borrow / Repay

  • borrow_max_notional_per_address, concentration cap per wallet.
  • borrow_max_total_outstanding, systemic cap on the borrow book. Protects the liquidation engine's ability to unwind in adverse markets.
  • Initial / maintenance / liquidation margins per asset class are published in Borrow overview.

#Supply / Withdraw

  • supply_max_per_block, platform-wide cap per block on stablecoin entering the pool.
  • withdraw_queue_cap, ceiling on the FIFO queue depth; new withdrawals beyond the cap return queue_full for the partner to retry once depth clears.
  • utilization_emergency_pause, optional pause on new borrow originations once utilization exceeds the second kink for an extended window.

#Bridge

  • bridge_min_amount_per_tx, dust floor.
  • bridge_max_amount_per_tx, per-message risk cap enforced before the CCIP message is committed.
  • bridge_max_total_per_block, per-route rate limit.
Exact values are published as order-of-magnitude bands to enterprise partners under NDA. If you hit a guardrail in production, the error body includes the limit you breached.
62Security and trust#security-ccip

Chainlink CCIP + Risk Management Network

Flo uses Chainlink CCIP on two distinct surfaces: Bridge moves tokenized assets between chains, and Wallet · Depositsends a message from the source chain to Arbitrum to credit the user's wallet balance. Both surfaces inherit the same two-network validation model and the same failure semantics; the difference is what the message carries.

Every CCIP message is validated by two independent Decentralized Oracle Networks: a Committing DON commits the message on the source chain, an Executing DON delivers it on the destination. In parallel, the Risk Management Network (RMN), run on a separate codebase by independent operators, monitors every message and can curse a lane if it detects anomalous activity. No single network can sign a fraudulent message. Source effects are irreversible at the protocol level; failed destination executions enter CCIP's 8-hour Smart Execution window for auto-retry, then move to manual execution via the CCIP Explorer. See Bridge → Failure semantics and Wallet → Deposit failure semantics for the per-surface lifecycles.

#Why two-network design

The Committing / Executing split prevents a compromised oracle set from forging messages, since both networks must independently attest. The RMN adds a separate kill switch operated by a different group of node operators on a different codebase, so a single-codebase or single-operator-set compromise cannot land a malicious mint.

Additional lane-level controls (per-lane caps, manual-execution requirements, or RMN-cursed alert webhooks) are available on request via enterprise@flo.finance.
63Security and trust#security-structure

Issuer + SPC structure & legal wrapper

  • Operating entity: Flo Finance, Inc., incorporated in Delaware.
  • Issuer entity: Flo Global Markets Ltd. (BVI). Issues structured notes under the FMA-approved Liechtenstein Base Prospectus (Swiss-law tokens); onboards the partner counterparty and runs counterparty AML, KYC, and sanctions screening at the BVI layer (not on end-user wallets, those are the partner's responsibility under the SDK access terms).
  • Asset-holding entity: Flo Capital SPC (Cayman). Bankruptcy-remote Segregated Portfolio Company with a trust-like shareholding arrangement and an independent director on the board. Single-purpose asset-holding charter; the BVI issuer is the sole investor; perfected security interests are current and enforceable; rotated monthly for attestation.
  • Security agent: independent corporate trustee, holds first-priority perfected security interest over the SPC's assets, authorized to initiate liquidation on LTV breach.
  • Holder claim: direct legal claim on the underlying asset held by Flo Capital SPC, not a claim on any Flo operating entity. If Flo shuts down, tokens remain redeemable through the BVI issuer and the SPC estate, and the security agent.
64Security and trust#security-custody

Custody & attestations

Underlying securities are held in segregated omnibus at Interactive Brokers and Alpaca Securities. SIPC coverage applies on the IBKR side ($500K). Daily reconciliation guarantees that onchain supply never exceeds custodied underlying; violations pause mints automatically and page on-call.

#Audit cadence

  • Smart contract audits: Sherlock, Halborn, Cantina, and Cyfrin, four independent firms. Every contract audited before mainnet.
  • Reserve attestation: daily, issued by an independent third-party attestor (Accountable) and served via the public Accountable API. See Daily attestations.
  • Proof of Reserves: Merkle-tree-based, onchain contract. Root recomputable from a JSON leaf. See Proof of reserves architecture and Verifying proof of reserves.
  • SOC 2 Type II: annual; currently in progress.
65Security and trust#por-architecture

Proof of reserves architecture

Flo publishes a two-layer daily proof of reserves so any partner can verify, at any time, that on-chain token supply matches the underlying held in segregated brokerage accounts at our prime broker partners (Interactive Brokers and Alpaca Securities).

#The two layers

  1. Daily Merkle root snapshot at 00:00 UTC. A full Merkle tree of every holding is rebuilt and the root is posted on-chain on Base, Arbitrum, and Ethereum. The corresponding broker-side attestation (signed by IB and Alpaca) is published alongside. Every holder can prove their leaf membership against the on-chain root.
  2. Daily third-party attestation by Accountable. Independent verifier reconciles on-chain supply against broker-side holdings and publishes a signed report each day, served via the public Accountable API. See Daily attestations.

#Contract address

The PoR contract address publishes at public launch. Until then, the contract runs in private beta against the same Merkle and attestation logic; the beta address is available on request via compliance@flo.finance.

66Security and trust#por-verification

Verifying proof of reserves

Two ways to verify: a CLI call against the published Merkle root, or in-browser using the verifier loaded from the proof page. Both produce the same answer: yes/no, your leaf is in the committed set, and the on-chain supply matches the broker-side total.

#CLI verification

npx @flo/por-verify \ --root 0xabcdef... \ --leaf 0x1234... \ --proof proof.json

The CLI fetches the published Merkle proof JSON for your snapshot, recomputes the leaf hash against the on-chain root, and compares the broker-side attestation signature.

#In-browser verification

The proof page (/transparency/proof-of-reserves) runs the same verification entirely in your browser. No data leaves the page; the proof and root are fetched on-chain and verified client-side.

#Proof JSON schema

Each daily snapshot publishes a Merkle tree as JSON.

{
  "root": "0xabc...",
  "snapshot_at": "2026-05-05T00:00:00Z",
  "chain": 1,
  "leaves": [
    {
      "token": "AAPLf",
      "supply": "12345.67",
      "broker_attestation_url": "https://attest.flo.finance/..."
    }
  ]
}
67Security and trust#daily-attestations

Daily attestations

Independent third-party attestation by Accountable, Flo's specialised verification provider for tokenized assets. Every day at 00:00 UTC, Accountable publishes a signed report covering the prior 24-hour reconciliation, served via the public Accountable API. No NDA, no waitlist, no PDF gate.

#What's in each report

  • Total on-chain token supply per series, snapshot timestamp, and chain.
  • Total underlying held at Interactive Brokers and Alpaca Securities, by CUSIP.
  • 1:1 reconciliation between on-chain supply and broker-side holdings.
  • Independence statement and attestor's identity.
  • Methodology notes for any timing differences.

#How to access

Attestations are served via Accountable's public API. Query the latest snapshot, any historical date, or subscribe to webhook notifications. The API is unauthenticated for read access. Endpoint and OpenAPI schema linked from /transparency/proof-of-reserves.

#Cadence

Reports cover 24-hour windows. Each daily snapshot is reconciled and signed by Accountable, and published via API on the same day as the corresponding 00:00 UTC Merkle-root snapshot.

#The two-layer model

Daily attestations are the second layer of Flo's proof of reserves stack, sitting alongside the daily on-chain Merkle-root snapshot. See Proof of reserves architecture for the full picture.

68Limits and restrictions#limits-non-us

Non-US restriction

Flo's offering is non-US only. Flo does not solicit, onboard, or knowingly serve US persons through the partner SDK.

#Partner obligation

  • Your terms of service must restrict the offering of Flo-issued tokens to non-US end users.
  • Your KYC stack must capture residency at minimum, and you must geofence US end users in your product.
  • Flo does not enforce US-person checks at the token layer. The token contracts are permissionless on chain. The compliance perimeter sits at your KYC layer, not Flo's smart contracts.

For the regulatory wrapper that supports this offering see Security · Issuer + SPC structure.

69Limits and restrictions#limits-minimums

Minimums, rate limits, circuit breakers

Flo is built for $1 minimum per trade. There is no separate per-customer rate limit beyond your API key's tier; per-block circuit breakers protect the protocol from a single-block spike.

70Status and support#status

System status

Live uptime, per-region latency, and incident feed at status.flo.finance. Incidents post within 60 seconds of detection. Historical uptime for the past 90 days is published per component (mint, redeem, borrow, bridge, webhooks, dashboard).

SLA: 99.9% target uptime, with a credits schedule for any deviation. Custom SLA terms are available on request via enterprise@flo.finance. Credits auto-apply to the next invoice when downtime crosses a threshold.
71Status and support#postman

Postman collection

Every REST endpoint is pre-baked in our Postman collection with sandbox base URLs and example bodies. Swap the key env var and hit Send, no SDK install required for exploratory work.

  • Download collection , or paste into Postman Web via Import → Link: https://flo.finance/assets/data/flo-api.postman_collection.json.
  • Download environment , switches between sandbox and production with one env flip.
72Status and support#support

Support & contact

Missing something? Email developers@flo.finance.