# Lotus documentation (full) > Lotus is directional liquidity and yield-bearing orders for Stellar: a Soroban range pool (lotus_pool) where a position only sells its inventory and never trades back, and a yield-bearing receipt (lUSDCb, minted 1:1 by lotus_vault against USDC lent on Blend through DeFindex) that can be quoted on the Stellar DEX. One order, four places to wait. Stellar testnet only, not audited. Generated from the same Markdown as https://lotus-stellar.vercel.app/docs. Index: https://lotus-stellar.vercel.app/llms.txt. Deployment check: https://lotus-stellar.vercel.app/api/health. --- # Overview > Lotus is directional liquidity and yield-bearing orders for Stellar. One order, chosen once, waits in one of four places. Everything runs on Stellar **testnet**; nothing is on mainnet and nothing is audited. ## What Lotus is Lotus is two products that work together: 1. **The Lotus pool** (`lotus_pool`, a Soroban contract): a range pool on a grid of price bins where the liquidity provider chooses a direction. A directional position only sells its inventory and only receives proceeds; nothing trades back. The USDC leg can optionally sit in a DeFindex vault that lends on Blend, so resting liquidity earns while it waits and is recalled just in time by the taker's own swap. 2. **The receipt tokens** (`lotus_vault`, a Soroban contract, plus the classic assets `lUSDCb` and `lUSDC`): USDC deposited into a DeFindex vault comes back as a classic Stellar asset at 1:1. Classic assets can be quoted on the Stellar DEX (SDEX), and an SDEX offer never moves funds out of the account, so a limit order quoted in `lUSDCb` keeps earning Blend interest while it waits in the book. `lUSDC` is the unlent version: an instant buffer for orders close to the price and for working capital. The market today is `LBOND / USDC`. `LBOND` is a **simulated** tokenized bond issued by the Lotus team with `AUTH_REQUIRED`, `AUTH_REVOCABLE` and clawback, to model a regulated real-world asset. ## The problem Depth is idle capital. A resting order earns nothing, and an AMM position forces the holder into round trips they do not want: buy low, get sold back the asset higher, hold whatever the market leaves. The holders who own the inventory and the cash, issuers and treasuries, are the ones who could provide depth and have the least reason to. On Stellar, yield-bearing RWAs already exist (Ondo USDY, Etherfuse Stablebonds). The asset side of a bond pair earns while it sits; the quote side, USDC, does not. Lotus makes the USDC side earn too. That gives large providers a reason to bring depth to Stellar: the same capital keeps its yield while it quotes. And the yield is better risk-adjusted than a generic lending vault's, because the Lotus curator knows the order behind every dollar: near-price capital stays unlent (`lUSDC`), capital that can wait is lent on Blend (`lUSDCb`). See [Concepts](/docs/concepts.md#the-buffer-lusdc). ## One order, four places to wait A liquidity provider creates one **range limit order** (sell LBOND across a price range, or USDC waiting to buy it) and chooses, at creation, where it waits: | | Yield off | Earn yield (USDC leg in DeFindex on Blend) | |---|---|---| | **Lotus pool** | directional position on the idle pool, 1 signature | directional position on the vault-backed pool, 1 signature | | **Stellar DEX** | ladder of offers quoted in USDC, 1 signature | ladder quoted in the receipt `lUSDCb`, 1 signature (sell) or 2 (buy: deposit, then offers) | Each bin of the range becomes one SDEX offer at the same price and size, so the order is the same wherever it waits. The choice is made once, at creation: a live position is never migrated between venues. Details in [Concepts](/docs/concepts.md). ## How the pieces connect ``` Stellar classic layer Soroban layer +---------------------------------------------+ +-------------------------------------+ | SDEX order books | | lotus_pool directional bins | | LBOND / lUSDCb, USDC / lUSDCb, LBOND/USDC | | (optional two-way) | | Classic assets: LBOND, USDC, lUSDCb | | lotus_vault deposit / redeem / | +----------------------^----------------------+ | distribute, admin of | | mints / burns lUSDCb | the lUSDCb SAC | +--------------------------+ DeFindex vault --> Blend lending | +-------------------------------------+ Bot (team accounts): market makers + arbitrageur, quotes and arbitrage across both venues ``` The pool and the SDEX are two venues for the same pair. They are linked on-chain only through shared assets and the DeFindex vault; prices are free to differ and an off-chain arbitrageur closes the gap. ## Status and honest limitations - **Testnet only.** Network passphrase `Test SDF Network ; September 2015`. No mainnet deployment, no audit, no pause, no upgrade path. - **Volume is our own.** Trades on the Lotus markets come from accounts run by the Lotus team (two market makers and an arbitrageur). It is market-maker volume, not user volume. See [Bots and volume](/docs/bots-and-volume.md). - **LBOND is simulated.** Its price follows a simulated US 10-year Treasury model; the only real input is the starting yield. - **Yield is observed, not promised.** Blend testnet supply interest is real, and its rate follows Blend's testnet utilization. Any APR the app shows is labelled "observed on testnet, not a forecast". - **Known weaknesses** of the receipt: yield is distributed by a keeper that mints to current holders, so a just-in-time depositor can capture a share it did not earn. The vault-backed pool does not have this problem (it accounts yield in vault shares). - **Issuer trust.** The LBOND issuer can revoke a trustline, including the pool's; a Bid LP's USDC could then be stuck until re-authorization. - **Footprint race** on the vault-backed pool: a transaction can fail with `ExceededLimit` when the shared vault changes by a stroop between simulation and inclusion; the app re-simulates and resends. The full list is on the [FAQ](/docs/faq.md) and in the repository README. ## Where to go next - [Concepts](/docs/concepts.md): bins, the four strategies, the receipt, the curator rule, locked orders. - [Using the app](/docs/using-the-app.md): `/swap`, `/pools`, `/trade`, `/portfolio`, `/faucet`. - [Contracts](/docs/contracts.md): ABI, errors, events and live testnet addresses. - [Integrate](/docs/integrate.md): SDK, MCP server for AI agents, HTTP endpoints. - For AI agents: [`/llms.txt`](/llms.txt), [`/llms-full.txt`](/llms-full.txt), and [`/api/health`](/api/health) to check that the deployment works. --- # Concepts > The directional range pool and its bins, the four places an order can wait, the receipt token and its redemption at par, the curator rule, and why orders are locked until filled. ## Units All on-chain amounts are `i128` stroops (7 decimals: `1 USDC = 10_000_000`). Pool prices are QUOTE per BASE in fixed point `1e7` (`1.0000000 = 10_000_000`). BASE is `LBOND`, QUOTE is `USDC` (Blend testnet USDC). ## The directional range pool `lotus_pool` keeps liquidity on a grid of **bins**. The price of bin `n` is ``` price_at(n) = round_half_up(1e7 * (1 + bin_step_bps / 10_000) ^ n) ``` with bins from `-200` to `200`. The live instances use `bin_step_bps = 10` (0.1 percent per bin), so bins `-20..20` cover about 0.98 to 1.02. A position covers at most 64 bins, each non-empty bin share must be at least `10_000` stroops, and a swap walks at most `max_bins_per_swap` (100) bins. A position has a **side**: - `Ask`: deposits BASE (LBOND) and sells it as the price rises through its bins; it receives USDC. - `Bid`: deposits QUOTE (USDC) and buys LBOND as the price falls; it receives LBOND. and a **mode**: - `Directional` (the default and the product): inventory only goes down, proceeds only come in, and proceeds are never re-offered. The position never trades back. - `TwoWay` (opt-in, only on pools without a vault): DLMM-style bins that hold both assets, so liquidity crosses back when the price returns, like a classic AMM LP. Directional and two-way liquidity share the same book; at each price, the swap takes directional liquidity first. **Fees**: 10 bps (`fee_bps = 10`), charged on top of the price leg and credited to the bin's LPs. The LP always receives its exact limit price. A buyer pays `ceil(q) + ceil(q * fee_bps / 10000)`, a seller receives `floor(q) - ceil(q * fee_bps / 10000)`, where `q = quantity * bin_price`. **Yield vault (optional, per pool instance)**: a pool instance can hold its whole USDC leg (bid inventory, ask proceeds, fees) as DeFindex vault shares, so it earns Blend interest without a keeper. A buy swap deposits the taker's USDC into the vault; a sell swap withdraws exactly what the taker is owed, just in time. The yield strategy is a property of the instance, so there is one instance per strategy (idle, vault-backed, two-way). ## The four strategies One order, chosen once: | | Yield off | Earn yield | |---|---|---| | **Lotus pool** | `add_position(Directional)` on the idle pool instance. 1 signature. | `add_position(Directional)` on the vault-backed instance (USDC leg in DeFindex on Blend). 1 signature. | | **Stellar DEX** | One classic transaction, one `manageSellOffer` per bin, quoted in USDC. 1 signature. | Offers quoted in the receipt `lUSDCb`. Sell: 1 signature. Buy: `lotus_vault.deposit(USDC)` then the offers, 2 signatures. | - **Ladder mapping.** Each bin becomes one SDEX offer at `price_at(bin)` with the bin's own amount. The SDEX ladder is capped at 50 levels (the pool allows 64), so the order plus up to two trustlines fit in one transaction. - **Why two transactions for a receipt buy.** A Soroban call must be the only operation of its transaction, so the deposit and the offers cannot be atomic. The app has both signed up front and only submits the offers once the deposit has succeeded. If the deposit fails nothing moves; if the offers fail, the user simply holds the receipt, redeemable at par, and can post the offers again. - **Where yield comes from.** Blend testnet supply interest, through a DeFindex vault the Lotus team created and seeded into the Blend strategy. Accrual is real; the rate follows Blend's testnet utilization, so it is an observation, not a forecast. BLND emissions are not counted. ## The receipt: lUSDCb `lUSDCb` is a **classic Stellar asset**. The admin of its Stellar Asset Contract (SAC) is the `lotus_vault` Soroban contract, which mints on deposit and distribution, burns on redeem, and tracks supply. - `deposit(from, amount)`: USDC goes into the DeFindex vault (which lends on Blend); `amount` of the receipt is minted 1:1. - `redeem(from, amount)`: the receipt is burned, the vault is withdrawn from, USDC is returned. **1:1 while fully backed**; pro rata (`amount * vault_value / supply`) if the vault position ever lost value, so the last redeemer is never left holding an unbacked receipt. - `distribute(recipients, amounts)` (keeper only): the keeper submits holders and amounts; each line is capped on-chain at `surplus * balance / supply` and the total cannot exceed the surplus. The keeper can omit a holder or be late; it cannot over-mint or redirect yield. Balances locked in open SDEX offers count, because an offer is a selling liability, not a transfer. **Par redemption keeps the peg.** `deposit` and `redeem` are permissionless and 1:1. If the receipt trades under 1 USDC anyone can buy it and redeem; above 1, anyone can deposit and sell. The `USDC / lUSDCb` book is that peg book. Redemption depends on Blend having liquidity: if Blend is fully utilized, redemption waits and the receipt can trade at a discount. **Why a receipt and a pool.** A Blend position or a DeFindex share is a Soroban token: it cannot sit in an SDEX offer or be a path-payment hop. `lUSDCb` can be the quote of a ladder and the middle leg of a path payment (`USDC -> lUSDCb -> LBOND` in one transaction), so a taker holding only USDC never has to know the receipt exists. Known limitation: **just-in-time yield capture**. Distribution mints to current balances, so someone who deposits right before a distribution and redeems right after captures a share they did not earn. The planned fix is an exchange-rate (share-price) token. ## The buffer: lUSDC `lUSDC` is the same receipt design on a second `lotus_vault` whose DeFindex vault is **never lent**: the USDC stays available, so it comes out instantly and redemptions never wait on Blend. It is made for orders close to the price, about to fill, and for the working capital a market maker keeps at hand. It earns nothing; `lUSDCb` is for orders that can wait. **Why two receipts.** A generic lending vault keeps blind buffers because it never knows when a depositor will withdraw. The Lotus curator does: every dollar sits behind a limit order with a known price, range and distance to the market. Capital behind orders about to fill stays unlent (`lUSDC`); capital behind orders that can wait is lent on Blend (`lUSDCb`). That is more yield for the same liquidity risk, and it lets large providers keep their yield while they post depth on Stellar. Today the split is enforced by the curator rule below; routing every order automatically by its distance to the price is the next step. ## The curator rule Yield is not offered for a **pool Bid** whose range starts within 10 bins (about 1 percent) of the reference price, because every trade would recall that USDC from the vault: little yield, more failure risk. In the app, the "Lotus pool + Blend yield" card is then disabled with "Too close to the price" and a **Change range** button. - Sell orders are never blocked (their yield is on proceeds that wait). - Receipt orders on the SDEX are never blocked (a receipt in an open offer keeps earning). - The rule lives in the app (`PROXIMITY_MIN_BINS = 10`); the contracts do not enforce it. ## Direction rule A sell range sits at or above the reference price of the venue it rests on; a buy range sits at or below it. The app clamps handles and presets and marks a strategy unavailable when its own reference price breaks the rule. The contract does not prevent crossed books (an Ask below a Bid). ## Locked orders In the app, a **directional order is locked until it is fully filled**: you can **Claim** proceeds and fees at any time, but Remove is replaced by "Early redemption, soon". The same holds for SDEX ranges quoted in a Lotus receipt. Two-way LP positions keep Remove. This is a product rule enforced by the UI only. On-chain, the pool contract's `remove_position` works at any time for the owner, and SDEX offers belong to the user's account and can be cancelled by any Stellar client. ## Compliance from the asset `LBOND` is `AUTH_REQUIRED`, `AUTH_REVOCABLE` and clawback-enabled. Makers and takers need a trustline authorized by the issuer, and the pool contract itself is authorized through the LBOND SAC. An unauthorized taker's trade fails at the protocol level with `op_not_authorized`. Compliance comes from the asset, not from pool code; the faucet authorizes testnet trustlines for you. --- # Using the app > A walk through every screen: get test tokens on `/faucet`, swap or place one-price and range orders on `/swap`, build a position across the four strategies on `/pools`, trade the SDEX books on `/trade`, and manage everything on `/portfolio`. Every action is signed in your own wallet. Any wallet supported by Stellar Wallets Kit works (Freighter, xBull, Albedo, Lobstr, Hana, ...). Set the wallet to **testnet**. There are no session keys and no auto-signing. ## /faucet: get started in about a minute 1. **Fund**: if the account does not exist yet, the Friendbot button funds it with test XLM. 2. **Add trustlines**: one classic transaction adds every missing `changeTrust` (LBOND, USDC, the active receipt). Each row shows the code, issuer, `CODE:ISSUER` with copy buttons and an explorer link. 3. **Claim test tokens**: the server authorizes your LBOND trustline (LBOND is `AUTH_REQUIRED`) and sends LBOND, Blend testnet USDC and the active receipt, one transaction per asset, with the hash of each. The amounts are set by the server; `GET /api/faucet` returns them. One claim per address and asset, and a light rate limit per network. A thin "Get test tokens" line appears under the nav on every page while your wallet is not ready. ## /swap: Lite and Pro One page with a **Lite / Pro** switch (`/swap`, `/swap?mode=pro`). - **Swap** (Lite and Pro): against the directional Lotus pool only. Every live directional instance is quoted (`quote_swap`) and the best one is kept. The details box shows the route, pool price, price impact, minimum received (with your slippage setting), pool fee and bins crossed. If the size is larger than the pool depth, it tells you how much would really fill. Execution re-quotes, then calls `swap(min_out)`. - **Limit** (Lite): Sell or Buy, "When 1 LBOND is worth" with market and percentage presets. One price is one bin. Orders have no expiry ("Until filled"). Then choose a strategy and confirm. - **Range** (Pro): the liquidity chart with draggable range handles next to Selling / Buying, start price and range end, presets, the strategy picker, then **Get started**. - DCA is visible but disabled ("Coming soon"). Limit and Range are the same order as `/pools` New position: same data, same strategy picker, same review dialog. ## /pools: New position Three steps, then a review dialog: 1. **Pair and side**: LBOND / USDC, **Sell LBOND** (Ask) or **Buy LBOND with USDC** (Bid). 2. **Price range and deposit**: draggable Start / End handles on a chart of SDEX trades (1H to 1Y) next to a depth histogram of every live pool instance's bins plus the SDEX books, all on the same grid. Presets, min / max fields with one-bin steppers, weight shape (even, front, back) and the deposit. 3. **Choose your strategy**: four cards (Lotus pool, Lotus pool + Blend yield, Stellar DEX, Stellar DEX + Blend yield), each with the expected yield, the number of signatures and, when unavailable, why (direction rule, curator rule, no instance, more than 50 SDEX levels). 4. **Review**: deposit, range, average price, fee or XLM reserve, trustlines added, crossing warning, then **Sign**. The two-transaction case (receipt buy) is handled here, including "Post the offers again" if the second step fails. The expected yield is the Blend USDC supply APR read live from the Blend pool (annualized growth of `b_rate` between two reads); until two reads exist the card shows the last figure measured by the team. Always labelled "observed on testnet, not a forecast". **Your positions** lists pool positions of every instance and your SDEX ladders, grouped per pair and side: status, fill bar, remaining, claimable proceeds and fees, **Claim**, and **View position** (the creating transaction on stellar.expert). Directional orders are locked until filled (see [Concepts](/docs/concepts.md#locked-orders)). Header actions: **Swap on the pool** and **Advanced: two-way position** (`?pool=lbond-usdc-twoway`), the builder for `Mode::TwoWay` LP positions. ## /trade: the SDEX terminal A classic exchange layout for the Stellar DEX books: - **Market bar**: `?market=LBOND-lUSDCb`, `USDC-lUSDCb` (the peg book), `LBOND-USDC`; last price, 24h change, high, low, volume, mid, spread. - **Chart**: candles from Horizon trade aggregations (1m to 1W), or cumulative depth. Empty buckets are drawn as flat grey candles at the previous close with zero volume: no trade is invented. - **Order book and trades**. Clicking a level fills the Limit price. - **Swap**: one `pathPaymentStrictSend` quoted from Horizon (`USDC -> lUSDCb -> LBOND` is one transaction). For LBOND/USDC, a venue comparison shows the Lotus pool quote next to the SDEX path. - **Limit**: one `manageSellOffer`, with a warning when the price crosses the book. - **Bottom panel**: open orders with Cancel, trade history (one row per operation, routed hops merged), balances. Range orders are placed from `/pools`; the order panel links there. ## /portfolio In order: a USDC-equivalent summary; **pool positions** (only instances where you hold one: pair, side, range, fill bar, amount to claim, **Claim** when something is claimable, otherwise "Locked until filled"); **Lotus receipts** per vault (`/portfolio#receipts`) with **Yield earned** (paid by the keeper every ~15 minutes, plus what is accrued) and **Redeem** to USDC; **open orders** on the SDEX; then **balances** (available versus committed to offers). Every address, asset, offer and transaction links to stellar.expert (testnet). ## Common errors | message | meaning | |---|---| | Not authorized by the issuer | your LBOND trustline is not authorized yet: claim from `/faucet` | | No liquidity in the Lotus pool at this size | the pool has no bins on that side; try a smaller size or the SDEX | | The vault state changed while signing, retry | footprint race on the shared DeFindex vault; the app already retried | | Too close to the price | curator rule: move the Bid range at least 10 bins below the price, or pick another strategy | | `op_not_authorized` | protocol-level rejection of an unauthorized LBOND trustline | --- # Contracts > The `lotus_pool` ABI (v1), the `lotus_vault` interface, their errors and events, and the live testnet addresses read from the deployment config at request time. Both contracts are Soroban (Rust, `soroban-sdk`), deployed on Stellar testnet only, not audited. Errors surface as `Error(Contract, #n)`; authorization failures are host `Auth` errors, not contract errors. Amounts are `i128` stroops (7 decimals). ## Live testnet addresses The table below is generated from the deployment files (`deployments/testnet.*.json`) every time this page is served, so it always matches what the app uses. If anything else disagrees with it, these files win. | | value | |---|---| | network | `testnet` | | passphrase | `Test SDF Network ; September 2015` | | Soroban RPC | `https://soroban-testnet.stellar.org` | | Horizon | `https://horizon-testnet.stellar.org` | | active lotus_vault | [`CCCZ6XUDCWTOKZS6AYNECY3QPOFBICGMBBZXM63U4WZQD4EBFYFW2QKG`](https://stellar.expert/explorer/testnet/contract/CCCZ6XUDCWTOKZS6AYNECY3QPOFBICGMBBZXM63U4WZQD4EBFYFW2QKG) | ### Lotus pool instances (lotus_pool) | id | status | contract | USDC leg | two-way | bin step / fee | |---|---|---|---|---|---| | `lbond-usdc-idle` | live | [`CCE2ISQKVWZJAZL6FRW227WDVWXODWUZ75ZQXGPKJWXERRW64NHS4CA3`](https://stellar.expert/explorer/testnet/contract/CCE2ISQKVWZJAZL6FRW227WDVWXODWUZ75ZQXGPKJWXERRW64NHS4CA3) | idle (held by the pool) | yes | 10 bps / 10 bps | | `lbond-usdc-defindex` | live | [`CDNG7T2KSDQ7ZYXHSDAM63RLLVPCQ5V6T5GRTXOOX47EZEMU636ZFPNG`](https://stellar.expert/explorer/testnet/contract/CDNG7T2KSDQ7ZYXHSDAM63RLLVPCQ5V6T5GRTXOOX47EZEMU636ZFPNG) | DeFindex vault [`CDCIDX5FXOENXW4ODR2D7QMRKL4F2NTZYFVZEFF63GTNRVVDXLCQ7EEC`](https://stellar.expert/explorer/testnet/contract/CDCIDX5FXOENXW4ODR2D7QMRKL4F2NTZYFVZEFF63GTNRVVDXLCQ7EEC) | no | 10 bps / 10 bps | | `lbond-usdc-twoway` | live | [`CC264FOWPI5R5FG4276ZIPGJ5OG5AP3IZ4RZNMC2KQNJDA7LQSFQ3SEI`](https://stellar.expert/explorer/testnet/contract/CC264FOWPI5R5FG4276ZIPGJ5OG5AP3IZ4RZNMC2KQNJDA7LQSFQ3SEI) | idle (held by the pool) | yes | 10 bps / 10 bps | Retired instances (hidden from the app's pool views, still listed on /portfolio so owners can claim or remove): `lbond-usdc-idle-v0` [`CDOXTKGMK7MHEIOVXN5OBETMFUQATRQCB5NHFBFKCVUF5OUHQHVTN6CF`](https://stellar.expert/explorer/testnet/contract/CDOXTKGMK7MHEIOVXN5OBETMFUQATRQCB5NHFBFKCVUF5OUHQHVTN6CF). ### Lotus vaults (lotus_vault) and receipts | strategy | status | lotus_vault | receipt (classic asset) | receipt SAC | DeFindex vault | |---|---|---|---|---|---| | `usdc-defindex-idle` | idle | [`CA5JHSNNGRX3QLLNRLOTDCXYGAYVC3ZRHXX6IWXQAD6YLILZ7TL2AHG6`](https://stellar.expert/explorer/testnet/contract/CA5JHSNNGRX3QLLNRLOTDCXYGAYVC3ZRHXX6IWXQAD6YLILZ7TL2AHG6) | [`lUSDC`](https://stellar.expert/explorer/testnet/asset/lUSDC-GBBBCE5XIN3KPPNETITZF4ZEC6NG4MZZ3I2CYIQGGGY4GN4DJO73GDL6) issuer [`GBBBCE5XIN3KPPNETITZF4ZEC6NG4MZZ3I2CYIQGGGY4GN4DJO73GDL6`](https://stellar.expert/explorer/testnet/account/GBBBCE5XIN3KPPNETITZF4ZEC6NG4MZZ3I2CYIQGGGY4GN4DJO73GDL6) | [`CDQ7HJZGK5RGIHAPWVCE7KVJIDOCQFPJI7CIYLRVNF5RKM62OJXL2HAV`](https://stellar.expert/explorer/testnet/contract/CDQ7HJZGK5RGIHAPWVCE7KVJIDOCQFPJI7CIYLRVNF5RKM62OJXL2HAV) | [`CBMVK2JK6NTOT2O4HNQAIQFJY232BHKGLIMXDVQVHIIZKDACXDFZDWHN`](https://stellar.expert/explorer/testnet/contract/CBMVK2JK6NTOT2O4HNQAIQFJY232BHKGLIMXDVQVHIIZKDACXDFZDWHN) | | `usdc-defindex-blend` (active) | live | [`CCCZ6XUDCWTOKZS6AYNECY3QPOFBICGMBBZXM63U4WZQD4EBFYFW2QKG`](https://stellar.expert/explorer/testnet/contract/CCCZ6XUDCWTOKZS6AYNECY3QPOFBICGMBBZXM63U4WZQD4EBFYFW2QKG) | [`lUSDCb`](https://stellar.expert/explorer/testnet/asset/lUSDCb-GC7QBZD2LMPZZ3BT4ZLKPYUR23GJKT47BLYJKMXUY523M7HMUGT64TVO) issuer [`GC7QBZD2LMPZZ3BT4ZLKPYUR23GJKT47BLYJKMXUY523M7HMUGT64TVO`](https://stellar.expert/explorer/testnet/account/GC7QBZD2LMPZZ3BT4ZLKPYUR23GJKT47BLYJKMXUY523M7HMUGT64TVO) | [`CC6YDSEJRF7QH5JL7MVEYEIU3CTMNEHS4XIK5R36DYA5EVUYIP4PAHPG`](https://stellar.expert/explorer/testnet/contract/CC6YDSEJRF7QH5JL7MVEYEIU3CTMNEHS4XIK5R36DYA5EVUYIP4PAHPG) | [`CDCIDX5FXOENXW4ODR2D7QMRKL4F2NTZYFVZEFF63GTNRVVDXLCQ7EEC`](https://stellar.expert/explorer/testnet/contract/CDCIDX5FXOENXW4ODR2D7QMRKL4F2NTZYFVZEFF63GTNRVVDXLCQ7EEC) | Announced, not deployed: `xlm-defindex-blend`. ### Assets and external contracts | | asset / contract | issuer | SAC | |---|---|---|---| | LBOND (simulated bond, AUTH_REQUIRED, clawback) | [`LBOND`](https://stellar.expert/explorer/testnet/asset/LBOND-GCMNI332OEZL2AVBKJVDZ2IPREWMEYRENGFFVEGXS4JVXUWJEIAT5EQF) | [`GCMNI332OEZL2AVBKJVDZ2IPREWMEYRENGFFVEGXS4JVXUWJEIAT5EQF`](https://stellar.expert/explorer/testnet/account/GCMNI332OEZL2AVBKJVDZ2IPREWMEYRENGFFVEGXS4JVXUWJEIAT5EQF) | [`CAVDLKQ2T4XBIQNY2WBUZKWQM6YDQ6NVX2N3HQ3P5Z2SBQ3VNZCBVBKE`](https://stellar.expert/explorer/testnet/contract/CAVDLKQ2T4XBIQNY2WBUZKWQM6YDQ6NVX2N3HQ3P5Z2SBQ3VNZCBVBKE) | | USDC (Blend testnet) | [`USDC`](https://stellar.expert/explorer/testnet/asset/USDC-GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56) | [`GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56`](https://stellar.expert/explorer/testnet/account/GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56) | [`CAQCFVLOBK5GIULPNZRGATJJMIZL5BSP7X5YJVMGCPTUEPFM4AVSRCJU`](https://stellar.expert/explorer/testnet/contract/CAQCFVLOBK5GIULPNZRGATJJMIZL5BSP7X5YJVMGCPTUEPFM4AVSRCJU) | | lUSDCb (active receipt) | [`lUSDCb`](https://stellar.expert/explorer/testnet/asset/lUSDCb-GC7QBZD2LMPZZ3BT4ZLKPYUR23GJKT47BLYJKMXUY523M7HMUGT64TVO) | [`GC7QBZD2LMPZZ3BT4ZLKPYUR23GJKT47BLYJKMXUY523M7HMUGT64TVO`](https://stellar.expert/explorer/testnet/account/GC7QBZD2LMPZZ3BT4ZLKPYUR23GJKT47BLYJKMXUY523M7HMUGT64TVO) | [`CC6YDSEJRF7QH5JL7MVEYEIU3CTMNEHS4XIK5R36DYA5EVUYIP4PAHPG`](https://stellar.expert/explorer/testnet/contract/CC6YDSEJRF7QH5JL7MVEYEIU3CTMNEHS4XIK5R36DYA5EVUYIP4PAHPG) | | DeFindex vault (active) | [`CDCIDX5FXOENXW4ODR2D7QMRKL4F2NTZYFVZEFF63GTNRVVDXLCQ7EEC`](https://stellar.expert/explorer/testnet/contract/CDCIDX5FXOENXW4ODR2D7QMRKL4F2NTZYFVZEFF63GTNRVVDXLCQ7EEC) | | | | Blend pool | [`CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF`](https://stellar.expert/explorer/testnet/contract/CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF) | | | Machine-readable: `GET /api/config` returns the same values as JSON. ## lotus_pool (ABI v1) ### Types ```rust enum Side { Ask, Bid } // Ask: sells BASE as price rises. Bid: buys BASE as price falls. enum Mode { Directional, TwoWay } // TwoWay: pools without a vault only struct Config { admin, base, quote, vault: Option
, bin_step_bps: u32, fee_bps: u32, max_bins_per_swap: u32, supports_two_way: bool } struct BinView { side: Side, bin: i32, price: i128, inventory: i128, total_shares: i128, filled: i128 } struct Position { id: u64, owner, side, mode, lo_bin: i32, hi_bin: i32, deposited: i128, inventory: i128, filled: i128, pending_proceeds: i128, pending_fees: i128, vaulted: bool } struct PositionPayout { base: i128, quote: i128 } struct SwapQuote { amount_out: i128, fee: i128, avg_price: i128, bins_crossed: u32, partial: bool } struct Tvl { base: i128, quote_value: i128, vault_shares: i128 } ``` Encoding: `Side` and `Mode` are unit-variant enums, on the wire `ScVal::Vec([Symbol("Ask")])` (stellar CLI: `--side Ask`; spec-based stellar-sdk clients: `{ tag: "Ask", values: undefined }`). Structs are `ScVal::Map` keyed by field name. Field notes: for Ask, `inventory` / `deposited` / `filled` are in BASE, `pending_proceeds` and `pending_fees` in QUOTE. For Bid, `inventory` / `deposited` / `filled` are in QUOTE, `pending_proceeds` is BASE bought, `pending_fees` QUOTE. `SwapQuote.avg_price` includes the fee. A removed position disappears: `get_position(id)` then fails with `NoSuchPosition`. ### Functions ```rust __constructor(admin, base, quote, vault: Option
, bin_step_bps: u32, fee_bps: u32) // LP add_position(owner, side: Side, mode: Mode, lo_bin: i32, hi_bin: i32, amount: i128, weights: Vec) -> u64 // Ask deposits BASE, Bid deposits QUOTE, spread over lo..=hi by weights (empty = uniform). // owner.require_auth(). At most 64 bins; each non-zero bin share >= 10_000 stroops. claim(owner, id: u64) -> PositionPayout // pay pending proceeds and fees, keep the position open remove_position(owner, id: u64) -> PositionPayout // unfilled inventory + pending proceeds + fees, close it // Taker quote_swap(buy_base: bool, amount_in: i128) -> SwapQuote // read only, never reverts for lack of liquidity swap(taker, buy_base: bool, amount_in: i128, min_out: i128) -> i128 // amount_in is a maximum, fee included; partial fills allowed; unspent input is never pulled. // Admin (the only admin power) set_max_bins_per_swap(max_bins: u32) // 1..=100 // Views get_config() -> Config get_position(id: u64) -> Position positions_of(owner) -> Vec // open positions, at most 100 per owner get_bin(side: Side, bin: i32) -> BinView depth(side: Side, from_bin: i32, to_bin: i32) -> Vec // non-empty bins, at most 100 tvl() -> Tvl price_at(bin: i32) -> i128 abi_version() -> u32 // 1 ``` Client notes: - On the **vault-backed pool**, `quote_swap` (sell side), `get_position`, `get_bin(Bid)`, `depth(Bid)` and `tvl` call a DeFindex view that writes a fee report. Run them with `simulateTransaction` only and never submit them (stellar CLI: `--send=no`). - Re-run `quote_swap` right before `swap` and pass `min_out`. A buy needs the taker's QUOTE `transfer` authorized under the `swap` call; a sell signs the BASE `transfer`. - Calls that withdraw from the shared DeFindex vault can hit a footprint race (`Error(Storage, ExceededLimit)`): re-simulate, re-sign and resend. - The exact price grid for every bin and step is in `contracts/lotus_pool/test-vectors.json`. ### Errors | # | name | when | |---|---|---| | 1 | NotInitialized | call before the constructor | | 2 | InvalidAmount | amount <= 0 or > 1e18 | | 3 | InvalidRange | lo > hi, outside [-200, 200], more than 64 bins | | 4 | InvalidWeights | weights length != bins, or all zero | | 5 | InvalidConfig | constructor or admin parameter out of range | | 6 | NoSuchPosition | unknown or already removed id | | 7 | NotOwner | claim or remove by an address that is not the owner | | 8 | NoLiquidity | the swap would move nothing | | 9 | SlippageExceeded | amount_out < min_out | | 10 | NotImplemented | `Mode::TwoWay` on a pool whose `supports_two_way` is false | | 11 | Overflow | arithmetic overflow | | 12 | AmountTooSmall | a bin would get too little to mint a share | | 13 | BinDust | the bin is almost fully filled: use another bin | | 14 | VaultShortfall | the vault paid less than needed, minted 0 shares, or has a zero share price | | 15 | TooManyPositions | the owner already has 100 open positions | ### Events | topics | data | |---|---| | `["position_added", owner]` | `id, side, lo_bin, hi_bin, amount` (mode: read `get_position(id).mode`) | | `["position_removed", owner]` | `id, base, quote` | | `["claimed", owner]` | `id, base, quote` (not emitted when nothing is paid) | | `["swap", taker]` | `buy_base, amount_in, amount_out, fee, bins_crossed, last_bin` | The two SACs also emit their token `transfer` events. ### Invariants (unit-tested) 1. Directional: per bin, inventory never increases after a fill and proceeds are never re-offered. 2. Solvency: pool balances (plus vault shares at current value) always cover remaining inventory plus all pending claims; checked after every operation of randomized sequences. 3. Rounding always favors the pool; no payout exceeds what a position is owed. 4. Late depositors into a partly filled bin cannot take earlier proceeds (share-based accounting per bin). ## lotus_vault The admin of the receipt's Stellar Asset Contract. Routes deposits into a single-asset DeFindex vault and distributes yield. ```rust __constructor(admin, keeper, underlying: Address, receipt: Address, vault: Address) deposit(from, amount: i128) -> i128 // from.require_auth(); mints `amount` receipt 1:1; returns DeFindex shares received redeem(from, amount: i128) -> i128 // from.require_auth(); burns the receipt; returns underlying sent distribute(recipients: Vec
, amounts: Vec) -> i128 // keeper only; returns total minted surplus() -> i128 // max(0, vault_value() - supply()) vault_value() -> i128 // value of the DeFindex shares + idle underlying held by the contract supply() -> i128 // receipts minted by this contract and still outstanding get_config() -> Config // { admin, keeper, underlying, receipt, vault } set_keeper(new_keeper) // admin only ``` `vault_value()` and `surplus()` call DeFindex `get_asset_amounts_per_shares`, which updates a fee report: read them by simulation only. `supply()` and `get_config()` are plain instance reads. ### Errors | # | name | raised by | |---|---|---| | 1 | NotInitialized | any call before the constructor | | 2 | InvalidAmount | `deposit` / `redeem` with amount <= 0, negative amount in `distribute` | | 3 | InsufficientSupply | `redeem` with amount > `supply()` | | 4 | LengthMismatch | `distribute` with different list lengths | | 5 | InsufficientSurplus | `distribute` with sum(amounts) > `surplus()` | | 6 | ExceedsCap | a `distribute` line above `surplus * balance / supply` (duplicates are summed) | | 7 | ZeroSupply | `distribute` when nothing is outstanding | | 8 | Overflow | i128 overflow | | 9 | NoVaultPosition | `redeem` needs DeFindex funds but the contract holds no shares | ### Events | topics | data | |---|---| | `["deposit", from]` | `{ amount, shares }` | | `["redeem", from]` | `{ amount, paid }` | | `["distribute"]` | `{ total, surplus_before, recipients }` | | `["set_keeper"]` | `{ keeper }` | ### Invariants - `deposit` and `redeem` leave `surplus()` unchanged (rounding dust stays in the contract). - While `vault_value() >= supply()` every receipt redeems 1:1; otherwise `redeem` pays `amount * vault_value / supply`. - `distribute` can mint at most `surplus()`, each recipient at most `surplus * balance / supply` measured before the call. Known limits: the testnet receipt issuer accounts are not locked (on mainnet the issuer would be locked after `set_admin`, so the contract is the only minter); `distribute` aborts if any recipient cannot receive (the keeper filters the list); no pause, no upgrade, no fee switch. ## Source Contract sources and tests live in the Lotus repository under `contracts/lotus_pool` and `contracts/lotus_vault`; the full pool ABI is `docs/pool-abi.md`, the accounting notes `contracts/lotus_pool/README.md`. --- # Integrate > Three ways to build on Lotus: the HTTP endpoints of this app, a TypeScript SDK, and an MCP server that lets AI agents (Claude Code, Codex, Cursor, ...) read the markets and build transactions. Testnet only. ## For AI agents: start here 1. Read [`https://lotus-stellar.vercel.app/llms.txt`](https://lotus-stellar.vercel.app/llms.txt) (index) or [`https://lotus-stellar.vercel.app/llms-full.txt`](https://lotus-stellar.vercel.app/llms-full.txt) (every page in one file). Every docs page is also served as raw Markdown at `https://lotus-stellar.vercel.app/docs/.md`. 2. Call `GET https://lotus-stellar.vercel.app/api/health` to check that the deployment works before relying on it. 3. Call `GET https://lotus-stellar.vercel.app/api/config` for every contract id and asset. Never hard-code ids: they change when a contract is redeployed. 4. Use the MCP server (below) to quote and build transactions. Signing stays with the user's wallet or key. ## HTTP endpoints All responses are JSON with `Cache-Control: no-store`. ### GET /api/health Machine-readable deployment check. Reads are cheap: one Soroban RPC `getHealth`, one Horizon root request, one `getLedgerEntries` for every contract instance, and one read-only simulation per Lotus contract (`abi_version()` on each pool, `supply()` on each vault). Results are cached for about 15 seconds. ```jsonc { "status": "ok", // "ok" | "degraded" (a contract check failed) | "down" (RPC or Horizon unreachable) "checkedAt": "2026-10-26T09:00:00.000Z", "app": { "name": "lotus-web", "version": "0.1.0", "commit": "abc1234...", "env": "production" }, "network": { "name": "testnet", "passphrase": "Test SDF Network ; September 2015" }, "rpc": { "url": "https://...", "ok": true, "latencyMs": 120, "latestLedger": 123456, "status": "healthy" }, "horizon": { "url": "https://...", "ok": true, "latencyMs": 90, "latestLedger": 123456 }, "contracts": [ { "kind": "lotus_pool", "id": "lbond-usdc-idle", "contract": "C...", "status": "live", "ok": true, "instance": true, "liveUntilLedger": 999999, "read": { "method": "abi_version", "ok": true, "value": 1 }, "explorer": "https://stellar.expert/..." } ], "summary": { "total": 9, "ok": 9, "failed": [] } } ``` HTTP 200 for `ok` and `degraded`, 503 for `down`. `commit` is `null` when the host does not expose it. ### GET /api/config The merged deployment config, public keys only, re-read from the deployment files at request time. ```jsonc { "network": "testnet", "rpc": "https://...", "horizon": "https://...", "passphrase": "Test SDF Network ; September 2015", "lotusVault": "C...", // active lotus_vault "underlying": { "code": "USDC", "issuer": "G...", "sac": "C..." }, "receipt": { "code": "lUSDCb", "issuer": "G...", "sac": "C..." }, "rwa": { "code": "LBOND", "issuer": "G..." }, "defindexVault": "C...", "blendPool": "C...", "strategies": [ { "id": "usdc-defindex-blend", "label": "...", "status": "live", "lotusVault": "C...", "underlying": {}, "receipt": {}, "defindexVault": "C...", "blendPool": "C...", "observed": {} } ], "pools": [ { "id": "lbond-usdc-idle", "label": "...", "status": "live", "contract": "C...", "base": {}, "quote": {}, "vault": null, "binStepBps": 10, "feeBps": 10, "supportsTwoWay": false, "mock": false } ], "retiredPools": [], "accounts": { "maker": "G...", "...": "G..." }, "sources": {}, "loadedAt": "2026-10-26T09:00:00.000Z" } ``` ### GET /api/faucet Faucet status: `{ ok, testnet, enabled, assets: [{ role, code, amount, configured, source }] }`. ### POST /api/faucet Body `{ "address": "G..." }`. The account must exist (Friendbot) and hold the trustlines. The server authorizes the LBOND trustline and sends each faucet asset in its own transaction. ```json { "ok": true, "address": "G...", "results": [ { "role": "rwa", "code": "LBOND", "amount": "500", "status": "sent", "hash": "...", "message": "trustline authorized by the issuer" }, { "role": "underlying", "code": "USDC", "amount": "200", "status": "skipped", "message": "already claimed by this address" } ], "error": "only when nothing was sent" } ``` Errors: 400 invalid address / account missing / no trustline, 403 not testnet, 409 claim already running, 429 rate limited, 502 Horizon unavailable. Limits: one claim per address and asset, 6 requests per 10 minutes per IP. ## TypeScript SDK The SDK lives in the repository at `sdk/` and is documented in `sdk/README.md`, which is the reference (install, configuration, function list). It wraps the same building blocks the app uses: reading the config, quoting the pool, reading positions and offers, and building unsigned transactions for the pool, the vault and the SDEX. ## MCP server (for AI agents) The MCP server lives at `mcp/` and is documented in `mcp/README.md`, which is the reference for the exact command, environment variables and signing options. It exposes Lotus as tools: | tool | purpose | |---|---| | `lotus_health` | the deployment check (same data as `/api/health`) | | `lotus_config` | contract ids, assets, network (same data as `/api/config`) | | `lotus_markets` | the markets and their state | | `lotus_orderbook` | an SDEX order book | | `lotus_pools` | Lotus pool instances and their depth | | `lotus_quote_swap` | quote a swap on the pool | | `lotus_positions` | an account's pool positions | | `lotus_offers` | an account's SDEX offers | | `lotus_build_swap` | build a swap transaction | | `lotus_build_range_order` | build a range order (pool position or SDEX ladder) | | `lotus_build_limit_order` | build a one-price limit order | | `lotus_build_claim` | build a claim of proceeds and fees | | `lotus_build_remove` | build a position removal | | `lotus_build_deposit` | build a `lotus_vault` deposit (USDC to receipt) | | `lotus_build_redeem` | build a `lotus_vault` redeem (receipt to USDC) | | `lotus_build_trustlines` | build the missing trustlines | | `lotus_faucet_claim` | claim testnet tokens through `/api/faucet` | | `lotus_submit` | submit a signed transaction | The `lotus_build_*` tools produce transactions to be signed by the user; read `mcp/README.md` for how signing is handled before you submit anything. Build it once (`cd mcp && npm install && npm run build`); at the time of writing the server is started with `node mcp/dist/index.js`. If `mcp/README.md` says otherwise, follow it. ### Install in Claude Code ```sh claude mcp add lotus -- node /absolute/path/to/lotus-stellar/mcp/dist/index.js claude mcp list # check that "lotus" is connected ``` ### Install in Codex Add a server entry to `~/.codex/config.toml`: ```toml [mcp_servers.lotus] command = "node" args = ["/absolute/path/to/lotus-stellar/mcp/dist/index.js"] ``` ### Other clients Cursor, ChatGPT and other MCP clients take the same command and arguments in their own MCP settings. Without MCP, an agent can still use the HTTP endpoints above with Horizon and Soroban RPC directly; the [Contracts](/docs/contracts.md) page has the full ABI. ## Rules for integrators - Testnet only: check `network.passphrase` is `Test SDF Network ; September 2015`. - Read contract ids from `/api/config` (or the deployment files), never from copies in docs. - Run vault-backed pool views and `lotus_vault.vault_value()` / `surplus()` by simulation only, never submit them. - Re-quote right before a swap and always pass `min_out`. - A Soroban call must be the only operation of its transaction: trustlines and deposits are separate transactions. --- # Bots and volume > The trades on the Lotus testnet markets come from accounts run by the Lotus team. They are real on-chain transactions, but it is market-maker volume, not user volume. The LBOND price is simulated. ## Where the volume comes from Three accounts operated by the Lotus team keep the testnet markets alive: - `mm_a`, a **market maker**: quotes `LBOND / lUSDCb`, `LBOND / USDC` and the `USDC / lUSDCb` peg book on the SDEX. - `mm_b`, a **flow generator**: trades against those quotes (and sometimes against the team's demo ladders); it is also the account that moves the peg in stress scenarios. - `arb`, an **arbitrageur**: buys the receipt under 1 USDC and redeems it at par, or mints it at par and sells above 1, and closes gaps between the Lotus pool and the SDEX. They trade assets the team issues (`LBOND`) or mints through `lotus_vault` (the receipt, minted 1:1 against Blend testnet USDC from the public faucet). Every trade is a real testnet transaction. **Nobody else uses these markets yet**: it is a hackathon testnet, and the point is to exercise the mechanism under load. Arbitrage profit in the demo scenarios is a transfer between our own accounts, not revenue. The bot accounts, read from `deployments/testnet.bot.json` when this page is served: | account | address | role | |---|---|---| | `mm_a` | [`GATZCJW4Q3O6DGJIZ5PRF3MBXX7BDEK4PLFQXTN6XSFGWTBO6KTPGUGB`](https://stellar.expert/explorer/testnet/account/GATZCJW4Q3O6DGJIZ5PRF3MBXX7BDEK4PLFQXTN6XSFGWTBO6KTPGUGB) | market maker: quotes LBOND/lUSDCb (4 levels each side), LBOND/USDC (3 levels each side) and the USDC/lUSDCb peg book | | `mm_b` | [`GARJTMBHMNJCFQO5XFWGGJJAIV36GT6SQTZHIEZU3QD7IWUV5EJLBZZU`](https://stellar.expert/explorer/testnet/account/GARJTMBHMNJCFQO5XFWGGJJAIV36GT6SQTZHIEZU3QD7IWUV5EJLBZZU) | flow generator / taker; also the actor that moves the peg in the stress scenarios | | `arb` | [`GAQWKGZ3XGFFU3TF5HKBC3JHY4OF4XSLXSPGXMAKC66POM3JC6PQ26XI`](https://stellar.expert/explorer/testnet/account/GAQWKGZ3XGFFU3TF5HKBC3JHY4OF4XSLXSPGXMAKC66POM3JC6PQ26XI) | arbitrageur: buys lUSDCb under 1 USDC and redeems it, or mints it at par and sells above 1 | ## The LBOND price is simulated There is no oracle and no market data. The LBOND fair value follows a simulated US 10-year Treasury: - a yield `y` that follows an Ornstein-Uhlenbeck process around a reference yield, with a daily volatility of about 6.5 bp (a real 10Y moves about 5 to 8 bp a day); - `price = nav - duration * (y - y_ref)` with a duration of 7 years, and a NAV that accrues carry at the simulated yield, plus a tiny micro-noise. The only real input is the starting point: the latest FRED DGS10 print fetched at first start. Every move after that is random. Market makers quote around this simulated fair value. ## The receipt price stays at par The receipt is minted and redeemed 1:1 by `lotus_vault`, so its price on `USDC / lUSDCb` is par plus a few bps of noise, never a drift. The arbitrageur acts when the book moves more than a couple of bps away from par. ## Measured runs (testnet, 25 October 2026) - A depeg of the receipt to 0.985 was restored by the arbitrageur in 10 to 12 seconds, on both vault deployments. - A cross-venue dislocation between the pool and the SDEX was closed in 26 seconds. - The scripted demo run lasts about 167 seconds with 25 flow trades and ends with a keeper distribution that minted real interest to 6 holders. - Two 10-minute runs: 0 failed transactions, 0 HTTP 429, about 0.52 XLM spent in total. ## Running it The bot is a Node program in the repository under `scripts/bot/` (documented in `scripts/bot/README.md`). It refuses to start on any network other than testnet, and its keys live in git-ignored `*.secrets.json` files. --- # FAQ > Short, honest answers to the questions judges and integrators ask. Everything described runs on Stellar testnet unless stated otherwise. Nothing is audited. ## 1. Aquarius already has concentrated liquidity on Soroban. What is new? Ranges are not the innovation; bins and ranges are a known design (Trader Joe LB, Meteora DLMM, Aquarius CLMM). What Lotus adds is direction as the product: a position is one-way by construction (inventory only goes down, proceeds only come in, nothing trades back) and two-way is an explicit opt-in. On top of that the USDC leg can sit in a DeFindex vault on Blend and is recalled just in time by the taker's own swap. Aquarius pools are complementary: the receipt pair can be listed on Aquarius too. ## 2. Why a receipt token if you have a pool? Because the SDEX is where Stellar's liquidity and routers already are. A Blend position or a DeFindex share is a Soroban token: it cannot sit in an SDEX offer or be a path-payment hop. `lUSDCb` is a classic asset, so it can be the quote of a ladder and the middle leg of a path payment, and an SDEX offer does not move funds, so the order keeps earning while it waits. A Stellar core engineer we spoke to also preferred staying on the SDEX over Soroban liquidity walls that appear near the price when a trade comes in. So we kept the pool and made the yield tradable on the SDEX. ## 3. Is the volume real? The transactions are real and on-chain. The source is not users: it is three accounts we run (two market makers and an arbitrageur) trading assets we issue (LBOND) or mint through the vault (the receipt). Their addresses are in `deployments/testnet.bot.json`. We describe it as market-maker volume everywhere, and the arbitrage profit in the demo scenarios is a transfer between our own accounts. There are no users on a hackathon testnet; the point is to exercise the mechanism under load. Measured: depeg restored in 10 to 12 s, cross-venue dislocation closed in 26 s, 0 failed transactions in the two measured 10-minute runs (over the whole day of bot activity, `mm_a` had 11 failed transactions out of about 1,300). ## 4. Where does the yield come from, and why did earlier testnet vaults show none? Blend supply interest, through a DeFindex vault we created and seeded ourselves. The existing PaltaLabs testnet vault earned nothing because DeFindex invests a deposit in proportion to the vault's existing allocation and skips an asset with nothing invested; only the vault's rebalance manager could seed it. We observed about 6.9 percent APR over 34 minutes, which follows Blend's testnet utilization (88.7 percent then; about 1.6 percent at 70 percent utilization). The app showed about 7.6 percent on 26 October. It is an observation, not a forecast. ## 5. Who runs the keeper, and what can it do wrong? We do. `distribute` is keeper-only and the admin can rotate the keeper. The contract caps every mint at `surplus x balance / supply` using balances before any minting, and the total cannot exceed the surplus, so the keeper can omit a holder or be late but cannot over-mint or move one holder's share to another. The known cost of the simple design is **JIT yield capture**: someone who deposits just before a distribution and redeems after captures a pro-rata share they did not earn, and a holder who redeems early forfeits accrued yield. The fix is an exchange-rate token; until then the keeper should run often. The vault-backed pool does not have this issue, since yield accrues in vault shares held by the positions. ## 6. How is the receipt's peg kept? By redemption at par. `deposit` and `redeem` are permissionless and 1:1, so if the receipt trades under one USDC anyone can buy and redeem, and above one anyone can deposit and sell. In the demo our arbitrageur does this: a depeg to 0.985 was restored in 10 to 12 seconds. The `USDC/lUSDCb` book needs a resting liquidity provider; today that is us. ## 7. What if Blend is illiquid? Redemption waits, and the receipt would trade at a discount until liquidity returns. If the vault position ever loses value, redemptions pay pro rata rather than at par, so nobody drains the backing ahead of others. Same risk as any lending receipt. That is what `lUSDC` is for: its USDC is never lent, so orders close to the price and working capital can sit there and redeem instantly; routing near-price orders to it automatically is on the roadmap. ## 8. What can the LBOND issuer do to the pool? LBOND is `AUTH_REQUIRED`, `AUTH_REVOCABLE` and clawback-enabled on purpose (it models a regulated RWA). The issuer must authorize the pool's trustline through the LBOND SAC, and could revoke it. If that happens, swaps that deliver LBOND fail, and a Bid LP's USDC cannot be exchanged for LBOND until the issuer re-authorizes. A quote-only exit for Bid LPs is on the roadmap. This is an issuer-trust property of RWAs on Stellar, not something a venue can remove. ## 9. What did the adversarial review of the pool find? No solvency or overpay bug. Findings and what we did: the vault pool can hit a footprint race (the vault's idle USDC flips by a stroop between simulation and inclusion), so the client re-simulates and resends; Bid LP USDC can be trapped if the LBOND leg is undeliverable (documented); a dead yield branch in the fuzz test (fixed); a fee-free mixed-bin swap in two-way mode (documented, low severity); junk-bin griefing (mitigated by minimum bin size and `max_bins_per_swap`). The vault-backed instance still runs an earlier build of the same source (it holds vault shares); the idle instance was redeployed from the final build. Details in `contracts/lotus_pool/README.md`. ## 10. What will fees look like on mainnet? Unmeasured. On testnet, calls that touch Blend through our vault cost between 0.0015 and 0.14 XLM, and a strategy withdraw used about 3.5M CPU instructions of 400M and 13 of 200 ledger entries. An earlier measurement of about 100 XLM did not reproduce and is retracted. Mainnet rent differs and needs measuring. ## 11. Why Stellar rather than an EVM chain? Three protocol features we could not replicate elsewhere: an orderbook whose offers reserve funds as liabilities, path payments that cross two books atomically, and asset-level authorization and clawback enforced by the network. Plus the SAC: a Soroban contract can administer a classic asset, which is how the receipt exists. The RWA issuers we want first (USDY, Etherfuse Stablebonds) already trade here. ## 12. What is the adoption path? One issuer and one treasury, not retail. An RWA issuer distributes on a ladder (SDEX) or a directional pool position; a treasury accumulating the asset posts bids whose USDC earns while waiting. Takers keep paying in USDC through path payments and routers that cover the SDEX. Market makers on RWA pairs come next, with Aquarius market incentives as the complement. ## 13. Regulatory angle? We are not lawyers and this is a testnet prototype. The contract holds the funds, not us; the yield is a pass-through from a transparent lending market; the Stellar asset model lets an issuer require authorization and use clawback for compliance. A yield-bearing dollar token raises securities and e-money questions that a mainnet launch would need to analyse per jurisdiction. ## 14. Security and audit plan? Small contracts with written invariants and tests: supply never exceeds vault value, distributions are capped on-chain, pool accounting is checked for solvency after every operation of randomized sequences. No audit yet, no pause, no timelock, no upgrade path, and the testnet receipt issuers are not locked. Before mainnet: external audit with OpenZeppelin's audited Stellar library as the base for access control and pausing, lock the receipt issuer after `set_admin`, add a pause and a timelock, and a bug bounty. ## 15. Why can't I exit an order early from the app, even for a fee? Orders and pool positions created from the app stay open until filled ("Early redemption ยท soon"). This is a UI rule, not a contract rule: `remove_position` works at any time and SDEX offers belong to your account, so the CLI, the SDK or the MCP server can always exit. An early exit against a fee (for example 10 percent) is easy to build: on the SDEX it is one classic transaction that cancels the offers and pays the fee atomically. We did not build it on purpose. On testnet there is no real curator whose allocation an early exit would disturb, and no real withdrawable liquidity to protect, so a fee would only simulate a constraint that does not exist yet, and anyone could skip it through the CLI. It belongs in the contract, where it can be enforced: early redemption with an exit fee is on the roadmap for the next contract version.