Skip to content

Proposal: add Jupiter Lend lending/borrowing/looping endpoints (new lending trading type) #672

Description

@fengtality

Scope: Jupiter Lend endpoints in Gateway (lending / borrowing / looping)

Status: scoping (no code yet). Adds a fourth connector trading type — lending — alongside router, amm, clmm, backed by Jupiter Lend (Solana). Kamino is deferred to a later connector; the lending schema/trading-type is designed connector-agnostic so Kamino slots in later without reshaping.

v1 scope: reads + supply/withdraw/borrow/repay and looping (loop/deleverage) — all Jupiter Lend.


1. Why Jupiter-only first is the right call

  • One web3 stack. Jupiter Lend uses classic @solana/web3.js + base64 VersionedTransaction — same as the existing Jupiter connector and Gateway's send path. (Kamino v7's @solana/kit/web3.js-v2 mismatch was the biggest risk; deferring it removes that.)
  • One auth, already wired. Jupiter Lend shares api.jup.ag and the same x-api-key as Jupiter Swap. Gateway already has jupiter.apiKey config and an HTTP client for it — reused directly.
  • Looping composes with Jupiter's own Swap API, which we already integrate — so loop is feasible in v1 (see §4).

2. Earn vs Borrow (the two sides of Jupiter Lend)

Jupiter Lend is "Fluid"-style: two products over a shared liquidity layer.

  • Earn = passive lending / supply-to-yield. You deposit an asset into an Earn vault and receive shares (jlTokens) that accrue supply APY. No borrowing, no liquidation, no collateral. This is a money-market deposit. Endpoints: /lend/v1/earn/{deposit,withdraw,mint,redeem} (deposit/withdraw by amount, mint/redeem by shares).
  • Borrow = collateralized borrowing. You deposit collateral into a Borrow vault, which mints a position NFT, and borrow another asset against it up to the vault's collateralFactor (LTV), subject to a liquidationThreshold. One endpoint — /lend/v1/borrow/operate — drives deposit-collateral / withdraw-collateral / borrow / repay via signed colAmount/debtAmount.

The link: assets supplied on Earn are the liquidity that Borrow users borrow. Looping lives entirely on the Borrow side (flash-borrow + swap + operate), not Earn.

Mapping to our verbs: treat supply/withdraw as covering both sides via a side: 'earn' | 'borrow' param — earn → Earn vault (yield), borrow → deposit/withdraw collateral in a Borrow position. borrow/repay/loop/deleverage are Borrow-only. (Kamino later unifies both under one obligation, so a single verb set with a side hint generalizes.)


3. Endpoint surface (lending trading type)

Per-connector /connectors/jupiter-lend/lending/* and unified /trading/lending/* (connector-param dispatch, mirroring /trading/amm/*).

Reads

Endpoint Backing Returns
GET markets GET /earn/tokens + GET /borrow/vaults per asset/vault: supply APY, borrow APY, LTV (collateralFactor), liquidationThreshold, available liquidity, minimumBorrowing
GET position-info GET /borrow/positions?users= (+ /earn/positions) a position: collateral, borrows (dustBorrow = incl. accrued), current LTV / health
GET positions-owned GET /borrow/positions?users= all of a wallet's Lend positions (NFTs)

Writes (all return the shared signed-and-sent tx result { signature, status, data? } — signature holds a Solana signature or an EVM tx hash, per Gateway's existing convention; §6)

Endpoint Backing
POST supply side=earn → /earn/deposit; side=borrow → operate colAmount>0
POST withdraw side=earn → /earn/withdraw; side=borrow → operate colAmount<0 (MIN_I128=all)
POST borrow operate debtAmount>0 (≥ minimumBorrowing)
POST repay operate debtAmount<0 (MIN_I128=repay-all, clears dust)
POST loop flash-borrow → Jupiter Swap → operate(deposit+borrow) → flash-payback (§4)
POST deleverage unwind recipe (flash-borrow debt → repay → withdraw coll → swap → flash-payback)
GET quote-loop (read) project resulting leverage / LTV / health for a loop without signing

4. Looping in v1 — and reusing the existing Jupiter swap work

A loop is one atomic transaction: flash-borrow(debt) → swap(debt→collateral) → operate(deposit collateral + borrow debt) → flash-payback(debt). Built from @jup-ag/lend ix-builders (getFlashBorrowIx, getOperateIx, getFlashPaybackIx) + the Swap API's instructions, compiled into a v0 message with address lookup tables.

What we reuse from the existing Jupiter connector (src/connectors/jupiter/jupiter.ts):

  • ✅ Auth + host + HTTP client — same api.jup.ag + jupiter.apiKey (jupiter.config.ts:33). The lending connector shares it.
  • ✅ Quote — GET /swap/v1/quote (jupiter.ts:219) gives the debt→collateral route for the loop.
  • ✅ Send path — sendAndConfirmTransactionForWallet already handles VersionedTransaction (solana.ts:1435), so v0 txs send today.

What's new (the gap):

  • ⚠️ The connector today calls POST /swap/v1/swap (jupiter.ts:309), which returns a standalone serialized tx — it cannot be interleaved into the loop. Looping needs composable swap instructions: POST /swap/v1/swap-instructions (raw ixs + addressLookupTableAddresses). Add it as a shared getSwapInstructions() helper on the Jupiter connector so both it and the lending connector reuse it. Staying on v1 keeps the loop on the same mature, already-authenticated API version as the rest of the swap connector (see §4a).
  • ⚠️ v0-message composition from raw ixs + resolved ALTs. Sending a pre-built v0 tx works; building one from composed instructions (fetch the ALT accounts, MessageV0.compile) is new plumbing. Add a small Solana helper (buildV0Tx(ixs, payer, lookupTableAddresses)); reuse it for both simple operate flows and loop.

So: yes, we reuse the Jupiter swap work substantially (auth, client, quote, send) — the only genuinely new pieces are the /swap/v1/swap-instructions call and v0-from-ixs assembly, both small and shared.

4a. Why v1 for the loop (and where v2 belongs)

Jupiter Swap API v2 (api.jup.ag/swap/v2, launched Mar 2026) has two modes: Meta-Aggregator (GET /order + POST /execute, assembled tx + RPC-less managed landing, all routing engines — Metis/JupiterZ/Dflow/OKX) and Router (GET /build, raw composable instructions, Metis-only). v1 is not deprecated, and Ultra is a separate (RPC-less + gasless) product.

For looping, v2 buys nothing functional. The loop composes its own atomic tx, so it can't use v2's headline feature (RPC-less managed landing — that only lands standalone swaps). The only composable path, v2 Router /build, is Metis-only — identical routing to v1 /swap-instructions and returns the same raw-ixs+ALT shape. So the choice is a wash functionally; we pick v1 /swap-instructions because it's the mature endpoint the swap connector already authenticates against and keeps the whole connector on one API version. No net-new code is saved by v2 either — the connector calls /swap/v1/swap today, not /swap-instructions, so composable ixs are new work on either version.

Where v2 does belong (separate fast-follow, not this PR): migrating the live router-routes (quoteSwap/executeSwap/executeQuote) to v2 Meta-Aggregator /order+/execute. That's where the real v2 benefits land (more routing engines, managed landing) — but it changes the execution model on the swap path Hummingbot strategies depend on, so it needs its own PR + regression testing, and it's a genuine tradeoff (managed landing vs Gateway's own Helius/priority-fee send path — see §8.8). No forcing function while v1 lives.

Simple verbs are even easier: /earn/* and /borrow/operate REST endpoints return a ready base64 v0 tx → deserialize → sendAndConfirmTransactionForWallet. No ix composition needed for supply/withdraw/borrow/repay; that's only for loop/deleverage.


5. Position model — positionAddress is a per-connector concern (carry over the DAMM-v2 fix)

Lending protocols split into two position models, exactly like AMMs split into NFT (Meteora) vs fungible (Raydium/Uniswap). The lending schema must treat positionAddress as a per-connector optional, not a universal field:

Model Protocols Position identity positionAddress positions-owned
Multi-position (NFT / obligation) Jupiter Lend (NFT positionId), Kamino (obligation PDA per type) several per wallet per market required on writes that target an existing position enumerates them
Account-based (fungible) Aave (aTokens + debt tokens; one health factor per account) one aggregate per wallet per market ignored (single position) returns the single aggregate (or n/a)

For the multi-position connectors, apply the pattern we shipped for Meteora DAMM v2:

  • position-info returns the position(s) + identifiers; positions-owned lists all.
  • Writes that target an existing position (withdraw/repay/borrow-against/deleverage) take a positionAddress/positionId (required when acting on an existing position); supply(open)/loop(open) may omit it → new position. Prevents "repay-all / withdraw-all" hitting the wrong position when several exist.

For account-based connectors (Aave), positionAddress is ignored and there's a single aggregate position — the same way the fungible-LP AMMs ignore it today. No schema reshaping is needed to add Aave — it's the "fungible" branch of a field the schema already carries as optional.


6. Standardized schema (src/schemas/lending-schema.ts)

Connector-agnostic TypeBox (Jupiter now; Kamino-ready):

  • ReserveInfo — { marketAddress, tokenSymbol, tokenAddress, supplyApy, borrowApy, maxLtvPct, liquidationThresholdPct, availableLiquidity, price }
  • LendingPositionInfo — { marketAddress, walletAddress, positionAddress, side, deposits[], borrows[], currentLtvPct, liquidationLtvPct, healthFactor } (+ optional positions[] breakdown)
  • SupplyRequest/WithdrawRequest — { network, walletAddress, tokenSymbol|marketAddress, amount, side?: 'earn'|'borrow', positionAddress? } (side/positionAddress are per-connector optionals, §5/§10)
  • BorrowRequest/RepayRequest — { network, walletAddress, marketAddress, tokenSymbol, amount, positionAddress? } (repay: amount|'ALL')
  • LoopRequest — { network, walletAddress, collateralToken, debtToken, principalAmount, targetLeverage, slippagePct, positionAddress? }
  • Write response — reuse the shared AMM/CLMM ExecuteSwapResponse-style shape: { signature, status, data? }. This is already chain-neutral in Gateway: the signature field carries a Solana signature on Solana and an EVM tx hash on EVM (see uniswap/amm-routes/executeSwap.ts:244 → signature: receipt.transactionHash). So no txHash field is needed — Aave/EVM connectors populate signature with the receipt hash, matching the existing convention.

Per-connector jupiter-lend/schemas.ts adds Jupiter specifics (vaultId, positionId, market: 'main'|'ethena', side).


7. Architecture insertion points (mirrors the AMM work)

Create

  • src/schemas/lending-schema.ts
  • src/connectors/jupiter-lend/ — jupiter-lend.ts (singleton getInstance(network), binds Solana.getInstance, reuses Jupiter API client/auth), jupiter-lend.config.ts (tradingTypes=['lending'], apiKey/base — can share jupiter.apiKey), jupiter-lend.routes.ts (exports { lending }, tag /connector/jupiter-lend), schemas.ts, lending-routes/ (one file per op + index.ts; each exports a worker fn + *Route).
  • src/trading/trading-lending-routes/ — {markets,position-info,positions-owned,supply,withdraw,borrow,repay,loop,deleverage,quote-loop}.ts + common.ts (LENDING_CONNECTORS=['jupiter-lend'], parseChainNetwork) + index.ts.
  • src/templates/connectors/jupiter-lend.yml + src/templates/namespace/jupiter-lend-schema.json.
  • A shared getSwapInstructions() on the Jupiter connector + a buildV0Tx(ixs, payer, lutAddresses) Solana helper (for loop).

Edit

  • src/trading/trading.routes.ts — add tradingLendingRoutes.
  • src/app.ts — import barrels; swagger tags /trading/lending, /connector/jupiter-lend; app.register(tradingLendingRoutes, { prefix: '/trading/lending' }); app.register(jupiterLendRoutes.lending, { prefix: '/connectors/jupiter-lend/lending' }).
  • src/config/routes/getConnectors.ts — register JupiterLendConfig with trading_types: ['lending'].
  • src/templates/root.yml — $namespace jupiter-lend: block.

8. Open decisions & risks

  1. Ix-builders vs REST for writes. Simple verbs: REST returns a ready v0 tx (minimal). Loop: needs composition → use @jup-ag/lend SDK ix-builders (getFlashBorrowIx/getOperateIx/getFlashPaybackIx) + Swap /swap-instructions. Decide whether to use the SDK for all writes (consistency) or REST for simple + SDK for loop (less surface). Leaning: REST for simple verbs, SDK for loop.
  2. side: earn|borrow param design — confirm one verb set with a side hint vs separate earn-* routes. Leaning: one set + side. Keep side a per-connector optional — it's Jupiter's Earn/Borrow split; Aave/Kamino unify supply=collateral+yield and ignore it (§10).
  3. Health guardrails. borrow/loop responses (and quote-loop) should surface resulting LTV / health / liquidation price so callers don't over-lever. Add a maxLtvPct safety check that 400s before signing.
  4. WSOL + ATA. Jupiter Lend Borrow does not wrap SOL (caller pre-wraps WSOL) and auto-creates the borrow ATA — handle WSOL wrap/unwrap in the connector.
  5. Rate units. /earn/tokens rates are raw fixed-point (~1e12); compute APY in the handler (no pre-formatted %). /borrow/vaults carries rate fields directly.
  6. API key. Default api.jup.ag + jupiter.apiKey; keyless lite-api.jup.ag is deprecating — don't default to it. No hardcoded key.
  7. Live field verification. Pull real /earn/tokens and /borrow/vaults responses with a key to lock exact JSON field names/scaling before finalizing schemas.
  8. RPC-less tradeoff (fast-follow only). v2 Meta-Aggregator's managed landing (/execute) is RPC-less — Jupiter handles priority fees / retries / staked-Jito landing. Upside: better landing under congestion, less client code. Downside for Gateway: it bypasses Gateway's own send path (sendAndConfirmTransactionForWallet — simulation, retries, Helius sender endpoints, priority-fee/compute-budget control), adds an external landing dependency, and doesn't compose (standalone swaps only). Even then Gateway still needs an RPC to read the confirmed tx for its {signature, balanceChanges} response — so "RPC-less" only removes the landing RPC calls. Net: adopt deliberately in the swap-migration PR, not by default; does not affect this lending work (loop is RPC-full via v1 /swap-instructions + buildV0Tx).

9. Phasing (Jupiter-only)

  • v1 — lending type + jupiter-lend connector; reads (markets, position-info, positions-owned); writes supply/withdraw/borrow/repay; loop/deleverage (+ quote-loop); position-addressed; unified /trading/lending/*. Reuses Jupiter auth/client/quote/send; adds /swap-instructions + buildV0Tx.
  • Fast-follow — migrate the existing Jupiter swap connector (router-routes) from Swap API v1 to v2 Meta-Aggregator (/order+/execute) where the real v2 benefits apply (more routing engines, managed landing). Separate PR (live-path execution-model change; own regression testing; RPC-less tradeoff per §8.8). This lending work stays entirely on v1.
  • Later — Kamino connector (Solana, resolve the @solana/kit bridge); Aave v3 connector (EVM) — both under the same lending schema (see §10); optional upstream (hummingbot-api → SDK → condor manage_lending) if agent-driven.

10. Chain- & model-agnostic design (future integrations: Aave v3 / EVM)

The lending trading type must be chain- and connector-agnostic from day one — same as amm, which serves Solana (meteora/raydium) and EVM (uniswap/pancakeswap) under one schema and dispatches by chainNetwork. Jupiter Lend (v1) is Solana-only, but the schema and /trading/lending/* layer are designed so Aave v3 (EVM) slots in as another connector with no reshaping. What must stay neutral, and how Aave maps:

Axis Solana (Jupiter Lend, Kamino) EVM (Aave v3) Design consequence
SDK / client @jup-ag/lend REST+SDK / klend-sdk @aave/client (viem client-actions, sendWith(viem|ethers)) connector-local; only the shared schema is common
Position model multi-position (NFT / obligation) → position-addressed account-based: aTokens + variable/stable debt, one health factor per account → single aggregate positionAddress stays a per-connector optional (§5); Aave is the "fungible" branch — ignored
Earn vs Borrow Jupiter splits Earn/Borrow (side param) unified — supplying both earns yield and is collateral (toggle usageAsCollateral) side is a Jupiter-specific optional, not universal; Aave/Kamino ignore it
Tx model build v0 tx / VersionedTransaction → sendAndConfirmTransactionForWallet build calldata → sign+send via Gateway's ethereum chain (ethers) shared {signature, status, data} response is already chain-neutral — signature = Solana sig or EVM tx hash (repo convention, §6); buildV0Tx is Solana-only
Approvals ATA auto-create; pre-wrap WSOL ERC-20 approve or EIP-2612 permit before supply/repay EVM lending connectors reuse Gateway ethereum allowances/approve flow (permit preferred)
Loop swap leg Jupiter /swap/v1/swap-instructions EVM DEX (uniswap/0x, already in Gateway) + Aave flash loan loop resolves its swap+flash-loan provider per chain; the endpoint shape is identical
Rates / risk compute APY from fixed-point rates; LTV/health derived Aave exposes reserve APYs, LTV, liquidation threshold, and a native health factor ReserveInfo / LendingPositionInfo (§6) already cover APY/LTV/liqThreshold/healthFactor
Discovery Jupiter /tokens,/vaults; Kamino API Aave market/reserve addresses per network (address-book / chains action) dynamic per connector; no hardcoded addresses

Net: nothing Jupiter-specific may leak into src/schemas/lending-schema.ts or the unified routes. Keep positionAddress and side optional (per-connector), reuse the existing chain-neutral write response (signature = Solana sig or EVM tx hash, §6 — no new field), and resolve the loop's swap+flash-loan provider per chain. With that, Aave v3 is a new connector folder + config, mirroring how uniswap joined the AMM type — not a schema change. (Same generalization covers Kamino on the Solana side.)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions