# Destination calls

> Bridge and then do something on the destination chain in one user action, such as supplying to Aave, buying an item or topping up a game contract. Calls are simulated before you get a quote.

Source: https://docs.layerhop.com/guides/destination-calls

**Destination calls** let a quote include contract calls that run on the destination chain with the bridged output, in the same user action. The user signs once on the source chain. The provider delivers the output to its executor contract, which makes your calls in order. Typical uses: bridge USDC from Base and supply it to Aave on Arbitrum for the user, buy an NFT, or top up a game or subscription contract.

Send the calls with [`POST /v1/quote`](/api-reference#get-quotes-json-body-destination-calls). Either pass raw `calls`, or pass an `action` preset that LayerHop encodes for you.

## How it works

1. You ask for an **exact output**: `dstAmount` is the amount of `dstToken` your calls receive. The quote reports `srcAmount`, which is what the sender pays, fees included.
2. Only providers that run destination calls are asked. Today that is **Relay**; the other providers are skipped for these requests.
3. Before LayerHop returns a quote, it **simulates your calls** on the destination chain with `eth_simulateV1` (read-only, public mainnet state). The simulation runs them from the provider's executor after giving it exactly `dstAmount` of the output token. If any call would revert, the request fails with [`CALLS_REVERTED`](/errors/CALLS_REVERTED), and the message names the call and the revert reason. If the simulation can't run, you get `PROVIDER_ERROR` and no quote. LayerHop never returns a calls quote that has not been simulated.
4. The user signs the source transaction as usual (`POST /v1/orders`, approval first for ERC-20 inputs). On the destination chain the executor receives the output and runs the calls.
5. If a call reverts at fill time anyway (for example the state changed after the quote), the provider refunds the **sender**.

Our fee applies as on any other quote (`fees.ourFeeBps`), and partner fees work the same way.

## Trust model: read this first

- **`msg.sender` is the executor, not the user.** Each quote shows the executor in `calls.executor` (for Relay, its ERC20Router `0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f`). A contract that credits `msg.sender` would credit the executor, and the user would lose the funds. Targets must take the beneficiary as a parameter and credit `recipient`, as Aave `supply(asset, amount, onBehalfOf, referralCode)` does.
- **ERC-20 outputs need an approval call.** The executor holds the tokens, so the first call must be `approve(target, dstAmount)` on `dstToken`, followed by the call that spends them.
- **Spend exactly `dstAmount`.** Tokens left in the executor after your calls do not reach anyone you control. The simulation reports any remainder in `calls.simulation.leftover`. It should be `"0"`.
- **Only actions that benefit the user.** Anyone can make the executor call anything, so never design a target that lets the executor withdraw, sell or cancel on someone's behalf.
- **Blocked patterns.** LayerHop rejects calls that move or authorize someone else's assets: `transferFrom`, `safeTransferFrom` and `safeBatchTransferFrom` (ERC-20, ERC-721 and ERC-1155), `permit` (EIP-2612 and DAI), any call to Permit2, and calls to the zero address or precompiles.

## Limits

| Rule | Value |
|---|---|
| Destination | EVM chains only. Not Solana or Hyperliquid. Cross-chain only (`srcChain` ≠ `dstChain`). |
| Calls | 1 to 8, run in order. |
| Calldata | At most 10,000 bytes per call and 24,000 bytes in total. Hex with whole bytes. |
| `value` | Wei, only when `dstToken` is the native coin. The values together must not exceed `dstAmount`. |
| Amount | Send `dstAmount`, not `amount`. |
| Not combinable with | `gasOnArrival` / `gasOnArrivalUsd`, and `calls` together with `action`. |

## Full example: bridge USDC and supply it to Aave

Base USDC to Arbitrum, supplying exactly 10 USDC to Aave v3 for the user, so the aUSDC goes to `recipient`. The `aave-v3-supply` preset builds two calls: `approve(pool, dstAmount)` on `dstToken`, then `supply(dstToken, dstAmount, recipient, 0)` on the chain's Aave v3 Pool. Presets are available on Ethereum, Optimism, BNB Chain, Polygon, Base and Arbitrum. On each of these chains the Pool address was read from Aave's PoolAddressesProvider on chain, and a supply from the executor was simulated against mainnet.

```bash
curl -X POST "https://api.layerhop.com/v1/quote" \
  -H "content-type: application/json" -H "x-api-key: $LAYERHOP_KEY" \
  -d '{
    "srcChain": "base", "srcToken": "USDC",
    "dstChain": "arbitrum", "dstToken": "USDC", "dstAmount": "10000000",
    "sender": "0xYourUser", "recipient": "0xYourUser",
    "action": { "type": "aave-v3-supply" }
  }'
```

The response is a normal quote with `srcAmount` and `calls` added. This one is from a real read-only quote with a 25 bps fee:

```json
{
  "quoteId": "q_713598a2-7493-4cf5-8412-7e44015c9c88",
  "provider": "relay",
  "dstAmount": "10000000",
  "dstAmountMin": "10000000",
  "srcAmount": "10083608",
  "fees": { "ourFeeBps": 25, "providerFeeUsd": 0.0586, "gasUsd": 0.001194 },
  "calls": {
    "items": [
      { "to": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", "data": "0x095ea7b3…", "value": "0" },
      { "to": "0x794a61358d6845594f94dc1db02a252b5b4814ad", "data": "0x617ba037…", "value": "0" }
    ],
    "executor": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f",
    "executed": true,
    "simulation": { "ok": true, "gasUsed": "288491", "leftover": "0" }
  }
}
```

The user pays 10.083608 USDC on Base, and 10 USDC is supplied to Aave on Arbitrum for them. Build and send it like any quote. With the TypeScript SDK:

```ts
import { LayerHop } from "@layerhop/sdk";

const lh = new LayerHop({ apiKey: process.env.LAYERHOP_KEY });
const { best } = await lh.quote({
  srcChain: "base", srcToken: "USDC",
  dstChain: "arbitrum", dstToken: "USDC", dstAmount: "10000000", // exactly 10 USDC into Aave
  sender: user, recipient: user,
  action: { type: "aave-v3-supply" },
});
console.log(`Pay ${best.srcAmount} on Base; calls simulated: ${best.calls?.simulation?.ok}`);

const tx = await lh.buildTx(best.quoteId); // EVM tx, with `approval` first for USDC
// approve tx.approval.spender for tx.approval.amount, then send { to, data, value } from `user`
```

## Raw calls

The same deposit written out by hand with viem shows the pattern for any target: approve the output token, then call the target with `recipient` as the beneficiary.

```ts
import { encodeFunctionData, parseAbi } from "viem";

const USDC = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; // Arbitrum USDC
const POOL = "0x794a61358D6845594F94dc1DB02A252b5b4814aD"; // Aave v3 Pool, Arbitrum
const amount = 10_000_000n;
const abi = parseAbi([
  "function approve(address spender, uint256 amount) returns (bool)",
  "function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)",
]);

const { best } = await lh.quote({
  srcChain: "base", srcToken: "USDC", dstChain: "arbitrum", dstToken: "USDC",
  dstAmount: amount.toString(), sender: user, recipient: user,
  calls: [
    { to: USDC, data: encodeFunctionData({ abi, functionName: "approve", args: [POOL, amount] }) },
    { to: POOL, data: encodeFunctionData({ abi, functionName: "supply", args: [USDC, amount, user, 0] }) },
  ],
});
```

For a native output (for example ETH on Arbitrum), set `value` on the call instead of approving, as in `{ to: game, data: topUp(user), value: dstAmount }`.

If the calls would fail, nothing is quoted:

```json
{
  "error": {
    "code": "CALLS_REVERTED",
    "message": "Destination call 1 (to 0x794a61358d6845594f94dc1db02a252b5b4814ad) would revert: Error(ERC20: transfer amount exceeds balance)"
  }
}
```

## Availability and test mode

Live-mode destination calls are switched on per deployment. Until then, a live request with `calls` or `action` gets `FORBIDDEN`, while test keys always work.

With an `lh_test_` key the quote comes from the sandbox: `srcAmount` is priced from USD prices and the transaction is a placeholder. Your calls are still validated and simulated against real mainnet state, so test mode catches calls that would revert.

## Related

- [Gas on arrival](/guides/gas-on-arrival): deliver native gas instead of making calls.
- [Error handling](/guides/error-handling) and [`CALLS_REVERTED`](/errors/CALLS_REVERTED).
- [API reference](/api-reference#get-quotes-json-body-destination-calls).
