# Quickstart (v1)

> Quote, build, deposit, submit and track one v1 order from a browser page with an EVM wallet.

> API v1 is deprecated. It stays supported, but new integrations should use the [Khalani API](/). See [Migrate to v2](/v1/migrate/).

This page runs one real v1 order: 10 USDC from Arbitrum to Base, from a browser page with an EVM wallet. Most of the time goes to waiting for the order to fill.

:::caution[This uses real funds]
There is no sandbox. The API you call is the production API, and the deposit moves real tokens. Use small amounts while you integrate.
:::

## Before you start

- A browser with an EIP-1193 wallet that injects `window.ethereum`, such as MetaMask.
- 10 USDC on Arbitrum in that wallet, and a little ETH on Arbitrum for gas.
- Node.js 22 or later to run the TypeScript compiler, and Python 3 for the local web server below.
- No API key: v1 does not use one.

## Run it

Save the following as `v1-quickstart.ts` in a new directory. It has no runtime package dependencies, but browsers cannot execute TypeScript directly.

```ts title="v1-quickstart.ts"
// API v1 (deprecated): one order end to end. Quote, build the wallet actions, send them from the
// user's wallet, submit the deposit hash, and track the order until it is final.
// Compile to JavaScript first, then load as an ES module in a browser page with an injected
// EIP-1193 wallet (window.ethereum). See the guide for the compiler command and HTML page.
// v1 takes no API key.
export {};

type Eip1193Provider = {
	request(args: { method: string; params?: readonly unknown[] }): Promise<unknown>;
};

declare global {
	interface Window {
		ethereum?: Eip1193Provider;
	}
}

const API = "https://api.hyperstream.dev";

// 10 USDC on Arbitrum → USDC on Base.
const TRADE = {
	tradeType: "EXACT_INPUT",
	fromChainId: 42161,
	fromToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
	toChainId: 8453,
	toToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
	amount: "10000000", // base units: 10 × 10^6
};

// Stop polling on any of these. refund_pending is not final: it moves on to refunded.
const FINAL_STATUSES = new Set(["filled", "refunded", "failed"]);

type Route = {
	routeId: string;
	type: string;
	depositMethods: string[];
	quote: { amountIn: string; amountOut: string; validBefore: number; quoteExpiresAt: number };
};
type QuoteResponse = { quoteId: string; routes: Route[] };
type WalletAction =
	| {
			type: "eip1193_request";
			request: { method: string; params?: readonly unknown[] };
			waitForReceipt?: boolean;
			deposit?: boolean;
	  }
	| { type: "solana_sendTransaction"; transaction: string; deposit?: boolean };
type DepositPlan =
	| { kind: "CONTRACT_CALL"; approvals?: WalletAction[]; expiresAt?: number; buildId?: string }
	| { kind: "TRANSFER" | "PERMIT2" };
type Order = { id: string; status: string; routeId: string; srcAmount: string; destAmount: string };
type Receipt = { status: string };

const wallet = connectedWallet();
const [from] = (await wallet.request({ method: "eth_requestAccounts" })) as string[];
if (!from) throw new Error("The wallet returned no account");

// 1. Quote. When the native route quotes, it is the only route returned.
const quote = await api<QuoteResponse>("/v1/quotes", {
	method: "POST",
	body: JSON.stringify({ ...TRADE, fromAddress: from }),
});
const route = quote.routes[0];
if (!route) throw new Error("No route serves this pair and amount right now");
if (Date.now() / 1000 >= route.quote.quoteExpiresAt) throw new Error("The quote expired: request a new one");
console.log(`${route.routeId}: you send ${route.quote.amountIn}, you receive ${route.quote.amountOut}`);

// 2. Build the wallet actions for the chosen route, for the wallet that quoted.
const plan = await api<DepositPlan>("/v1/deposit/build", {
	method: "POST",
	body: JSON.stringify({ from, quoteId: quote.quoteId, routeId: route.routeId }),
});
if (plan.kind !== "CONTRACT_CALL") throw new Error(`This example covers wallet deposits only, not ${plan.kind}`);

// 3. Run the actions in order, unchanged: switch chain, approve if needed, deposit. Wait for a
// receipt wherever the plan asks for one, and keep the hash of the action marked deposit.
let depositTxHash: string | undefined;
for (const action of plan.approvals ?? []) {
	if (action.type !== "eip1193_request") throw new Error("Expected EVM wallet actions");
	const result = await wallet.request(action.request);
	if (action.request.method !== "eth_sendTransaction") continue;
	if (typeof result !== "string") throw new Error("The wallet returned no transaction hash");
	if (action.waitForReceipt) await waitForReceipt(result);
	if (action.deposit) depositTxHash = result;
}
if (!depositTxHash) throw new Error("The plan had no deposit action");

// 4. Submit the deposit hash once. A build returns buildId only for a quote made without
// fromAddress; when it does, submit must send it back.
const submitted = await api<{ orderId: string; txHash: string }>("/v1/deposit/submit", {
	method: "PUT",
	body: JSON.stringify({
		txHash: depositTxHash,
		quoteId: quote.quoteId,
		routeId: route.routeId,
		...(plan.buildId ? { buildId: plan.buildId } : {}),
	}),
});
console.log(`Order ${submitted.orderId}`);

// 5. Track until the order is final.
let order = await api<Order>(`/v1/orders/by-id/${submitted.orderId}`);
while (!FINAL_STATUSES.has(order.status)) {
	await sleep(5_000);
	order = await api<Order>(`/v1/orders/by-id/${submitted.orderId}`);
	console.log(order.status);
}
console.log(order);

function connectedWallet(): Eip1193Provider {
	if (!window.ethereum) throw new Error("No EIP-1193 wallet found: window.ethereum is missing");
	return window.ethereum;
}

async function waitForReceipt(hash: string): Promise<void> {
	for (;;) {
		const receipt = (await wallet.request({ method: "eth_getTransactionReceipt", params: [hash] })) as Receipt | null;
		if (receipt) {
			if (receipt.status !== "0x1") throw new Error(`Transaction ${hash} reverted`);
			return;
		}
		await sleep(2_000);
	}
}

// Error bodies vary (JSON exceptions, a rate-limit object, plain text), so read text first.
async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
	const res = await fetch(`${API}${path}`, {
		...init,
		headers: init.body ? { "Content-Type": "application/json" } : {},
	});
	const text = await res.text();
	if (!res.ok) throw new Error(`${res.status} ${path}: ${text} (x-request-id ${res.headers.get("x-request-id")})`);
	return JSON.parse(text) as T;
}

function sleep(ms: number): Promise<void> {
	return new Promise((resolve) => setTimeout(resolve, ms));
}
```

Compile it to a browser ES module:

```bash
npx --yes --package typescript@5.9.3 tsc v1-quickstart.ts \
  --target ES2022 --module ES2022 --lib ES2022,DOM --strict \
  --outDir public --noEmitOnError
```

Save this page as `public/index.html`. The button starts the order and asks the wallet to connect:

```html
<!doctype html>
<html lang="en">
  <meta charset="utf-8">
  <title>Khalani v1 quickstart</title>
  <button>Start order</button>
  <p id="status">Open the browser console to follow progress.</p>
  <script type="module">
    document.querySelector("button").addEventListener("click", async (event) => {
      event.currentTarget.disabled = true;
      try {
        await import("./v1-quickstart.js");
        document.querySelector("#status").textContent = "Order finished. See the console for its final status.";
      } catch (error) {
        document.querySelector("#status").textContent = String(error);
      }
    });
  </script>
</html>
```

Serve the compiled files and open `http://localhost:8000` in your wallet's browser:

```bash
python3 -m http.server 8000 --directory public --bind 127.0.0.1
```

Click **Start order** once and approve the wallet requests. The script logs the order id and its final status in the console. Reloading the page and clicking again starts a new order.

## What each step does

1. **Quote.** `POST /v1/quotes` asks every route for a price for the connected wallet. When the native route quotes, it is the only route in `routes[]`. Otherwise you get the external routes that quoted. An empty `routes[]` means nothing serves that pair and amount right now.
2. **Build.** `POST /v1/deposit/build` takes the `quoteId`, the chosen `routeId` and the wallet in `from`. It returns `kind: "CONTRACT_CALL"` with an ordered list of wallet actions in `approvals[]`. Build before `quoteExpiresAt`. After it, the build fails with `400` (or `404` once the stored quote is gone), and you must quote again.
3. **Execute.** The script forwards each action to the wallet unchanged: switch chain, approve if needed, then deposit. Where an action has `waitForReceipt: true`, it waits for the receipt before going on, so the deposit never runs ahead of its approval. It keeps the hash of the action marked `deposit: true`.
4. **Submit.** `PUT /v1/deposit/submit` registers that hash and creates the order. Submit once: a second submit for the same quote returns `404`, because the quote is consumed. When the build returned a `buildId`, send it with the submit.
5. **Track.** The script polls `GET /v1/orders/by-id/{orderId}` every 5 seconds until the status is `filled`, `refunded` or `failed`. `refund_pending` is not final. It means the order is waiting for its refund to arrive.

## With curl

The quote and the build from a shell, without a wallet. Replace the example address with the wallet that will deposit.

```bash
BASE=https://api.hyperstream.dev
WALLET=0x5b38da6a701c568545dcfcb03fcb875f56beddc4

QUOTE=$(curl -s -X POST "$BASE/v1/quotes" -H "Content-Type: application/json" -d '{
  "tradeType": "EXACT_INPUT",
  "fromChainId": 42161,
  "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
  "toChainId": 8453,
  "toToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "amount": "10000000",
  "fromAddress": "'"$WALLET"'"
}')
echo "$QUOTE" | jq '.routes[] | {routeId, amountIn: .quote.amountIn, amountOut: .quote.amountOut}'

curl -s -X POST "$BASE/v1/deposit/build" -H "Content-Type: application/json" \
  -d "$(echo "$QUOTE" | jq --arg from "$WALLET" '{from: $from, quoteId: .quoteId, routeId: .routes[0].routeId}')"
```

The build reads the wallet's current allowance on-chain, so it includes an approval only when one is needed.

## Next

- [Integration guide](/v1/integration-guide/): routes, deposit methods, and what happens to the user's funds.
- [Wallet execution](/v1/wallet-execution/): EVM and Solana wallets, deposit addresses, and server-side broadcast.
- [Handling exceptions](/v1/handling-exceptions/): every error shape, and what to retry.
