Building a DeFi app on the 1delta API: lending, swaps, earn, and leverage over one contract
The earlier articles in this series covered the read side - normalized lending data across 3,000+ markets. This one is about the write side: how the /v1/actions endpoints turn an intent into ready-to-sign calldata, and how the same request/response contract builds plain lending, swaps, yield optimization, and leveraged positions.

The earlier articles in this series made a read argument: on-chain lending is fragmented, and the 1delta API collapses 3,000+ disjoint markets into one normalized surface you can query. That surface lives under /v1/data/* - pools, positions, rates, prices, all in one shape.
This article is about the other half. Reading the chain tells your app what's true; it doesn't move anything. To ship a product, you need the write side: turn "deposit this," "swap that," "open 3× long ETH" into a transaction a wallet can sign. That's what the /v1/actions/* endpoints do - they take an intent and hand you back ready-to-sign calldata, with approvals, routing, and (for the composed flows) flash loans already worked out.
The thesis of this piece is that four different DeFi products - plain lending, swaps, yield optimization, and leverage - are the same request against four endpoints. Once you learn the contract once, you've learned all four. Let's build them.
The two halves of the API
Every integration on 1delta is a loop between two endpoint families. The data layer answers what should I do?; the actions layer answers give me the transaction that does it.
Both halves share one base URL - https://portal.1delta.io - and one auth header. Send x-api-key on every request; you can mint a key in a minute, and unauthenticated calls are throttled to 10 requests per 15 minutes, which is enough to kick the tyres but not to run a product. As the agents case study showed, the clean way to hold that key is a thin server-side proxy that injects it, so the key never reaches the browser:
frontend → your proxy (adds x-api-key) → portal.1delta.io
That's the entire setup. Now the part that makes building fast: the actions layer has one contract, and every product reuses it.
One contract, every action
Each /v1/actions/* endpoint is a single GET (a couple also accept POST for large batches) that behaves two ways depending on one parameter:
- Omit
account→ you get a quote. Prices, expected output, projected rate impact. Nothing to sign. This is what you render while the user is still deciding. - Include
account(plusoperator, the signing wallet) → the API builds the full transaction calldata.
The built response is always the same envelope: { success, data, actions }. The actions object - an ActionSet - is what you sign, and it has three lists:
| Field | What's in it |
|---|---|
permissions | Approval / delegation txns that must land before the main call. Auto-filtered - if the wallet already has sufficient allowance, this is empty. |
transactions | Pre-trade setup (e.g. an e-mode switch) plus the main call itself, in order. |
alternatives | For anything involving a swap: the candidate DEX-aggregator routes, sorted best-output-first. |
Each entry is a plain { to, data, value } you can hand straight to eth_sendTransaction, viem's sendTransaction, or a relayer - no ABIs on your side. The simpler lending endpoints return a single data.transaction plus data.permissionTxns[] rather than the full ActionSet; same idea, fewer moving parts.
Two flags do a lot of work across all of them:
modepicks the execution path.directcalls the underlying protocol contract straight;proxyroutes through the 1delta Composer, which is what makes multi-step flows atomic. Single-step actions can use either; composed actions (leverage, zaps, migrations) need the composer.simulate=truetells the API to fetch the user's on-chain state and return projected post-trade metrics - the health factor and balances the position will have once this lands. That's your "you'll be at 1.42 HF after this" preview, computed server-side instead of in your UI.
Learn that once and the four products below are mostly a matter of which endpoint and which two or three parameters.
1. Plain lending
The simplest surface, and the one that establishes the pattern. Four verbs - deposit, withdraw, borrow, repay - each a GET under /v1/actions/lending/. You identify the market the way the whole API does: a marketUid of the form lender:chainId:address, e.g. AAVE_V3:42161:0x82aF… for the USDC market on Aave V3 on Arbitrum. You got that UID from the data layer's /v1/data/lending/pools in the aggregation article; now you spend it.
A deposit, quote first:
GET /v1/actions/lending/deposit
?marketUid=AAVE_V3:42161:0x82aF...
&amount=1000000000 # 1,000 USDC, in wei (6 decimals)
Add the user and you get calldata back instead:
GET /v1/actions/lending/deposit
?marketUid=AAVE_V3:42161:0x82aF...
&amount=1000000000
&operator=0xUser... # the signing wallet
&simulate=true # include projected post-trade health
The response carries data.transaction (the deposit call) and data.permissionTxns[] (the ERC-20 approval - present only if the wallet hasn't already approved enough). Your app sends the permission txns, then the main one. withdraw, borrow, and repay are the mirror images, with the parameters you'd expect:
| Endpoint | Notable parameters |
|---|---|
deposit | payAsset (pay with a different token, or the zero address to pay with native ETH) |
withdraw | isAll=true to exit the full balance without dust; receiveAsset to take native ETH out |
borrow | lendingMode (2=variable, 1=stable); receiveAsset |
repay | isAll=true to clear the debt exactly; payAsset |
Two more rounding-out endpoints: /v1/actions/lending/enable-collateral toggles an asset on or off as collateral (Aave V2/V3, Compound via enterMarkets), and /v1/actions/lending/mode switches Aave V3 e-mode - exposed under the protocol-agnostic name "mode" so your code doesn't hard-code one lender's vocabulary. The same four verbs work against every cross-margin and isolated lender the API indexes; your deposit form never branches on whether the destination is Aave, Compound, Morpho, or a Silo vault.
The payoff of the unified
marketUid: a "Supply" button in your UI is one code path that targets any of 3,000+ markets by swapping a string.
2. Swaps
Spot swaps run through one endpoint - /v1/actions/swap/spot - and it's a meta-aggregator: it queries multiple DEX aggregators and returns their routes ranked by output, so you don't integrate 0x, 1inch, Paraswap, and friends yourself.
The contract is identical to lending. Omit account for a quote:
GET /v1/actions/swap/spot
?chainId=42161
&tokenIn=0xaf88...USDC
&tokenOut=0x82aF...WETH
&amount=1000000000
&slippage=50 # basis points (0.5%)
&tradeType=0 # 0 = EXACT_INPUT, 1 = EXACT_OUTPUT
That returns a SpotQuoteResponse - input/output currency info and a quotes array with each venue's expected fill. Include account and the response's actions.alternatives[] comes back as the ranked list of executable swap transactions, best output first. Most integrations just take alternatives[0]; some present the top few and let the user pick. Either way each is a { to, data, value } ready to sign, and any required token approval is already sitting in permissions[].
One nice touch for the structured-products crowd: usePendleMintRedeem=true lets the router use Pendle's mint/redeem path for PT/YT tokens instead of a pool swap, which often prices large PT trades better. A cross-chain variant (/v1/actions/swap/x-chain) is specced but not yet live - it currently returns 501, so build against spot today.
3. Earn / yield optimization
"Earn" is where the data and actions layers really come together, because the interesting part isn't executing a deposit - you already have that - it's deciding which deposit. The API gives you three escalating tools.
Compare raw yields. /v1/data/lending/yields/by-asset/latest returns the best supply rates for an asset across every lender on a chain; /v1/data/lending/yields/intrinsic/latest adds the intrinsic yield of yield-bearing collateral (the staking APR baked into wstETH, the savings rate inside sDAI), so a "4.1% supply APR" on a token that itself earns 3% shows up as the ~7% it really is. That distinction is the difference between a leaderboard that's right and one that quietly misranks every LST.
Let the API do the ranking. /v1/data/lending/pairs/optimize is a purpose-built collateral⇄debt optimizer. You hand it filters - asset groups, a USD size, minimum deposit APR including intrinsic yield, max borrow rate, liquidity floors, a maximum risk score - and it returns ranked rows, optionally computing the opposite-side amount for each. "Best place to park $50k of stablecoins at risk score 3 or better, with at least $1M of liquidity" is one request, not a spreadsheet.
Reach the ERC-4626 universe. Beyond lending pools, /v1/data/vaults returns vault data across supported ERC-4626 providers on a chain in one round-trip, and /v1/data/vaults/user reads a wallet's vault balances with a single balanceOf per vault. To move funds in, /v1/actions/vaults/deposit and /v1/actions/vaults/withdraw build the calldata - routing directly to vault.deposit when nothing fancy is needed, and falling back to the composer when it has to wrap native ETH or compose a swap first.
When optimizing means moving an existing position rather than depositing fresh capital, reach for /v1/actions/allocate (a POST). It bundles several lending and token operations - Deposit, Withdraw, Borrow, wrap/unwrap - into one composer transaction, so a "withdraw from the lagging market, deposit into the winner" rebalance is atomic and costs the user a single signature instead of four. That's the same primitive Decimal used to ship "park idle stablecoins at the best rate" as a few no-code blocks.
4. Leverage
Leverage is where the composer earns its keep, and it's the reason 1delta exists. Opening a leveraged position by hand is the three-step dance from the intro: deposit collateral, borrow against it, swap the borrowed asset back into more collateral - and repeat to reach your target multiple. Done as separate transactions it's slow, gas-heavy, and unsafe between steps. /v1/actions/loop/leverage collapses the whole loop into one flash-loan-backed, atomic transaction.
You describe the position with two market UIDs - marketUidIn is the debt side, marketUidOut is the collateral side - plus how much to borrow and your slippage:
GET /v1/actions/loop/leverage
?marketUidIn=AAVE_V3:42161:0x...USDC # borrow USDC
&marketUidOut=AAVE_V3:42161:0x...WETH # long WETH
&debtAmount=3000000000 # borrow 3,000 USDC
&leverage=3 # target 3x
&slippage=50
&payAsset=0xaf88...USDC # optional zap-in collateral
&payAmount=1000000000
&account=0xUser...
Omit account and you get data.quotes - candidate routes with their price deltas - to preview the entry price and resulting health factor. Include it and the API returns the full ActionSet: the flash loan, the best swap route in alternatives, the collateral-and-debt composer call in transactions, and any approval/delegation in permissions. One signature opens the whole position; if any leg would breach your slippage or leave the position unsafe, the bundle reverts whole.
The same loop family covers the rest of a position's life cycle, all with the identical marketUidIn / marketUidOut + account contract:
To populate the picker in front of all this, the data layer has a matching pair: /v1/data/lending/pairs/leverage browses every viable (collateral, debt) leverage pair with its rates, max leverage, and liquidity, and /v1/data/loop/range/leverage gives the safe leverage range for a chosen pair. So the full leverage flow - discover pairs → preview a target → open → manage → close - is one data endpoint feeding one actions endpoint, the same shape you've now seen four times.
The shape of the whole app
Step back and the four products are the same two-step rhythm with different nouns:
| Product | Read (decide) | Write (execute) |
|---|---|---|
| Lending | /data/lending/pools · /user-positions | /actions/lending/{deposit,withdraw,borrow,repay} |
| Swaps | quote via swap/spot (no account) | swap/spot with account → alternatives[0] |
| Earn | /yields/* · /pairs/optimize · /data/vaults | /actions/vaults/deposit · /actions/allocate |
| Leverage | /pairs/leverage · /loop/range/* · quote | /actions/loop/{leverage,close,…} |
Your application code converges on a single function: take an intent, call one actions endpoint without account to render a quote, then call it again with account to get an ActionSet, send the permissions then the transactions. Swap the endpoint string and the same function ships a lending app, a swap widget, an earn vault, or a leverage terminal. The fragmentation the data articles normalized away on the read side is normalized away on the write side too - you never encode an Aave call by hand, never wire a DEX aggregator, never write the flash-loan choreography for a loop.
That's the whole pitch for building on the API rather than against the protocols: the aggregation gets you comparable data, and the actions layer gets you comparable execution. Pick the product, reuse the contract.
Where to go next
- API reference - every endpoint above with full request/response schemas. Start with
GET /v1/actions/lending/depositand a quote-only call. - Get an API key - mint one, then drop the throttle by sending
x-api-key. - Docs site - supported protocols, network coverage, the Composer, and the rest of this series on the data layer underneath all of this.