A fast, keychain-secured, agent-friendly command line for the Lofty fractional real-estate marketplace: market data, order books, LP-reward (market-making) programs, and — with a trading-enabled API key — limit orders and AMM swaps.
Built on Lofty's official SDK wire contract (@loftyaicode/sdk):
https://api.lofty.ai/public/v1/* with Authorization: Bearer lofty_live_….
Every command has a --json mode emitting a schema-tagged DTO, so it composes
cleanly in scripts and agents.
Not affiliated with or endorsed by Lofty. An independent, unofficial client. Trading commands move real money on your real account — read Safety.
# from source (recent stable Rust; MSRV 1.88)
$ cargo install --git https://github.com/piekstra/lofty-cli --locked
# or clone + build
$ git clone https://github.com/piekstra/lofty-cli && cd lofty-cli
$ make install # cargo install --path . --forcePrebuilt binaries for macOS and Linux are attached to each
release; once installed,
lofty self-update upgrades in place.
$ lofty auth login # paste your API key (Lofty → Account → Settings → API Keys)
$ lofty auth status --json
$ lofty rewards programs # who pays for liquidity, and the rulesThe key is verified against /account/balance before it's stored (skip with
--no-verify) and lives only in the OS keychain (service piekstra.lofty) —
never on disk, never in argv, never in logs. Enable Trading on the key in
your Lofty dashboard to place/cancel orders and swap.
$ lofty properties list # marketplace listings
$ lofty properties orderbook <ID> # live bids/asks
$ lofty properties trades <ID> # recent fills + best bid/ask
$ lofty rewards programs # LP-reward programs and their terms
$ lofty rewards history --since <ms> # your reward payouts
$ lofty rewards reconcile # audit payouts: totals, math check, gap detection
$ lofty rewards eligibility # are your orders earning right now, and if not why
$ lofty account balance|positions|trades
$ lofty account coverage # what's reserved backing your bids vs free to spend
$ lofty account breakeven --margin 5 # fee-inclusive sell price (basis hides the buy fee)
$ lofty orders list|get|create|cancel # mutations confirm, or --force
$ lofty quote recenter --property-id <ID> --bid <P> # move a quote safely (dry run by default)
$ lofty quote provision --property-id <ID> --bid <P> --ask <Q> # post a fresh two-sided quote
$ lofty quote pull --property-id <ID> --keep-above <P> # stand down, keeping recovery asks
$ lofty amm pools|quote|swap # swap needs a slippage bound (see Safety)
$ lofty api GET /public/v1/amm/pools # raw passthrough (Bearer attached)
$ lofty api --internal GET /properties/v2/marketplace # website API (open reads)
$ lofty catalog --group exchange # observed endpoint inventory$ lofty rewards programs
ALLOWEDSPREAD | DAILYREWARDS | MINCONTRACTS | MINTWOSIDEDLIQUIDITY | PROPERTYID | SLUG
2 | 10 | 4 | 1 | 01FMN4Q2M9MW33ERXJKG61MTRD | 2094-W-34th-Place_Cleveland-OH
2 | 10 | 2 | 1 | 01JGMSZ4NEGCFF8T8CEE5XXYQ7 | 2221-E-Chase-St_Baltimore-MD$ lofty --json rewards programs | jq '.programs[0]'
{
"address": { "line1": "2094 W 34th Place", "line2": "Cleveland, OH 44113" },
"allowedSpread": 2,
"blockDurationMs": 3600000,
"blocksPerDay": 24,
"dailyRewards": 10,
"minContracts": 4,
"minOrderAgeMs": 3000,
"minTwoSidedLiquidity": 1
}reconcile pulls your full payout history and checks it: per-property totals,
an internal-consistency check (MISMATCH counts payouts where the amount ≠
percentOfPool × pool-per-block — a nonzero value is worth reporting to Lofty),
and GAPS (payout periods that got nothing mid-streak — candidate misses to
investigate; some are legitimate ineligibility). Numbers below are illustrative.
$ lofty rewards reconcile
AVG%POOL | EARNED | GAPS | MISMATCH | PAYOUTS | PROPERTY
20.0 | $3.5000 | 1 | 0 | 48 | Sample City, ST 00000
total: $3.5000 across 48 payout(s); 1 gap period(s)
⚠ Sample City, ST 00000: 1 period(s) with no payout — verify eligibility: 2026-01-02 13:00 UTCLP rewards have several qualification rules that must all hold at once, and missing any one silently pays you nothing. The costliest is that liquidity must be two-sided — a bid alone earns nothing, no matter how large or how close to mid it rests.
eligibility checks every open order against the published rules (two-sided,
at least minContracts, within allowedSpread of the book mid, covered by your
balances), projects your pool share from the published score formula, and names
the reason whenever something earns nothing. Numbers below are illustrative.
$ lofty rewards eligibility
Sample City, ST 00000: EARNING — 27.4% of pool ≈ $2.74/day (mid $61.63, competing score 5.0)
Other City, ST 11111: $0 — not two-sided: no qualifying ASK (one-sided liquidity earns nothing)
sell $13.60 x1 — out of band, below minContracts
projected total: $2.74/day across 2 propertiesQualifying a side and scoring are separate: an order that is sized and covered establishes that side exists, while only in-band orders accrue score — so a position can be two-sided yet still score nothing, and the output distinguishes the two rather than collapsing them into one boolean.
Lofty funds open orders from your live balances: buys need the USDC and sells need the shares, for as long as the order is open, across all your open orders on a property combined — and orders your wallet can't cover are canceled automatically. The check is per property, not aggregate, so several properties' bids can lean on the same USDC at once. Two consequences your raw balance won't tell you:
- The wallet floor every bid must clear is your largest single-property bid total — that much is reserved, and only the surplus above it is spendable.
- The sum of your bids can legitimately exceed your wallet while they rest
(
overCommitted) — but one fill spends cash the other properties were also counting on, which can cascade into automatic cancellations.
coverage computes both, checks every order against its cover, and with
--spend simulates a purchase to show what it would uncover. Numbers below are
illustrative.
$ lofty account coverage
ASK COVERED | ASK QTY | BID $ | BID COVERED | HELD | PROPERTY
true | 4 | 190.00 | true | 4 | 01SAMPLEPROPERTY000000000A
true | 8 | 120.00 | true | 8 | 01SAMPLEPROPERTY000000000B
wallet $200.00 — $190.00 reserved (largest single-property bid), $10.00 free to spend
note: $310.00 total bid exposure exceeds the wallet — fine while they rest
(coverage is per property), but one fill spends cash the others also rely on
$ lofty account coverage --spend 50
...
spending $50.00 leaves $150.00 — EXCEEDS free capital
⚠ would uncover 01SAMPLEPROPERTY000000000A (bids $190.00)A position's costBasis is what you paid for the tokens. The platform buy fee
was charged on top of it, so it is not your true cost — and two fees bracket
a round trip, both paid out of the sale:
trueCost = costBasis × (1 + buyFee)
ask = trueCost × (1 + margin) / (1 − sellFee)
Selling at your displayed basis therefore books a loss twice over. breakeven
does the arithmetic with live fee rates — read from the property
(mtSellFeePct) and its AMM pool, so it reflects the venue you actually trade on.
AMM rates are all-in: a swap pays the platform fee plus the pool's LP fee,
which the pool reports as separate lines (e.g. buy = platformBuy 3% + lp 2% =
5%). The order book has no LP fee.
The buy fee you paid depends on where you bought, which the API doesn't record —
so both venues are priced and labelled rather than guessing. Pass --buy-fee if
you know it. Numbers below are illustrative.
$ lofty account breakeven --margin 5
01SAMPLEPROPERTY000000000A: cost basis $63.68/token (excludes the buy fee) — selling on book at 3.50% fee
ACQUIRED VIA | ASK +5.0% | BREAK-EVEN ASK | BUY FEE % | TRUE COST
amm | $72.75 | $69.29 | 5.00 | $66.86
book | $71.37 | $67.97 | 3.00 | $65.59So a $63.68 basis needs $69.29 just to break even, and $72.75 to net 5%.
--sell-venue amm prices an exit into the pool instead, --cost prices a
hypothetical position, and --sell-fee / --buy-fee override the published rates.
quote recenter is a mechanism, not a strategy: you supply the target prices
and it performs the move safely. It has no opinion about where to quote.
It is a dry run unless you pass --execute — the dangerous thing needs a
flag, the safe thing happens when you forget one.
$ lofty quote recenter --property-id <ID> --bid 61.50
DRY RUN — nothing sent. mid $62.98, reward band [$60.98, $64.97]
cancel buy $62.25 x3
create buy $61.50 x3 (1.48 from mid)
keep sell $72.80 x8 (untouched)
re-run with --execute to applySides you don't give a price for are left alone, so re-centering a bid can never orphan an earning ask into a non-earning one-sided position. Before sending anything it refuses to:
| Rail | Why |
|---|---|
| cross the market | a bid at/above the best ask fills instantly as a taker — this command only rests liquidity |
| exceed your cover | Lofty auto-cancels uncovered orders (counting the side left in place) |
go below minContracts |
the order would rest but earn nothing |
| leave the reward band | same — earns nothing (override with --allow-out-of-band) |
undercut --min-ask |
guards a break-even/cost floor against an accidental below-cost sell |
Cancel always precedes the matching create: posting first would double-commit capital and can breach per-property coverage, which auto-cancels both orders. The trade-off is a brief one-sided window (the API has no atomic modify) — if the re-post fails, the command says so loudly, because that window earns nothing.
provision posts a fresh two-sided quote and refuses to leave you one-sided —
if either leg fails a rail (usually too few tokens to back the ask), nothing is
sent and the shortfall is reported, because the half that would rest costs cover
and earns nothing. It never buys inventory for you; that stays an explicit
amm swap.
pull stands down from a property. --keep-above protects orders you mean to
leave resting — typically a cost-basis recovery ask parked above the band — so
standing down never sweeps away the sell that's waiting to recover your position:
$ lofty quote pull --property-id <ID> --keep-above 13.00
DRY RUN — nothing sent.
keep sell $13.60 x1 (untouched)
re-run with --execute to applyAll three share one implementation of the rails (check_placement), so they
cannot drift apart.
--jsonon any command prints one machine-readable DTO to stdout, tagged with aschemafield (e.g."schema":"rewards-programs/v1"); diagnostics go to stderr, never mixed in.- Exit codes:
0ok ·2usage ·3auth ·4not found ·5upstream ·6confirmation required (non-interactive without--force). - Rate limits (per key): 300 reads/min, 30 writes/min; writes also capped at 60/min per account across keys.
Swap slippage bounds are checked, not trusted. Every amm swap prices a
fresh quote first and compares your bound against it, reporting the tolerance it
implies:
$ lofty amm swap --pool-id <ID> --side buy --tokens 1 --max-usdc 56.00
error: bound $56.00 is 9.2% from the live quote $51.27 — that authorises far more
slippage than you likely intend. Use --max-slippage-pct for an exact bound, or
--allow-high-slippage to proceed.A bound is refused above 5% from the quote — pinned to the platform's own fee
load (3% platform + 2% pool LP), not to taste: a tolerance wider than the entire
fee schedule is far likelier a typo or a rounded guess than an intention. Prefer
--max-slippage-pct 1, which derives the bound from the quote instead of asking
you to pick a number. The check applies under --force too, where no prompt
is shown and a loose bound would otherwise pass unseen.
Placing orders, cancelling, and swapping are real trades on your real
account. They prompt for confirmation; in scripts and agents pass --force
explicitly to proceed non-interactively (otherwise they exit 6). Writes carry
an automatic Idempotency-Key, so retrying a failed create won't double-submit.
Read-only by default: nothing mutates without an explicit mutating subcommand.
$ make verify # fmt-check + clippy (-D warnings) + tests + smoke
$ make test # unit + integration
$ cargo run -- rewards programsTests are fully offline: raw captured API responses live in
tests/fixtures/ as independent files (see its README) and the contract tests
load them from disk — no network, no credentials. See
CONTRIBUTING.md and AGENTS.md.
MIT © piekstra. Contributions welcome under the same terms — see CONTRIBUTING.md.