> For the complete documentation index, see [llms.txt](https://helvex.gitbook.io/helvex-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://helvex.gitbook.io/helvex-docs/01-helvex-api-integrator-guide.md).

# API integrator guide

Call Helvex from your own client: hosts, HMAC signing, quote, RFQ, fill, transfer. Session-only deposit and withdraw.

Helvex is a permissioned, trader-first RFQ desk on Canton Network. The maker sets `minBuyAmount` — the minimum they'll receive. A fill qualifies only when `buyAmount` meets or beats it. A qualifying fill is not guaranteed: the first accepted fill wins, and a second attempt gets `409`.

How to call Helvex from **your own client** (backend, script, or trading desk). Most people never need this. They use the app:

* TestNet: <https://testnet-app.helvex.cc>
* MainNet: <https://app.helvex.cc>

Use this guide if you want to post RFQs, fill RFQs, or transfer between Helvex accounts from code.

**Not covered here:** KYC. Finish verification and log in through the app first. For the product model (Loop vs trading ID, lock, settle), read [How the app works](/helvex-docs/02-helvex-how-the-app-works.md).

***

## Hosts (use these — do not invent your own)

|                 | TestNet                          | MainNet                  |
| --------------- | -------------------------------- | ------------------------ |
| **App**         | <https://testnet-app.helvex.cc>  | <https://app.helvex.cc>  |
| **API**         | <https://testnet-api.helvex.cc>  | <https://api.helvex.cc>  |
| **Loop wallet** | <https://testnet.cantonloop.com> | <https://cantonloop.com> |

Keep Helvex and Loop on the **same** network. Examples below use TestNet.

```bash
curl https://testnet-api.helvex.cc/health
curl https://api.helvex.cc/health
# {"status":"ok"}
```

`GET /v1/pairs`, `GET /v1/limits`, and `GET /v1/quote` need no login. They are still **geo-gated** (OFAC / blocked-country list). Every other `/v1` route needs an app session or an API client key.

***

## Two ways to call the API

| How you call    | Who it is for                                            | What you can do                                                           |
| --------------- | -------------------------------------------------------- | ------------------------------------------------------------------------- |
| **App session** | A logged-in user in the Helvex website                   | Everything: Activate, deposit, withdraw, mint an API key, trade, transfer |
| **API client**  | Your server or script, using a key you minted in the app | Quote, pairs, limits, trade, fill, transfer, and read balances / account  |

The website sends `x-proxy-identity`. You do **not** build that header yourself.

An **API client** sends four headers on every call (see below). It **cannot** deposit, withdraw, Activate, or mint keys — even if the key has a `withdraw` scope. Do those in the app, then automate trading with the key.

API-key allowlist (hard-coded): `GET /v1/pairs`, `/v1/quote`, `/v1/limits`, `/v1/accounts/me`, `/v1/balances`; intents (list, get, post, cancel); solver list + fill; transfers (list + send). Anything else with `x-api-key` returns `403`.

***

## 1. Create an API client key (once, in the app)

Log in → Activate your trading ID → **Account** → create a key.

Or from a logged-in session (not from an API key):

```http
POST /v1/keys
{ "label": "desk-1", "scopes": ["read", "maker", "solver", "transfer"] }
```

The **secret is shown once**. Save it. Losing it means mint a new key. `GET /v1/keys` lists keys; `DELETE /v1/keys/{id}` revokes one. Both are session-only.

| Scope      | What it allows                                                              |
| ---------- | --------------------------------------------------------------------------- |
| `read`     | `GET /v1/balances`, your intents                                            |
| `maker`    | Post and cancel RFQs                                                        |
| `solver`   | List open RFQs and fill them                                                |
| `transfer` | Send to another Helvex user by email                                        |
| `withdraw` | Exists as a name. API clients still cannot call withdraw or deposit routes. |

`GET /v1/accounts/me` is on the API-key allowlist and does not need `read`.

Self-serve keys are capped at **2 requests per minute**. An operator can raise a key’s tier; you cannot raise it yourself.

### Sign every API-client request

Four headers on every call that sends `x-api-key` (including GET):

| Header        | Value                                              |
| ------------- | -------------------------------------------------- |
| `x-api-key`   | Key id (public)                                    |
| `x-timestamp` | Unix time in **milliseconds** (`Date.now()`)       |
| `x-nonce`     | Unique per request (a UUID is fine). Reuse → `401` |
| `x-signature` | Lowercase hex HMAC-SHA256 of the canonical string  |

Canonical string (five fields, dots):

```
{timestamp}.{nonce}.{METHOD}.{path}.{sha256hex(body)}
```

* `METHOD` is uppercase (`GET`, `POST`)
* `path` is only the path (`/v1/quote`, `/v1/balances`). **No host. No query string.**
* Empty body → hash the empty string `""`
* Clock must be within **5 minutes** of the server
* The secret is **never** sent on the wire

{% hint style="danger" %}
If you put `?pair=…&amount=…` into the HMAC `path`, the signature will not match. Sign `/v1/quote`. Append the query only on the URL you fetch.
{% endhint %}

```javascript
import { createHash, createHmac, randomUUID } from "node:crypto";

async function signedFetch(base, keyId, secret, method, path, body, query) {
  const raw = body ? JSON.stringify(body) : "";
  const timestamp = Date.now().toString();
  const nonce = randomUUID();
  const canonical = [
    timestamp, nonce, method.toUpperCase(), path,
    createHash("sha256").update(raw).digest("hex"),
  ].join(".");
  const signature = createHmac("sha256", secret).update(canonical).digest("hex");
  const url = query ? `${base}${path}?${query}` : `${base}${path}`;
  const res = await fetch(url, {
    method,
    headers: {
      "content-type": "application/json",
      "x-api-key": keyId,
      "x-timestamp": timestamp,
      "x-nonce": nonce,
      "x-signature": signature,
    },
    body: raw || undefined,
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return res.json();
}

// Public quote — no API key needed:
//   GET https://testnet-api.helvex.cc/v1/quote?pair=CBTC_USDCX&amount=0.0003
// Same quote with a key — HMAC path is /v1/quote, query is only on the URL:
//   signedFetch(base, keyId, secret, "GET", "/v1/quote", undefined, "pair=CBTC_USDCX&amount=0.0003")
```

### Amounts

Always **decimal strings**: `"0.0003"`, never `0.0003`. No spaces. Pattern: `^\d+(\.\d+)?$`.

API instrument keys: `CC`, `CBTC`, `USDCX`.

CC and CBTC use 10 decimal places. USDCx uses **6** on TestNet and **10** on MainNet.

***

## 2. Account and balances

In the app: **Activate trading account**. That creates your Helvex **trading ID** (app party). You trade from this ID, not from Loop.

Loop is only for **deposit and withdraw**. You can Activate and trade without Loop if the trading ID is already funded (for example by an internal transfer).

```http
POST /v1/accounts/onboard
{ }

GET /v1/accounts/me
GET /v1/balances
```

`loopPartyId` on onboard is optional if Loop is already linked.

Balances return `total`, `locked`, and **`available`**. Use **`available`** for a new RFQ, fill, or transfer. `locked` includes open RFQs, in-flight withdrawals, and in-flight transfers.

***

## 3. Deposit (app session only)

1. Prepare:

```http
POST /v1/deposits/prepare
{
  "instrument": "CBTC",
  "amount": "0.001",
  "idempotencyKey": "<at least 8 characters, unique per deposit>"
}
```

Response includes `depositTo` (your trading ID) and `loopInstrument` (registry metadata so the wallet does not guess ids).

2. From **Loop on the same network**, send that amount to `depositTo`. Only the Loop wallet you connected is accepted.
3. Canton transfers are two-step. After Loop sends, Helvex accepts:

```http
POST /v1/deposits/accept-pending
```

`{"accepted": 0, "failed": []}` usually means the background sweep already credited you (\~30s). That is success.

```http
GET  /v1/deposits
GET  /v1/deposits/pending
POST /v1/deposits/{id}/abandon
```

Abandon only if Loop never signed (for example no CC for gas) and the row is still `PENDING`. Do not abandon a deposit that already credited.

Loop must hold **CC** before it can send CBTC or USDCx (gas).

***

## 4. Withdraw (app session only)

Destination is always the connected Loop party. There is **no destination field**.

```http
POST /v1/withdrawals
{
  "instrument": "CBTC",
  "amount": "0.001",
  "idempotencyKey": "<unique string, at least 8 characters>"
}

GET /v1/withdrawals
GET /v1/withdrawals/{id}
```

Withdrawals at or above **$10,000** USD notional wait for Helvex approval. Amounts below **$10,000** auto-approve.

Leave some **CC** on the trading ID so the withdraw has gas.

***

## 5. Market data (no login)

```http
GET https://testnet-api.helvex.cc/v1/pairs
GET https://testnet-api.helvex.cc/v1/limits
GET https://testnet-api.helvex.cc/v1/quote?pair=CBTC_USDCX&amount=0.0003
```

`amount` and `sellAmount` both work on the quote.

**Do not hard-code the pair list.** `GET /v1/pairs` is the source of truth for that network.

On **both** TestNet and MainNet today: `CBTC_USDCX`, `USDCX_CBTC`, `CC_USDCX`, `USDCX_CC`, `CC_CBTC`, `CBTC_CC`.

```http
GET /v1/limits
```

returns:

| Field                 | Live default                                                |
| --------------------- | ----------------------------------------------------------- |
| `minNotionalUsd`      | **10**                                                      |
| `maxNotionalUsd`      | **1000**                                                    |
| `maxTradesPerMin`     | **2** (applies to `POST /v1/intents` per account)           |
| `maxIntentTtlSeconds` | **86400** (24 hours) — RFQ `deadline` cannot be further out |

The quote’s `withinLimits` must be `true` before you post.

Instrument floors (also on each pair as `minSell` / `minBuy`): CC **0.10**, CBTC **0.00001**, USDCx **1.0**.

| Quote field                   | What to do with it                                 |
| ----------------------------- | -------------------------------------------------- |
| `minReceive`                  | Put this on the RFQ as `minBuyAmount`              |
| `estReceive`                  | What you might receive (indicative)                |
| `notionalUsd`                 | Size in USD                                        |
| `withinLimits`                | Must be `true` before you post                     |
| `ttlSeconds` / `expiresAt`    | Quote lasts **30 seconds** — refresh if it expired |
| `rate`, `feeBps`, `feeAmount` | Helvex app fee is **0** today (`feeBps` = 0)       |

There is no `buyAmount` on the quote. Prices: CBTC ≈ CoinGecko Bitcoin, CC ≈ Canton network CC price, USDCx = **$1.00**.

***

## 6. Maker — post an RFQ

Needs an app session **or** an API client with `maker` (and `read` to poll).

Trades do **not** open Loop. Helvex authorizes the call from your login or API key, and `maker` must be **your** trading ID.

You still send a `signature` field. It is a hash of the RFQ body so the terms cannot be silently changed — not a Loop wallet pop-up.

### Payload (field order must stay exactly like this)

```json
{
  "domain": "intent-swap/v1",
  "intentId": "<new uuid, unique — reuse returns 409>",
  "maker": "<your trading ID>",
  "pair": "CBTC_USDCX",
  "sellAmount": "0.0003",
  "minBuyAmount": "<copy quote.minReceive>",
  "deadline": "<ISO-8601, in the future, at most 24 hours from now>",
  "nonce": 1787212900394
}
```

`deadline` example in code: `new Date(Date.now() + 15 * 60 * 1000).toISOString()`. `nonce` is a non-negative integer.

Hash the UTF-8 bytes of `JSON.stringify` of **exactly those keys in that order** (no `signature` inside the hash).

```
signature = "session:" + sha256hex(canonicalJson)
```

`dev:` plus the same hash is also accepted. Do **not** send `loop:…` unless Helvex has told you your environment uses Loop signing (it does not on TestNet or MainNet today).

```http
POST /v1/intents
{ ...payload, "signature": "session:<sha256 hex>" }
```

After submit: `SUBMITTED` → `LOCK_PENDING` → `LOCKED`. At `LOCKED` the sell amount is escrowed and solvers can see the RFQ.

```http
GET  /v1/intents
GET  /v1/intents/{intentId}
POST /v1/intents/{intentId}/cancel
```

`GET /v1/intents` returns RFQs you **created** (`makerParty`) and RFQs you **filled** (`winningSolver`). Optional `?role=maker|solver|all` (default `all`). You cannot list another party’s book.

Cancel needs `maker`. Unfilled `LOCKED` goes `REFUNDING` → `REFUNDED` (sell returns to **available**). Same if the deadline passes. The type list still contains `EXPIRED` / `CANCELLED`; the live workers go straight to `REFUNDING`.

***

## 7. Solver — fill someone else’s RFQ

Needs `solver`. You **cannot** fill your own RFQ. Use a second Helvex account.

```http
GET /v1/solver/intents
```

Only **`LOCKED`** RFQs (sell already escrowed).

```http
POST /v1/solver/fill
{
  "intentId": "<id>",
  "solver": "<your trading ID>",
  "buyAmount": "<at least the maker’s minBuyAmount>"
}
```

* `buyAmount` is **required** on the API (the website may pre-fill the minimum)
* `buyAmount` > 0 and ≥ the maker’s `minBuyAmount` (meet or beat the minimum; a higher amount is price improvement, not a guaranteed fill)
* `solver` must be your trading ID
* You need enough **`available`** buy asset
* Second filler gets `409` — that is normal

The HTTP response is usually **`MATCHED`**. A worker then runs `SETTLING` → `SETTLED` (atomic DvP: both legs move or neither).

Poll `GET /v1/intents/{intentId}`. The maker can always read their RFQ. The solver who filled it (`winningSolver`) can read it after `MATCHED`, including `SETTLED` and `FAILED`. Any other solver may read it only while it is `LOCKED`, `MATCHED`, or `SETTLING`.

Fill is **not** counted in the 2-intents-per-minute cap. That cap is only `POST /v1/intents`.

***

## 8. Internal transfer (optional)

App session or API client with `transfer`. No Loop. Recipient must already be a **verified** Helvex user with an **activated trading ID**.

```http
POST /v1/transfers
{
  "recipientEmail": "other-user@their-domain.com",
  "instrument": "CBTC",
  "amount": "0.001",
  "idempotencyKey": "<unique string, at least 8 characters>"
}

GET /v1/transfers
```

Default individual caps (overridable by Helvex): **5 CBTC** per transfer, **20 CBTC** per UTC day, **50** transfers per day. CC and USDCx defaults are much higher (100,000 per transfer). Institution / market-maker tiers only raise the daily count unless Helvex sets more.

***

## 9. Try a full swap (two accounts)

1. Account A and account B: KYC done, trading ID activated, each holds **`available`** sell/buy assets (and some CC).
2. A: `GET /v1/quote` → confirm `withinLimits` → build payload with `minReceive` and a deadline ≤ 24h → `POST /v1/intents` → wait until `LOCKED`.
3. B (other login or other API key): `GET /v1/solver/intents` → `POST /v1/solver/fill` with a required `buyAmount`.
4. Poll until `SETTLED`.
5. Optional: A cancels an unfilled RFQ, or lets the deadline refund.
6. In the app: withdraw to Loop if you connected a wallet.

One account cannot fill its own RFQ.

***

## 10. Common errors

| Code  | Meaning                                                                                                                                 |
| ----- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Bad body, below $10 / above $1,000, past or >24h deadline, not enough **available**, `maker`/`solver` not your trading ID, or self-fill |
| `401` | Missing or wrong API-client signature, timestamp outside 5 minutes, or reused nonce                                                     |
| `403` | Missing scope, API key used on a blocked route, blocked country, or not allowed to see that intent                                      |
| `404` | Unknown id, or not yours                                                                                                                |
| `409` | Already filled, or duplicate `intentId`                                                                                                 |
| `429` | Too many requests (2 RFQs/min per account; 2 API-key calls/min per self-serve key)                                                      |
| `503` | Canton ledger unreachable                                                                                                               |

Party ids and amounts are always strings. Prefer `available` over `total`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://helvex.gitbook.io/helvex-docs/01-helvex-api-integrator-guide.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
