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
- 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.
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).
- 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.
- 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.
- Rate units.
/earn/tokens rates are raw fixed-point (~1e12); compute APY in the handler (no pre-formatted %). /borrow/vaults carries rate fields directly.
- API key. Default
api.jup.ag + jupiter.apiKey; keyless lite-api.jup.ag is deprecating — don't default to it. No hardcoded key.
- Live field verification. Pull real
/earn/tokens and /borrow/vaults responses with a key to lock exact JSON field names/scaling before finalizing schemas.
- 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.)
Scope: Jupiter Lend endpoints in Gateway (lending / borrowing / looping)
Status: scoping (no code yet). Adds a fourth connector trading type —
lending— alongsiderouter,amm,clmm, backed by Jupiter Lend (Solana). Kamino is deferred to a later connector; thelendingschema/trading-type is designed connector-agnostic so Kamino slots in later without reshaping.v1 scope: reads +
supply/withdraw/borrow/repayand looping (loop/deleverage) — all Jupiter Lend.1. Why Jupiter-only first is the right call
@solana/web3.js+ base64VersionedTransaction— 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.)api.jup.agand the samex-api-keyas Jupiter Swap. Gateway already hasjupiter.apiKeyconfig and an HTTP client for it — reused directly.loopis 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.
/lend/v1/earn/{deposit,withdraw,mint,redeem}(deposit/withdraw byamount, mint/redeem byshares).collateralFactor(LTV), subject to aliquidationThreshold. One endpoint —/lend/v1/borrow/operate— drives deposit-collateral / withdraw-collateral / borrow / repay via signedcolAmount/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/withdrawas covering both sides via aside: 'earn' | 'borrow'param —earn→ Earn vault (yield),borrow→ deposit/withdraw collateral in a Borrow position.borrow/repay/loop/deleverageare Borrow-only. (Kamino later unifies both under one obligation, so a single verb set with asidehint generalizes.)3. Endpoint surface (
lendingtrading type)Per-connector
/connectors/jupiter-lend/lending/*and unified/trading/lending/*(connector-param dispatch, mirroring/trading/amm/*).Reads
GET marketsGET /earn/tokens+GET /borrow/vaultscollateralFactor),liquidationThreshold, available liquidity,minimumBorrowingGET position-infoGET /borrow/positions?users=(+/earn/positions)dustBorrow= incl. accrued), current LTV / healthGET positions-ownedGET /borrow/positions?users=Writes (all return the shared signed-and-sent tx result
{ signature, status, data? }—signatureholds a Solana signature or an EVM tx hash, per Gateway's existing convention; §6)POST supplyside=earn→/earn/deposit;side=borrow→operate colAmount>0POST withdrawside=earn→/earn/withdraw;side=borrow→operate colAmount<0(MIN_I128=all)POST borrowoperate debtAmount>0(≥minimumBorrowing)POST repayoperate debtAmount<0(MIN_I128=repay-all, clears dust)POST loopoperate(deposit+borrow)→ flash-payback (§4)POST deleverageGET quote-loop(read)loopwithout signing4. 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/lendix-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):api.jup.ag+jupiter.apiKey(jupiter.config.ts:33). The lending connector shares it.GET /swap/v1/quote(jupiter.ts:219) gives the debt→collateral route for the loop.sendAndConfirmTransactionForWalletalready handlesVersionedTransaction(solana.ts:1435), so v0 txs send today.What's new (the gap):
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 sharedgetSwapInstructions()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).MessageV0.compile) is new plumbing. Add a small Solana helper (buildV0Tx(ixs, payer, lookupTableAddresses)); reuse it for both simpleoperateflows 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-instructionscall 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-instructionsand returns the same raw-ixs+ALT shape. So the choice is a wash functionally; we pick v1/swap-instructionsbecause 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/swaptoday, 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/operateREST endpoints return a ready base64 v0 tx → deserialize →sendAndConfirmTransactionForWallet. No ix composition needed for supply/withdraw/borrow/repay; that's only forloop/deleverage.5. Position model —
positionAddressis 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
lendingschema must treatpositionAddressas a per-connector optional, not a universal field:positionAddresspositions-ownedpositionId), Kamino (obligation PDA per type)For the multi-position connectors, apply the pattern we shipped for Meteora DAMM v2:
position-inforeturns the position(s) + identifiers;positions-ownedlists all.withdraw/repay/borrow-against/deleverage) take apositionAddress/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),
positionAddressis 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 }(+ optionalpositions[]breakdown)SupplyRequest/WithdrawRequest—{ network, walletAddress, tokenSymbol|marketAddress, amount, side?: 'earn'|'borrow', positionAddress? }(side/positionAddressare 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? }ExecuteSwapResponse-style shape:{ signature, status, data? }. This is already chain-neutral in Gateway: thesignaturefield carries a Solana signature on Solana and an EVM tx hash on EVM (seeuniswap/amm-routes/executeSwap.ts:244→signature: receipt.transactionHash). So notxHashfield is needed — Aave/EVM connectors populatesignaturewith the receipt hash, matching the existing convention.Per-connector
jupiter-lend/schemas.tsadds Jupiter specifics (vaultId,positionId,market: 'main'|'ethena',side).7. Architecture insertion points (mirrors the AMM work)
Create
src/schemas/lending-schema.tssrc/connectors/jupiter-lend/—jupiter-lend.ts(singletongetInstance(network), bindsSolana.getInstance, reuses Jupiter API client/auth),jupiter-lend.config.ts(tradingTypes=['lending'],apiKey/base — can sharejupiter.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.getSwapInstructions()on the Jupiter connector + abuildV0Tx(ixs, payer, lutAddresses)Solana helper (for loop).Edit
src/trading/trading.routes.ts— addtradingLendingRoutes.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— registerJupiterLendConfigwithtrading_types: ['lending'].src/templates/root.yml—$namespace jupiter-lend:block.8. Open decisions & risks
@jup-ag/lendSDK 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.side: earn|borrowparam design — confirm one verb set with asidehint vs separateearn-*routes. Leaning: one set +side. Keepsidea per-connector optional — it's Jupiter's Earn/Borrow split; Aave/Kamino unify supply=collateral+yield and ignore it (§10).borrow/loopresponses (andquote-loop) should surface resulting LTV / health / liquidation price so callers don't over-lever. Add amaxLtvPctsafety check that 400s before signing./earn/tokensrates are raw fixed-point (~1e12); compute APY in the handler (no pre-formatted %)./borrow/vaultscarries rate fields directly.api.jup.ag+jupiter.apiKey; keylesslite-api.jup.agis deprecating — don't default to it. No hardcoded key./earn/tokensand/borrow/vaultsresponses with a key to lock exact JSON field names/scaling before finalizing schemas./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)
lendingtype +jupiter-lendconnector; reads (markets,position-info,positions-owned); writessupply/withdraw/borrow/repay;loop/deleverage(+quote-loop); position-addressed; unified/trading/lending/*. Reuses Jupiter auth/client/quote/send; adds/swap-instructions+buildV0Tx.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.@solana/kitbridge); Aave v3 connector (EVM) — both under the samelendingschema (see §10); optional upstream (hummingbot-api → SDK → condormanage_lending) if agent-driven.10. Chain- & model-agnostic design (future integrations: Aave v3 / EVM)
The
lendingtrading type must be chain- and connector-agnostic from day one — same asamm, which serves Solana (meteora/raydium) and EVM (uniswap/pancakeswap) under one schema and dispatches bychainNetwork. 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:@jup-ag/lendREST+SDK / klend-sdk@aave/client(viem client-actions,sendWith(viem|ethers))positionAddressstays a per-connector optional (§5); Aave is the "fungible" branch — ignoredsideparam)usageAsCollateral)sideis a Jupiter-specific optional, not universal; Aave/Kamino ignore itVersionedTransaction→sendAndConfirmTransactionForWallet{signature, status, data}response is already chain-neutral —signature= Solana sig or EVM tx hash (repo convention, §6);buildV0Txis Solana-only/swap/v1/swap-instructionsloopresolves its swap+flash-loan provider per chain; the endpoint shape is identicalReserveInfo/LendingPositionInfo(§6) already cover APY/LTV/liqThreshold/healthFactor/tokens,/vaults; Kamino APIchainsaction)Net: nothing Jupiter-specific may leak into
src/schemas/lending-schema.tsor the unified routes. KeeppositionAddressandsideoptional (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.)