One deposit/withdraw entry point over every earn row
GET/v1/actions/earn/:action
One action entry point for every /v1/data/earn row. The earnUid a listing row carries is the only identity needed — this route parses it, decides which existing handler owns the row (lending vs vault, and which vault family), rewrites the query into that handler's vocabulary and calls it in-process. Nothing is built here and no route is duplicated: if a market can be transacted through /v1/actions/lending/* or /v1/actions/vaults/*, it can be transacted through here.
Routing
AAVE_V3:1:0xa0b8… → /v1/actions/lending/{deposit,withdraw}
vault.savings:1:0x9d39… → /v1/actions/vaults/{deposit,withdraw}
→ /v1/actions/vaults/savings (async verbs)
vault.lst:… (async) → /v1/actions/vaults/lst (queued LST exits)
vault.lagoon:… (async) → /v1/actions/vaults/withdraw (ERC-7540 requestRedeem / claimRedeem)
vault.gmx:… → /v1/actions/vaults/gmx (keeper tickets; no claim — the keeper pays out)
vault.hypercore:… → /v1/actions/vaults/{deposit,withdraw} with interface=hypercore
vault.pendle:… → traded on Pendle's AMM (slippage REQUIRED, bps)
Pay-asset conversion (vault rows)
A deposit whose payAsset differs from the vault's underlying is served HERE and nowhere else: the pay asset is quoted through the aggregators and the whole swap output is deposited in one composed transaction, shares to receiver. slippage (bps) becomes REQUIRED, and the built transactions come back as alternatives[] — one per aggregator, best output first. Only rows whose deposit is a plain synchronous ERC-4626 call qualify (that is what acceptsPayAsset on the row's capability advertises); paying native into a wrapped-native vault stays a plain wrap with no bound. /v1/actions/vaults/deposit deliberately does NOT gain this behaviour.
Verbs
deposit/withdraw— the synchronous pair. On a lending market both are same-block; on a vault,withdrawis only published where the exit really is synchronous.request-withdraw/claim/cancel— the async exit cycle, vaults only. Asking for one on a lending market is a 400, never a silent fallback towithdraw.
Which verbs a row supports — and which extra params each needs — is published on the row itself as capabilities[] (see /v1/data/earn). Render the CTA from that, not from provider knowledge: requires names params like slippage (Pendle, bps), executionFee/longToken/shortToken (GMX), or validator (delegated LSTs).
Parameter forwarding
Only earnUid and the verb are consumed. Everything else — payAsset, receiveAsset, slippage, isShares, isAll, mode, Yield Basis's debt/minShares, Strata's claimToken, Apyx's tokenId, an LST's kind, GMX's executionFee, an ERC-7540 sub-action (action=claimWithdraw), … — rides through verbatim to the owning handler, so provider-specific inputs need no support here.
Response is the standard action envelope: permissions[] (approvals, executed first), transactions[] (signed in order), alternatives[] where a trade offers multiple routes, plus per-venue state (e.g. dbr on Inverse) in data.
Plain-text reference — GET /v1/actions/earn/{action}
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
action | path | deposit, withdraw, request-withdraw, claim, cancel | yes | The verb. May also be passed as ?action= on the bare /v1/actions/earn path. |
earnUid | query | string | yes | The row identity from /v1/data/earn, verbatim and OPAQUE — never rebuild it from parts. Lending rows: the marketUid (AAVE_V3:1:0xa0b8…). Vault rows: vault.<provider>:<chainId>:<shareToken>. |
operator | query | string | yes | User wallet executing the action. receiver defaults to it. |
amount | query | string | no | Raw amount (wei). Required for deposit / withdraw / request-withdraw unless isAll=true. Denomination follows the owning route: assets by default, shares when isShares=true, PT units for Pendle. |
receiver | query | string | no | Recipient. Defaults to operator. |
payAsset | query | string | no | deposit — what the user actually pays, where the row publishes acceptsPayAsset. Zero address / 0xEEEE… = native. On a vault row a payAsset that differs from the underlying triggers the aggregator conversion (see above); amount is then denominated in the PAY asset and slippage is required. |
receiveAsset | query | string | no | withdraw — what the user wants out, where the row publishes acceptsReceiveAsset. |
slippage | query | string | no | Basis points. Required wherever the row’s capability lists it in requires (any leg that settles on a book — Pendle PTs in both directions). |
isShares | query | boolean | no | Interpret amount as shares (mint / redeem) instead of assets. |
isAll | query | boolean | no | Full-balance exit (vault withdraw). Not supported on swap-routed rows (Pendle) — pass the explicit amount there. |
simulate | query | boolean | no | Lending rows only — attach a post-trade simulation. |
Response 200
| Field | Type | Description |
|---|---|---|
success | True | |
data | object | Informational data (quotes, simulation results, etc.) |
actions | object | Transaction calldata and approvals. Null for quote-only responses (no account provided). |
actions.transactions | object[] | Pre-trade setup transactions (e.g. e-mode switch, collateral enable). Execute these before the main swap. Empty when no setup is needed. |
actions.transactions[].to | string | Target contract address |
actions.transactions[].data | string | Encoded calldata |
actions.transactions[].value | string | ETH value to send with the transaction |
actions.transactions[].description | string | Human-readable label. For alternatives, this is the aggregator name (e.g. "Paraswap"). For transactions, describes the setup action (e.g. "Switch e-mode to 1"). |
actions.alternatives | object[] | DEX aggregator swap transactions sorted by best output (descending). Each entry's description is the aggregator name. The client should pick one to execute. Present on loop action endpoints. |
actions.alternatives[].to | string | Target contract address |
actions.alternatives[].data | string | Encoded calldata |
actions.alternatives[].value | string | ETH value to send with the transaction |
actions.alternatives[].description | string | Human-readable label. For alternatives, this is the aggregator name (e.g. "Paraswap"). For transactions, describes the setup action (e.g. "Switch e-mode to 1"). |
actions.permissions | object[] | Approval/delegation transactions that must execute before both transactions and alternatives. Includes ERC20 allowances (targeting the composer contract) and lender borrow/withdrawal delegations (targeting the lending protocol contract directly). Filtered against on-chain state so only missing approvals are returned. Null when no approvals are needed. |
actions.permissions[].to | string | Target contract address |
actions.permissions[].data | string | Encoded calldata |
actions.permissions[].value | string | ETH value |
actions.permissions[].description | string | Human-readable description of the approval (e.g. "Approve borrow for AAVE_V3", "Approve ERC20") |
actions.permissions[].spender | string | ERC-20 approve spender (x-chain permissions). Match it against the selected quote's approvalTarget — several bridges can share one spender, so do not match by description. |
Example response
{
"success": true,
"data": {},
"actions": {
"transactions": [
{
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string"
}
],
"alternatives": [
{
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string"
}
],
"permissions": [
{
"to": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string",
"spender": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
}
]
}
}
Request
Responses
- 200
- 400
- 429
- 500
- 502
Action built by the owning handler. permissions[] first, then transactions[] in order.
Validation error
Rate limited. Unauthenticated callers share a per-IP budget; send an x-api-key header to lift it. Retry with exponential backoff.
Unexpected server error. Safe to retry with backoff.
An upstream data source or protocol origin failed (error.code is ORIGIN_FAILED). error.details carries the per-origin status. This is also what a missing or malformed required parameter currently returns, rather than a 400.