Skip to main content
This flow bridges ERC-20 tokens through Layerswap using a Privy server wallet. It creates a swap with use_depository: true, batches the token approval with the bridge call, and submits everything through Privy’s gas sponsorship — so the wallet doesn’t need native tokens for gas. See the full example on GitHub.

Prerequisites

Testnets are supported — use a testnet API key from the dashboard and get testnet funds from the Circle faucet.

Integration

1

Create a Layerswap swap

Create the swap with use_depository: true for the Privy wallet address:
curl --request POST 'https://api.layerswap.io/api/v2/swaps' \
  --header 'Content-Type: application/json' \
  --header 'X-LS-APIKEY: <LAYERSWAP_API_KEY>' \
  --data '{
    "source_network": "ETHEREUM_SEPOLIA",
    "source_token": "USDC",
    "destination_network": "ARC_TESTNET",
    "destination_token": "USDC",
    "destination_address": "<WALLET_ADDRESS>",
    "amount": 1,
    "use_depository": true
  }'
use_depository: true is recommended when using Layerswap from a contract or a server wallet.
The response includes deposit_actions with everything needed for the on-chain calls:
{
  "data": {
    "swap": {
      "id": "...",
      "status": "user_transfer_pending",
      "requested_amount": 1,
      "source_token": { "symbol": "USDC", "contract": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", "decimals": 6 }
    },
    "deposit_actions": [
      {
        "order": 0,
        "type": "transfer",
        "to_address": "<depository_contract>",
        "call_data": "<encoded_call>",
        "amount_in_base_units": "0",
        "token": { "symbol": "USDC", "contract": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", "decimals": 6 },
        "encoded_args": ["<swap_id>", "<token>", "<receiver>", "0x<amount>"]
      }
    ],
    "quote": { "..." : "..." }
  }
}
2

Check allowance and build the call batch

For ERC-20 transfers, the wallet needs to approve the depository contract before the bridge call:
import { encodeFunctionData, erc20Abi, parseUnits, toHex } from "viem";

const depositAction = preparedSwap.deposit_actions.find(
  (a) => a.type.toLowerCase().includes("transfer"),
);

const requiredAmount = parseUnits(
  String(preparedSwap.swap.requested_amount),
  preparedSwap.swap.source_token.decimals,
);

const allowance = await publicClient.readContract({
  address: depositAction.token.contract,
  abi: erc20Abi,
  functionName: "allowance",
  args: [walletAddress, depositAction.to_address],
});

const bridgeCall = {
  to: depositAction.to_address,
  data: depositAction.call_data,
  value: toHex(BigInt(depositAction.amount_in_base_units)),
};

const calls =
  allowance < requiredAmount
    ? [
        {
          to: depositAction.token.contract,
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: "approve",
            args: [depositAction.to_address, requiredAmount],
          }),
          value: "0x0",
        },
        bridgeCall,
      ]
    : [bridgeCall];
If allowance is already sufficient the approve call is skipped. Otherwise both calls are batched into a single sponsored wallet_sendCalls request.
3

Submit the sponsored transaction

Submit the batched calls with Privy wallet_sendCalls and sponsor: true:
const sendCallsResponse = await privyClient.wallets().rpc(walletId, {
  method: "wallet_sendCalls",
  chain_type: "ethereum",
  caip2: "eip155:11155111",
  sponsor: true,
  params: {
    calls,
  },
});
Privy returns a transaction_id — poll privyClient.transactions().get(transactionId) until transaction_hash is available, then use it in the next step.
4

Check the swap status

After Privy returns the final chain transaction hash, look it up through Layerswap:
curl \
  --header 'X-LS-APIKEY: <LAYERSWAP_API_KEY>' \
  "https://api.layerswap.io/api/v2/swaps/by_transaction_hash/<OUTER_TRANSACTION_HASH>"
This maps the final transaction back to the Layerswap swap record. See the Swap Lifecycle for all possible statuses.