# 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.

Source: https://docs.layerhop.com/guides/deposit-addresses

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](/guides/webhooks).

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

## How it works

1. **Create** an address with an [API key](/guides/dashboard-and-api-keys): 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

```bash
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:

```ts
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](/api-reference#get-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](/guides/partner-fees), 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`](/errors/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`](/errors/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](/guides/webhooks) 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:

```ts
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`](/errors/NOT_FOUND). **Disabling** stops LayerHop tracking the address (`status: disabled`); refreshing or simulating it then returns [`CONFLICT`](/errors/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](/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.

```bash
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](/guides/ai-agents#deposit-address-tools-api-key-required) 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](/guides/dashboard-and-api-keys) 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](/concepts/fees-and-channels)) plus your [partner fee](/guides/partner-fees) 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](/guides/test-mode): `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:

```bash
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" }'
```

```ts
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](/guides/test-mode#order-lifecycle-and-webhooks), with webhooks to your test endpoints. The same [magic amounts](/guides/test-mode#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/FORBIDDEN).

## Errors

| Code | When |
|---|---|
| [`INVALID_PARAMS`](/errors/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`](/errors/AMOUNT_TOO_SMALL) | `amount` is worth less than $1. |
| [`UNSUPPORTED_CHAIN`](/errors/UNSUPPORTED_CHAIN) | The chain is unknown, or deposit addresses are not available for the route. |
| [`UNAUTHORIZED`](/errors/UNAUTHORIZED) | No API key. |
| [`FORBIDDEN`](/errors/FORBIDDEN) | A live key before live mode is enabled for you, or `simulate` on a live address. |
| [`NOT_FOUND`](/errors/NOT_FOUND) | The address does not exist or belongs to another account or mode. |
| [`CONFLICT`](/errors/CONFLICT) | The address is disabled, or a one-time address was already used. |

See [Error handling](/guides/error-handling) for retries and the [API reference](/api-reference#create-a-deposit-address) for every field.
