# Local development & CI

> Build and test a LayerHop integration offline with layerhop dev, a local mock API with deterministic outcomes and signed webhooks, optional Anvil forks, the setup-layerhop GitHub Action and a Postman collection.

Source: https://docs.layerhop.com/guides/local-development

`layerhop dev` runs a mock LayerHop API on your machine, so you can build and test an integration with no account, no API key, no network and no funds. It answers the core routes with the same JSON shapes as `https://api.layerhop.com`, which means the SDKs, the CLI and your own HTTP code work against it unchanged. In CI, the `setup-layerhop` GitHub Action starts it for you. For a hosted sandbox with your dashboard, webhooks endpoints and logs, use [test mode](/guides/test-mode) instead.

## How do I start the mock API?

```bash
npx @layerhop/cli dev
# LayerHop mock API on http://127.0.0.1:8787
#   export LAYERHOP_API_URL=http://127.0.0.1:8787
```

Point your code at it:

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

const lh = new LayerHop({ baseUrl: process.env.LAYERHOP_API_URL ?? "https://api.layerhop.com" });
const { best } = await lh.quote({
  srcChain: "base", srcToken: "USDC", amount: "100000000",
  dstChain: "solana", dstToken: "SOL",
  sender: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  recipient: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
});
const tx = await lh.buildTx(best.quoteId);          // a placeholder: never broadcast it
await lh.reportSubmitted(best.quoteId, `0x${"ab".repeat(32)}`);
for await (const s of lh.watch("mock", `0x${"ab".repeat(32)}`, { intervalMs: 500 })) console.log(s.state);
```

The Python, Go and Rust SDKs take the same base URL, and every CLI command honours `LAYERHOP_API_URL`. The server allows any origin (CORS), so a local front end can call it from the browser.

| Option | Default | What it does |
|---|---|---|
| `--port <n>` | `8787` | Port to listen on (env `LAYERHOP_DEV_PORT`). |
| `--host <addr>` | `127.0.0.1` | Interface to bind. Use `0.0.0.0` inside Docker. |
| `--fill-after <s>` | `5` | Seconds between reporting a transaction and its final state. |
| `--webhook-url <url>` | | POST signed `order.*` webhooks to this URL. |
| `--secret <whsec_…>` | random | Webhook signing secret (env `LAYERHOP_WEBHOOK_SECRET`). Printed at start when generated. |
| `--fork`, `--fork-chains <a,b>` | `base,arbitrum` | Also start Anvil forks (below). |
| `--json` | | Print one JSON line (`url`, `webhookSecret`, `forks`) instead of text, and no request log. |

## What does the mock API cover?

| Route | Behaviour |
|---|---|
| `GET /v1/health`, `GET /v1/status` | Always healthy; `providers: ["mock"]`. |
| `GET /v1/chains`, `GET /v1/tokens`, `GET /v1/prices` | The built-in chain and token lists, with fixed prices (USDC/USDT $1, ETH $3,000, SOL $150, BNB $600; others $1). |
| `GET /v1/quote` | One `mock` quote, priced at the fixed prices minus the LayerHop fee of the `channel` (20 bps on `api`). Quote ids are sequential (`q_mock_1`, …) and valid for 60 seconds. |
| `POST /v1/orders`, `POST /v1/simulate` | A placeholder transaction (`sandbox: true`). EVM: a call to `0x…dEaD` with an `approval` for ERC-20 inputs. Solana: a memo transaction with an all-zero blockhash that no cluster accepts. |
| `POST /v1/orders/{quoteId}/submitted`, `POST /v1/orders/submit` | Records the order (any well-formed hash) and starts its lifecycle. |
| `GET /v1/orders/{provider}/{id}`, `GET /v1/orders` | `pending` until `--fill-after` has passed, then the final state, with a `dstTxHash` when filled. |
| `POST /v1/webhooks/trigger` | Sends a sample event of any type to `--webhook-url`. |

Anything else (checkout, invoices, subscriptions, deposit addresses, limit orders, the dashboard) returns `NOT_FOUND` with a pointer to test mode, which covers every endpoint. Errors use the real shape, `{ "error": { code, message, requestId, docsUrl } }`.

## How do I test refunds and failures?

The last digits of `amount` pick the outcome, exactly like [test mode's magic amounts](/guides/test-mode#magic-amounts):

| `amount` ends in | Outcome |
|---|---|
| `4001` | The order is `refunded`. |
| `4002` | The order is `failed`, and `POST /v1/simulate` returns `ok: false`. |
| `4004` | `GET /v1/quote` returns `NO_ROUTE` (404). |
| anything else | The order is `filled`. |

## How do I receive webhooks locally?

```bash
npx @layerhop/cli dev --webhook-url http://localhost:3000/api/layerhop-webhooks --secret whsec_dev_only_secret
```

Each reported order sends `order.submitted` and then `order.filled`, `order.refunded` or `order.failed`, signed with the `LayerHop-Signature` header exactly like real deliveries, so your verification code runs unchanged (see [Webhooks](/guides/webhooks)). `POST /v1/webhooks/trigger` with `{"type":"order.filled"}` sends a sample on demand.

## How do I use it in GitHub Actions?

The `layerhop/setup-layerhop` action starts the mock API in the background, waits until it answers, and exports `LAYERHOP_API_URL` (and `LAYERHOP_WEBHOOK_SECRET` when you pass a webhook URL) for the following steps:

```yaml
name: Integration tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - uses: layerhop/setup-layerhop@v1
        with:
          fill-after: "1"                                    # orders finish in 1 second
          webhook-url: http://localhost:3000/api/layerhop-webhooks   # optional
      - run: npm ci
      - run: npm test   # reads process.env.LAYERHOP_API_URL
```

`layerhop dev` ships in the first `@layerhop/cli` release after 0.6.0, and the action is published as `layerhop/setup-layerhop` with it.

Inputs: `port` (8787), `cli-version` (`latest`), `fill-after` (1), `webhook-url`, `webhook-secret` (random when empty), `fork` and `fork-chains`, `timeout` (60). Outputs: `api-url`, `webhook-secret`. Without the action, any CI can run `npx @layerhop/cli dev --port 8787 &` and poll `/v1/health`.

## Can I test against forked mainnet state?

Yes, for EVM chains. With [Foundry](https://book.getfoundry.sh) installed (`curl -L https://foundry.paradigm.xyz | bash && foundryup`), `--fork` starts one Anvil fork per chain next to the mock API:

```bash
npx @layerhop/cli dev --fork --fork-chains base,arbitrum
# Anvil fork of base (chain id 8453) on http://127.0.0.1:8545
# Anvil fork of arbitrum (chain id 42161) on http://127.0.0.1:8546
```

Each fork keeps the real chain id and copies mainnet state on demand, and Anvil gives you prefunded test accounts. Use the forks to exercise wallet connection, signing and token-approval code against real token contracts without spending anything. `GET /v1/health` lists the running forks. The mock API's transactions stay placeholders: forks don't make provider routes executable locally, because the bridges and solvers behind them only run on mainnet.

Forks read state through public RPCs (rate limited). Set your own per chain with `LAYERHOP_FORK_RPC_<CHAIN>`, for example `LAYERHOP_FORK_RPC_BASE=https://base-mainnet.example/your-key`. Without Anvil, `--fork` prints how to install it and starts the mock API alone.

## Can I import the API into Postman, Insomnia or Bruno?

Yes. The [Postman collection](/layerhop.postman_collection.json) is generated from the OpenAPI spec with a folder per API area, example requests and example responses. In Postman choose **Import → Link** and paste `https://docs.layerhop.com/layerhop.postman_collection.json`; Insomnia and Bruno import the same file. Set the collection variables `apiKey` (an `lh_test_` key) and `baseUrl` (`https://api.layerhop.com`, or `http://127.0.0.1:8787` for `layerhop dev`). Code generators can use the spec itself: [openapi.yaml](/openapi.yaml) or [openapi.json](/openapi.json).

## What's not included yet?

- **A local Solana validator.** `--fork` covers EVM chains only. Solana flows run against the mock API's placeholder transactions or [test mode](/guides/test-mode).
- **A testnet faucet.** LayerHop's providers are mainnet-only, so there is no LayerHop testnet to fund. A faucet would also need wallets funded by LayerHop, which is an owner decision that hasn't been made. Use test mode or `layerhop dev` instead: neither needs funds.
