LayerHopDocsLaunch app (opens in a new tab)

Deposit addresses

Let users swap or bridge by sending tokens to an address, with no wallet connection or signing in your app. Each deposit becomes an order with the usual webhooks; the address belongs to the liquidity provider, so LayerHop never holds funds.

A deposit address lets your user swap or bridge just by sending tokens to an address: no wallet connection, no signature in your app. You create an address for a route (for example USDC on Base → USDC on Arbitrum, delivered to your user's address), show it to your user, and every deposit to it is swapped or bridged and delivered to the recipient. It fits exchanges, games, payroll, and wallets with a "deposit from any chain" button.

Deposit addresses are non-custodial. The address is created by the liquidity provider (Relay), which picks up each deposit and delivers dstToken on dstChain. LayerHop never holds the funds; it creates the address, detects the deposits, records each one as an order and sends you webhooks.

Deposit addresses work with live (lh_live_) and test (lh_test_) keys. Build and check your integration in test mode first: test addresses are fake and deposits are simulated.

How it works#

  1. Create an address with an API key: the source chain and token, the destination chain and token, the recipient, and a refundTo address on the source chain.
  2. Show address to your user, with the token and chain to send (token on chainId), for example as text and a QR code.
  3. Your user sends srcToken on srcChain to the address from any wallet or exchange.
  4. LayerHop detects the deposit and records it as an order (quoteId starting with dep_, depositAddressId set). You get order.submitted, then order.filled (or order.refunded / order.failed) when it settles.

1. Create a deposit address#

Terminal
curl -X POST https://api.layerhop.com/v1/deposit-addresses \
  -H "x-api-key: $LAYERHOP_API_KEY" -H "content-type: application/json" \
  -d '{
    "srcChain": "base",
    "srcToken": "USDC",
    "dstChain": "arbitrum",
    "dstToken": "USDC",
    "recipient": "0xUserAddressOnArbitrum",
    "refundTo": "0xUserAddressOnBase",
    "label": "user-4821"
  }'

With the TypeScript SDK:

TypeScript
import { LayerHop } from "@layerhop/sdk";

const lh = new LayerHop({ apiKey: process.env.LAYERHOP_API_KEY });
const da = await lh.depositAddresses.create({
  srcChain: "base",
  srcToken: "USDC",
  dstChain: "arbitrum",
  dstToken: "USDC",
  recipient: "0xUserAddressOnArbitrum",
  refundTo: "0xUserAddressOnBase",
  label: "user-4821",
});
console.log(da.address);              // show this to your user: send USDC on Base here
console.log(da.estimate.dstAmount);   // what 100 USDC would deliver, in base units
Field Notes
srcChain, srcToken Where your user deposits, and the only token to send. Chains and tokens are given as for quotes (id or slug; address, symbol or native symbol).
dstChain, dstToken, recipient What is delivered, where, and to whom.
refundTo Your user's address on the source chain. Refunds (an underpaid one-time address, a deposit that cannot be routed) go here. Never use an exchange (CEX) deposit address.
reusable true (default): the address can be used again and again for the same route, with any amount; each deposit is a separate order. false: a one-time address for exactly amount.
amount Base units of srcToken. Required for a one-time address. For a reusable one it only sizes the estimate (default about $100).
label, metadata Your reference (up to 64 characters, filterable in the list) and up to 20 string key-value pairs.
integrator, integratorFeeBps Your partner fee, with the same rules as for quotes.

The response (201) is the deposit address: id (da_…), livemode, status (active), provider (relay, or sandbox in test mode), address, chainId and token (what to send where), dstChainId, dstToken, recipient, refundTo, reusable, amount (one-time addresses only), fees (ourFeeBps, integratorFeeBps, integrator), label, metadata, depositCount, lastDepositAt and createdAt, plus an estimate: { amount, dstAmount, dstAmountMin, estimatedSeconds }. The estimate is a guide only: each deposit is priced when it arrives.

Creating addresses uses the stricter rate-limit bucket. A route the provider cannot serve returns UNSUPPORTED_CHAIN.

2. Tell your user what to send#

Show the address together with the exact token and chain, and these rules:

  • Send only srcToken on srcChain. Other tokens, or the same token on another chain, sent to this address may be lost for good. Say it clearly next to the address.
  • Send at least $1 worth. Smaller deposits may never be picked up (the provider ignores deposits worth $0.05 or less). Creating an address with a smaller amount returns AMOUNT_TOO_SMALL.
  • One-time addresses take exactly amount, once. An underpayment is refunded to refundTo; do not reuse the address afterwards.
  • Withdrawing from an exchange works, but refunds always go to refundTo, never back to the exchange. That is why refundTo must be an address your user controls.

3. Track deposits#

Every deposit LayerHop detects becomes an order with quoteId starting with dep_ and depositAddressId set to the address id. Subscribe a webhook endpoint to the order events:

Event Sent when
order.submitted A deposit is seen and is being processed.
order.filled dstToken was delivered to recipient.
order.refunded The deposit was refunded to refundTo.
order.failed The deposit could not be processed.
JSON
{
  "id": "evt_…",
  "type": "order.filled",
  "livemode": true,
  "createdAt": "2026-10-19T10:04:12.000Z",
  "data": {
    "order": {
      "quoteId": "dep_0x5f2c…",
      "status": "filled",
      "provider": "relay",
      "srcChainId": 8453,
      "dstChainId": 42161,
      "amount": "25000000",
      "dstAmount": "24940000",
      "depositAddressId": "da_01k7x2m4c4x8q6r0v3b5n7p9d",
      "srcTxHash": "0x…",
      "dstTxHash": "0x…"
    }
  }
}

Match the event to your user with depositAddressId (and your label or metadata on the address). The order also shows up in GET /v1/dashboard/orders, and GET /v1/deposit-addresses/{id} returns the address with its 20 most recent deposits in deposits.

How fast deposits are detected. LayerHop checks an address about every 15 seconds while a deposit is in flight or it was recently active, then backs off to at most every 15 minutes when it is idle. When your user says "I sent it", call refresh to check right away:

TypeScript
await lh.depositAddresses.refresh(da.id); // POST /v1/deposit-addresses/{id}/refresh → 202

4. Manage addresses#

Action API SDK
List (newest first, ?status=active|disabled, label, limit, cursor) GET /v1/deposit-addresses lh.depositAddresses.list({ status, label })
Get one, with its 20 most recent deposits GET /v1/deposit-addresses/{id} lh.depositAddresses.get(id)
Check for deposits now POST /v1/deposit-addresses/{id}/refresh lh.depositAddresses.refresh(id)
Disable POST /v1/deposit-addresses/{id}/disable lh.depositAddresses.disable(id)
Simulate a deposit (test mode) POST /v1/deposit-addresses/{id}/simulate lh.depositAddresses.simulate(id, { amount })

Each address belongs to the account and mode (live or test) of the key that created it; other accounts' addresses return NOT_FOUND. Disabling stops LayerHop tracking the address (status: disabled); refreshing or simulating it then returns CONFLICT. Stop showing a disabled address to your user: the provider may still process funds sent to it later, but you will not get orders or webhooks for them.

From the CLI or an AI agent#

The CLI covers every endpoint with layerhop deposit-address (API key in --api-key or LAYERHOP_API_KEY). Amounts are in human units unless you pass --base-units; add --json for the raw API response.

Terminal
export LAYERHOP_API_KEY=lh_test_…
layerhop deposit-address create USDC base USDC arbitrum --recipient 0xUserAddressOnArbitrum --refund-to 0xUserAddressOnBase --label user-4821
layerhop deposit-address simulate da_… 25        # test keys only
layerhop deposit-address get da_…                # the address and its recent deposits
layerhop deposit-address list --status active    # also: refresh <id>, disable <id>

The MCP server has the same operations as tools: create_deposit_address, list_deposit_addresses, get_deposit_address, refresh_deposit_address, disable_deposit_address and simulate_deposit (test keys only). They need an API key (LAYERHOP_API_KEY locally, the x-layerhop-api-key header on the hosted server), and every response includes the rules the agent must pass on to the user: send only srcToken on srcChain, at least $1 worth, with a refundTo that is never an exchange deposit address.

Manage them in the dashboard#

The Deposit addresses page of the dashboard lists every address of the current mode (live or test), whether it was created with an API key or in the dashboard, with its route, label, deposit count and last deposit. Open one to see its QR code, its 20 most recent deposits (status, amounts and transaction links) and its details. From there you can:

  • Create an address with the same fields as the API (source token, destination token, recipient, refundTo, reusable or one-time, label). Addresses created in the dashboard belong to your account and have no API key.
  • Check for deposits now (the same as refresh) and disable an address.
  • Simulate a deposit in test mode, with the same magic amounts as the API.

The dashboard uses the same rules as the API: live mode needs deposit addresses enabled for your account, and each address belongs to one account and mode.

Fees#

A deposit pays the same fees as any API swap: LayerHop's API channel fee (20 bps by default, see Fees & channels) plus your partner fee if you set one. Both are fixed when you create the address and shown in fees; each deposit's order records the fee actually applied (ourFeeBps, partnerFeeBps), and your partner share counts towards your earnings and payouts like any other order.

Test mode#

With an lh_test_ key, deposit addresses are served by the sandbox: provider is sandbox and address is a fake address in the source chain's format. Never send real funds to it. Record a deposit instead:

Terminal
curl -X POST https://api.layerhop.com/v1/deposit-addresses/da_…/simulate \
  -H "x-api-key: $LAYERHOP_TEST_KEY" -H "content-type: application/json" \
  -d '{ "amount": "25000000" }'
TypeScript
const order = await lh.depositAddresses.simulate(da.id, { amount: "25000000" });
// order.quoteId starts with dep_, order.depositAddressId === da.id, status "submitted"

The deposit becomes a submitted test order that the worker settles like any test order, with webhooks to your test endpoints. The same magic amounts choose the outcome:

amount ends in Result
4001 The deposit is refunded (order.refunded)
4002 The deposit fails (order.failed)
anything else The deposit is filled (order.filled)

A one-time test address accepts a single simulated deposit of exactly its amount. simulate on a live address returns FORBIDDEN.

Errors#

Code When
INVALID_PARAMS A missing or malformed field, an address in the wrong format for its chain, a one-time address without amount, or a simulated amount that does not match a one-time address.
AMOUNT_TOO_SMALL amount is worth less than $1.
UNSUPPORTED_CHAIN The chain is unknown, or deposit addresses are not available for the route.
UNAUTHORIZED No API key.
FORBIDDEN A live key before live mode is enabled for you, or simulate on a live address.
NOT_FOUND The address does not exist or belongs to another account or mode.
CONFLICT The address is disabled, or a one-time address was already used.

See Error handling for retries and the API reference for every field.

Last updated