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.
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.
#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
mintcall 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.
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
- Partner calls
flo.mintwith an asset symbol, notional, and settlement instructions. - Flo locks the notional on-chain, returns an
order_id, and routes the order to the broker. - Broker fills the order at the venue.
- 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.
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.
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"
}sk_test_… for sk_live_… and env: "live" after partner KYB and the same eight lines run against a real prime-broker fill.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.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"
}#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.
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;
}
}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-Clockheader 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/resetwipes 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
~$100nominal 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"
}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,GETonly. 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.
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.
| Field | Type | Description |
|---|---|---|
| Base URL | string | https://api.flo.finance vs https://sandbox.flo.finance |
| Key prefix | string | sk_live_ vs sk_test_ |
| Settlement | boolean | Real prime-broker fills vs synthetic test network |
| Partner KYC/KYB required | boolean | Yes for live · No for sandbox. Org-level only; end-user wallets are not gated by Flo. |
| Rate limits | default + headroom | 1,000 RPS default in live · effectively unlimited in sandbox. See Rate limits. |
| Webhooks | separate | Subscribe per-environment; events don't cross over |
sk_test_… key will return 401 key_lacks_live_scope against api.flo.finance, and vice-versa.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.
- Issuance. Flo Global Markets Ltd. (BVI) issues the token under the Liechtenstein FMA-approved base prospectus, EEA-passported across 30 states.
- 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.
- 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.
- 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/mintexample 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.
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.
#End-to-end flow
- User sends USDC or USDT to the Flo deposit contract on a supported source chain.
- A Chainlink CCIP message dispatches from the source chain to Arbitrum. Once it executes, the user's Flo wallet balance is credited 1:1.
- 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.
- Trade. Every primitive draws notional from, or returns notional to, the wallet balance.
- 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.
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.
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
| Field | Type | Description |
|---|---|---|
| assetrequired | string | Ticker, e.g. AAPL, NVDA, SPY, TLT. See /v1/tokens for the full list. |
| notional_usdrequired | number | USDF 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_bps | integer | Maximum 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.currencyrequired | string | Always 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.chainrequired | integer (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.walletrequired | address | Destination 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. |
| gas | object | Gas-fee handling. { sponsor: "developer" | "user_notional", max_gas_usd?: number }. Default developer. See Gas for the full contract. |
| developer_fee | object | Optional partner markup. Provide amount_bps, amount_flat_usd, or both (additive). See Developer fees. |
| idempotency_key | string | Dedup window 24 h. SDK auto-generates a UUIDv4 if omitted. |
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 to10⁻¹⁸of a share at the contract level. - Mint / redeem fill granularity: capped at
10⁻⁶ sharesbecause that's the broker fill size at Interactive Brokers and Alpaca. Quantities passed tomintorredeembelow 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.
predeployis 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.
CREATE2plus 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.deployedwebhook 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 addressjson// 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.
| Field | Type | Description |
|---|---|---|
| queued | intermediate | Create-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. |
| active | intermediate | Broker leg open; waiting on a fill at or above min_quantity. Stablecoin held in the order contract. Webhook: mint.created. |
| settled | terminal | Broker 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. |
| cancelled | terminal | Partner 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 covernotional_usd, including the deducteddeveloper_feeand anygaswhensponsorisuser_notional.slippage_too_tight, the fee-and-slippage math would setmin_quantitybelow the 10⁻⁶ dust floor. Loosenslippage_bpsor raisenotional_usd.gas_budget_insufficient,gas.sponsorisdeveloperand the attached gas budget can't cover the on-chain step. Top up and retry.gas_exceeds_max,gas.sponsorisuser_notionaland the deducted gas would exceedgas.max_gas_usd. Raise the cap or switch todeveloper.order_already_settled/order_already_cancelled, the on-chain order is no longerACTIVE. 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_cancelcalled before the recovery window elapsed. Body includesrecovery_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.
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.
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.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 conditionIf 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.
(asset, chain, source_wallet) is sufficient. A user can redeem tokens received via transfer or bridge, not just tokens they minted themselves.#Parameters
| Field | Type | Description |
|---|---|---|
| assetrequired | string | Ticker of the Flo token to redeem (e.g. AAPL, NVDA, SPY). The (asset, chain) pair deterministically resolves to the ERC-20 contract. |
| chainrequired | integer (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. |
| quantityrequired | number | Token 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_bps | integer | Maximum 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_walletrequired | address | EVM address that holds the tokens. The wallet signs the lock-and-create-order tx and pays gas. Must hold ≥ quantity at time of submission. |
| gas | object | Gas-fee handling. Same shape as mint. See Gas. |
| developer_fee | object | Optional partner markup, same shape as on mint. Deducted from gross proceeds before the wallet credit. See developer revenue. |
| idempotency_key | string | Dedup 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
| Field | Type | Description |
|---|---|---|
| queued | intermediate | Lock-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. |
| active | intermediate | Tokens locked, broker offset open; waiting on a fill that meets min_notional_usd after fees and gas. Webhook: redeem.created. |
| settled | terminal | Broker 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. |
| cancelled | terminal | Partner 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.
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.
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
| Field | Type | Description |
|---|---|---|
| limit_pricerequired | number | The 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_till | string | Order 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
| Field | Type | Description |
|---|---|---|
| queued | intermediate | Stablecoin pulled (mint) or tokens locked (redeem). Waiting on mark to cross limit_price. Webhook: mint.queued / redeem.queued with queue_reason: "limit_unmet". |
| active | intermediate | Mark crossed limit_price; broker leg open. Same semantics as a market order from here. |
| settled | terminal | Broker leg confirmed and settle(order_id) ran. Same as market. |
| cancelled | terminal | Partner cancelled, good_till expired, or recovery window elapsed. Returns limit_price_unreachable on good_till expiry without a fill. |
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.
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
| Field | Type | Description |
|---|---|---|
| trigger_pricerequired | number | The 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_pricerequired | number | OCO only. Both required. Same validation as the single-sided variants. |
| slippage_bps | integer | Slippage 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
| Field | Type | Description |
|---|---|---|
| armed | intermediate | Tokens locked under order_id. Watching the mark. Webhook: order.armed. |
| triggered | intermediate | Mark 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. |
| active | intermediate | Broker leg open; behaves identically to a market redeem from here. |
| settled | terminal | Tokens burned, payout landed at payout.tx_hash. Webhook: redeem.settled. |
| cancelled | terminal | Partner cancelled before the trigger fired, OCO sibling fired first, or recovery window elapsed. Locked tokens released to the source wallet. Webhook: order.cancelled. |
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.
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
| Field | Type | Description |
|---|---|---|
| order_idrequired | string | The on-chain order_id from the original SDK call response. |
| idempotency_key | string | Dedup window 24 h. Cancel is idempotent: re-submitting on an already-cancelled order returns the same response. |
#Errors
order_already_settled, the order moved tosettledbefore 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 includesexecuted_quantityandexecuted_priceso the partner can settle the partial; the unfilled remainder is cancelled and refunded.order_not_found, theorder_idis not a Flo order or was reorged out.
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.
#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"
}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
| Field | Type | Description |
|---|---|---|
| token | string | Ticker. |
| quantity | decimal | Fractional holding. |
| nav.value | decimal | NAV per Flo token, denominated in nav.unit. Multiply by quantity for the position value in that unit. |
| nav.unit | string | The 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_of | iso8601 | Timestamp of the NAV tick. |
| nav.stale | boolean | True when the NAV tick is older than the venue's stale threshold (usually 120 s). |
| yield.gross_apy_bps | integer | Trailing-30-day gross yield annualized, in basis points. |
| yield.net_apy_bps | integer | Net of any management and performance fees levied by the underlying issuer or strategy. Use this when displaying yield to the end user. |
| attestation_url | string | Current SPC / broker attestation report for this ticker. |
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
| Field | Type | Description |
|---|---|---|
| 0–75% | Normal | Most operating time. Both sides have headroom; rates are stable. |
| 75–90% | Tight | First kink. Rate climbs more steeply to discourage marginal borrows and pull new supply. |
| 90–100% | Stress | Second kink. Rate spikes hard. Borrowers are pressured to repay; suppliers earn outsized yield. New withdrawals beyond available liquidity queue. |
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.
| Field | Type | Description |
|---|---|---|
| assetrequired | string | USDF, drawn from the Flo wallet balance. |
| amountrequired | number | Notional amount in stablecoin units. |
| settlement.chainrequired | integer (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.walletrequired | address | EVM address that receives lending-vault shares. |
| idempotency_key | string | Dedup 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
| Field | Type | Description |
|---|---|---|
| active | intermediate | Stablecoin pulled, supply order created on chain. Webhook: supply.created. |
| settled | terminal | Lending-vault shares minted to settlement.wallet; yield accrual active. Webhook: supply.settled. |
| cancelled | terminal | Supply 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 currentnav, the sharetoken, thechain, and arecorded_attimestamp. 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;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.
| Field | Type | Description |
|---|---|---|
| sharesrequired | number | Lending-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.currencyrequired | string | Always 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.chainrequired | integer (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.walletrequired | address | EVM address for stablecoin payout. |
#Order lifecycle
| Field | Type | Description |
|---|---|---|
| active | intermediate | Lock-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. |
| settled | terminal | Pool liquidity covered the order. The locked shares were burned and the stablecoin payout landed on chain, in the same settle tx. Webhook: withdraw.settled. |
| cancelled | terminal | Order 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. |
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"
}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
borrowcall rejects withltv_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_breachwebhook (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_breachandborrow.liquidation_imminentwebhooks 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.
| Field | Type | Description |
|---|---|---|
| Large-cap equities | 70% / 75% / 78% | AAPL, NVDA, TSLA, MSFT, S&P 500 constituents |
| Blue-chip ETFs | 75% / 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.
- Below the utilization cap: the pool has free stablecoin. The borrow order moves
active → settledin the usual window (typically a few blocks to a few minutes). - At the utilization cap (≈ 100%): the order remains
activein 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, reasonpool_capacity_unavailable).
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
| Field | Type | Description |
|---|---|---|
| active | intermediate | Collateral locked, borrow order created. Awaiting pool disbursement at settle. Webhook: borrow.created. |
| settled | terminal | Stablecoin disbursed to borrow.wallet. Position is now interest-accruing. Webhook: borrow.settled. |
| cancelled | terminal | Pool capacity unavailable / keeper failure / user_cancel after window. Collateral is unlocked back to the source wallet. Webhook: borrow.cancelled. |
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"
}developer_fee.amount_bps on the original borrow call to charge your user a markup you keep.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):
| Field | Type | Description |
|---|---|---|
| Healthy | LTV ≤ maintenance | Position is below its maintenance margin. No alerts. Steady state. |
| In maintenance breach | maintenance < LTV < liquidation | Position 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. |
| Liquidatable | LTV ≥ liquidation | The 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 interestborrow_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-checkmaintenance_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.740AAPL 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.
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.
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.
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
| Field | Type | Description |
|---|---|---|
| amount_usdrequired | number | USD amount to credit to the Flo wallet, 1:1 with the stablecoin pulled from the source wallet. |
| source.currencyrequired | string | Stablecoin sent in. USDC or USDT. Per-chain support comes from Supported chains and stablecoins. |
| source.chainrequired | integer (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.walletrequired | address | EVM 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_key | string | Dedup 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.
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.
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
| Field | Type | Description |
|---|---|---|
| amount_usdrequired | number | USD amount to debit from the Flo wallet, 1:1 with the stablecoin paid out on the destination chain. |
| payout.currencyrequired | string | Stablecoin paid out. USDC or USDT. Per-chain support comes from Supported chains and stablecoins. |
| payout.chainrequired | integer (EIP-155 chain ID) | Destination chain ID for the payout. Always the integer chain ID; string slugs are not accepted. |
| payout.walletrequired | address | Destination EVM address. Token contracts are permissionless: no on-chain wallet allowlist. |
| idempotency_key | string | Dedup 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 asfees.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.
Bridge, move tokens across chains
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
| Field | Type | Description |
|---|---|---|
| tokenrequired | string | Ticker of the Flo token to move. |
| quantityrequired | number | Token quantity to bridge (10⁻⁶ floor, same as mint/redeem). |
| from_chainrequired | integer (EIP-155 chain ID) | Source chain ID (e.g. 8453). Always the integer chain ID; string slugs are not accepted. See Chains catalog. |
| to_chainrequired | integer (EIP-155 chain ID) | Destination chain ID. Always the integer chain ID; string slugs are not accepted. |
| destinationrequired | address | Destination 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.
#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_executionwhen a message enters this state, andbridge.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_cursedwhen 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.
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
| Field | Type | Description |
|---|---|---|
| Base (8453) | USDC, USDT | Production. CCIP source-to-Arbitrum lane live. |
| Ethereum (1) | USDC, USDT | Production. CCIP source-to-Arbitrum lane live. |
| Arbitrum (42161) | USDC, USDT | Production. Same-chain credit; no CCIP hop. |
| Polygon (137) | USDC, USDT | Production. CCIP source-to-Arbitrum lane live. |
| Optimism (10) | USDC, USDT | Production. CCIP source-to-Arbitrum lane live. |
#Withdraw, destination chains
| Field | Type | Description |
|---|---|---|
| Base (8453) | USDC, USDT | Production. On-ramp coverage live. |
| Ethereum (1) | USDC, USDT | Production. On-ramp coverage live. |
| Arbitrum (42161) | USDC, USDT | Production. No CCIP hop required. |
| Polygon (137) | USDC, USDT | Production. On-ramp coverage live. |
| Optimism (10) | USDC, USDT | Production. On-ramp coverage live. |
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 (AAPL → AAPLf).
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.
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.
FX
29+ currency pairs, wrapped as f-suffixed tokens (EURUSD → EURUSDf). 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.
Commodities
30+ commodities, wrapped as f-suffixed tokens (GOLD → GOLDf). 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.
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.
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.
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.
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.
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
- Automatic daily settlement at 00:00 UTC, USDF, $10 minimum.
- On-demand via POST /v1/fees/settle.
- Balance lookup via GET /v1/fees/balance.
#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.
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"
}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
| Field | Type | Description |
|---|---|---|
| id | integer | EIP-155 chain ID. The only accepted identifier on input: every `chain` parameter across the SDK and REST API is this integer. |
| slug | string | Stable, 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. |
| name | string | Display name for human surfaces (network selector, dashboards). |
| native_currency | string | Native 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. |
| supports | string[] | Set of primitives currently routable on this chain. Use this to gate which UI surfaces show the chain. |
| pay_in_currencies | string[] | 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_currencies | string[] | Stablecoins available for pay-out on this chain (withdraw). Can differ from pay_in_currencies. Outbound liquidity is provisioned per direction. |
| explorer_url | string | Block explorer base URL for tx links. |
| rpc_hint | string? | Public RPC suggestion for clients that need to broadcast directly. Optional; Flo never relies on this. |
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.
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: created → settled or created → cancelled.
mint.*,queued,created,settled,cancelledredeem.*,queued,created,settled,cancelledorder.*,armed,triggered,cancelled. Trigger lifecycle for stop-loss, take-profit, and OCO. Settlement after a fired trigger emits aredeem.settledevent under the sameorder_id.supply.*,created,settled,cancelled,nav_updatedwithdraw.*,created,settled,cancelledborrow.*,created,settled,cancelled,maintenance_breach,liquidation_imminent,liquidatedrepay.*,created,settled,cancelledbridge.*,queued,committed,delivered,requires_manual_execution,lane_cursedposition.*,nav_updatedfee.*,accrued,settled,refundedtokens.deployed, fires the first time a Flo token contract lands on Arbitrum.
#Delivery headers
| Field | Type | Description |
|---|---|---|
| X-Flo-Signature | string | HMAC-SHA256 hex digest of timestamp + "." + raw_body, signed with your endpoint secret. |
| X-Flo-Signature-Timestamp | integer | Unix 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-Id | string | wh_.... Persist it in your handler to deduplicate at-least-once deliveries. |
| X-Flo-Event | string | Event type, e.g. mint.settled. Mirrors body.type. |
| X-Flo-Webhook-Id | string | Subscription 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"])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.
| Field | Type | Description |
|---|---|---|
| Attempt 1 | t + 0s | Immediate, counts as the first delivery. |
| Attempt 2 | t + 30s | |
| Attempt 3 | t + 5m | |
| Attempt 4 | t + 30m | |
| Attempt 5 | t + 2h | |
| Attempt 6 | t + 6h | |
| Attempt 7 | t + 12h | |
| Attempt 8 | t + 24h | |
| Attempt 9 | t + 48h | |
| Attempt 10 | t + 72h | Final. 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
abandoneddelivery 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:
- 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. - 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.
- 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.
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"
}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
| Field | Type | Description |
|---|---|---|
| kindrequired | "kyc" | "kyb" | Individual (kyc) or business (kyb). Drives reporting + dashboard filtering. |
| providerrequired | enum | "custom" | One of jumio, onfido, persona, sumsub, middesk, alloy, veriff, or custom. Unrecognized values must use custom + a provider name in provider_name. |
| vendor_referencerequired | string | Stable ID returned by your vendor (Jumio txn ID, Onfido check ID, Middesk business ID). Used for audit back-trace. |
| attested_atrequired | ISO-8601 | When the vendor signed off. Used to compute staleness. |
| expires_at | ISO-8601 | Optional. Flo fires customer.attestation_expiring 30 days out and customer.attestation_expired on the day. |
| signalsrequired | string[] | Any of document_verified, liveness_verified, sanctions_clear, pep_check_clear, accreditation_verified, ubo_verified, source_of_funds_verified, addresses_verified. Query-filterable. |
| attestation_hashrequired | hex (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_payload | base64 ciphertext | Optional. Present only when pii: "encrypted". Encrypted at-rest with the key referenced by kms_key_id. |
| kms_key_id | string | Optional 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).
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
| Field | Type | Description |
|---|---|---|
| date | ISO date | UTC calendar day the report covers. |
| status | "matched" | "break" | "pending" | matched = every line matched; break = one or more diffs > $100; pending = job still running. |
| summary.breaks | integer | Count of lines whose diff exceeded the $100 threshold. |
| summary.diff_usd | decimal | Aggregate absolute-value USD difference across all lines. Should round to 0 on a clean day. |
| lines[].token | string | Ticker. |
| lines[].chain | integer (EIP-155 chain ID) | Which chain this line is for. A ticker with distribution across 3 chains produces 3 lines. |
| lines[].onchain_supply | decimal | Sum of ERC-20 balances across all holder wallets at 00:00 UTC. |
| lines[].broker_position | decimal | Broker-of-record share count for the Flo Capital SPC vault at 00:00 UTC (T+0 cash-basis). |
| lines[].diff | decimal | onchain_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_url | string | PDF attestation for that line from our independent third-party attestor. |
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.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.
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.
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.
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.
| Field | Type | Description |
|---|---|---|
| type | URI | Canonical URL for the error code. Dereference at docs.flo.finance/errors/<code>. |
| title | string | Short human-readable summary. |
| status | integer | HTTP status (echoed for offline log analysis). |
| detail | string | Longer human-readable explanation with concrete values where applicable. |
| instance | path | The request path that triggered it. |
| request_id | string | req_…. Include this in support tickets, it indexes every log line end-to-end. |
| code | string | Machine-readable code, switch on this, not on the HTTP status. |
| retryable | boolean | Whether 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
| Field | Type | Description |
|---|---|---|
| unauthorized | 401 · permanent | Missing or malformed Bearer token. |
| forbidden | 403 · permanent | Key is valid but lacks scope for this resource. |
| key_lacks_live_scope | 403 · permanent | Using a sk_test_ key against api.flo.finance or vice-versa. Switch env. |
| ip_not_allowed | 403 · permanent | Request origin outside the IP allowlist attached to this key. |
| kyc_required | 403 · permanent | Live 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_restricted | 403 · permanent | The 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_found | 404 · permanent | Ticker not in the universe or delisted mid-session. |
| position_not_found | 404 · permanent | No 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_mismatch | 409 · permanent | Same Idempotency-Key with different body. Use a new key. |
| broker_leg_in_flight | 409 · partial | Cancel 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_unreachable | 410 · permanent | A limit order's good_till expired without the mark crossing limit_price. Order moved to cancelled; locked notional / tokens refunded. |
| trigger_price_invalid | 422 · permanent | Arming a trigger with an invalid threshold (stop_loss above mark, or take_profit below mark). Body includes mark and trigger_price. |
| slippage_too_tight | 422 · permanent | The fee-and-slippage math would set min_quantity below the 10⁻⁶ dust floor. Loosen slippage_bps or raise notional_usd. |
| gas_budget_insufficient | 402 · permanent | gas.sponsor is developer and the attached gas budget can't cover the on-chain step. Top up and retry. |
| gas_exceeds_max | 422 · permanent | gas.sponsor is user_notional and the live gas estimate would exceed gas.max_gas_usd. Raise the cap or switch to developer. |
| mint_paused_reconciliation | 409 · transient | Reconciliation break; retry after the break is resolved (status page updated). |
| rate_limited | 429 · retryable | Over your account RPS cap. retry_after_seconds in body, Retry-After header mirrors it. |
| ltv_exceeded | 422 · permanent | Requested 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_funds | 422 · permanent | Source 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_triggered | 422 · retry-after-window | Per-block circuit breaker. Body: limit, limit_value, current_value, retry_after_seconds. |
| bridge_route_unsupported | 422 · permanent | Flo doesn't support this (from_chain, to_chain) pair yet. |
| currency_not_supported_on_chain | 422 · permanent | The 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_error | 500 · retryable | Flo-side. Retry with the same idempotency key. |
| service_unavailable | 503 · retryable | Scheduled 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"
}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.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.
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.
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 returnqueue_fullfor 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.
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.
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.
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.
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
- 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.
- 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.
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.jsonThe 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/..."
}
]
}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.
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.
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.
- Minimum trade size: $1 notional per mint or redeem.
- API rate limits: see Conventions · Rate limits.
- Per-block guardrails: see Security · Per-block guardrails.
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).
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.
Support & contact
- Developer support: developers@flo.finance
- Enterprise / SLA: enterprise@flo.finance
- Security disclosures: security@flo.finance · Flo-operated bug bounty, up to $250K for critical findings.
- Compliance / AML / data processing: compliance@flo.finance
Missing something? Email developers@flo.finance.