Add Nado documentation and examples, including new API endpoints, FAQs, and guides for using the TypeScript SDK. Update .env.example with additional configuration options.

This commit is contained in:
discountry
2025-12-19 01:38:09 +08:00
parent 624fecfa70
commit c69ea72860
148 changed files with 17031 additions and 27 deletions
@@ -0,0 +1,82 @@
# Authentication
### Rate limits
A **single wallet address** can be authenticated by up to 5 websocket connections, regardless of the originating IP address. Connections exceeding these limits will be automatically disconnected.
{% hint style="info" %}
See [rate limits](https://docs.nado.xyz/developer-resources/api/subscriptions/rate-limits) for more details.
{% endhint %}
### Request
To access streams that require authentication, submit a request with the <mark style="color:red;">`method`</mark> field set to <mark style="color:red;">`authenticate`</mark>.
{% tabs %}
{% tab title="Authenticate" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
```json
{
"method": "authenticate",
"id": 0,
"tx": {
"sender": "0x...",
"expiration": "1..."
},
"signature": "0x..."
}
```
{% endtab %}
{% endtabs %}
### Request Parameters
<table><thead><tr><th width="198" align="center">Parameter</th><th width="142" align="center">Type</th><th width="112" align="center">Required</th><th>Description</th></tr></thead><tbody><tr><td align="center">method</td><td align="center">string</td><td align="center">Yes</td><td><mark style="color:red;"><code>authenticate</code></mark></td></tr><tr><td align="center">id</td><td align="center">number</td><td align="center">Yes</td><td>Can be set to any positive integer. Can be used to identify the websocket request / response.</td></tr><tr><td align="center">tx</td><td align="center">object</td><td align="center">Yes</td><td><mark style="color:red;"><code>StreamAuthentication</code></mark> object that needs to be signed. See <a href="#signing">Signing</a> section for more details.</td></tr><tr><td align="center">tx.sender</td><td align="center">string</td><td align="center">Yes</td><td>A hex string representing a <mark style="color:red;"><code>bytes32</code></mark> of a specific subaccount.</td></tr><tr><td align="center">tx.expiration</td><td align="center">string</td><td align="center">Yes</td><td>Represents the expiration time in milliseconds since the Unix epoch.</td></tr><tr><td align="center">signature</td><td align="center">string</td><td align="center">Yes</td><td>Hex string representing hash of the <strong>signed</strong> <mark style="color:red;"><code>StreamAuthentication</code></mark> object.See <a href="#signing">Signing</a> section for more details.</td></tr></tbody></table>
{% hint style="info" %}
**Notes**:
* Although <mark style="color:red;">sender</mark> specifies a specific subaccount, authentication applies to the entire wallet address, enabling access to authenticated streams for different subaccounts under that address.
* Once authenticated, the authentication status of that websocket connection cannot be changed and stays for the duration of the connection.
{% endhint %}
## Signing
{% hint style="info" %}
See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page.
{% endhint %}
The typed data struct that needs to be signed is:
```solidity
struct StreamAuthentication {
bytes32 sender;
uint64 expiration;
}
```
<mark style="color:red;">`sender`</mark>: A hex string representing a <mark style="color:red;">`bytes32`</mark> of a specific subaccount. The signature must be signed by the wallet address specified by sender.
<mark style="color:red;">`expiration`</mark>: Represents the expiration time in milliseconds since the Unix epoch. Requests will be denied if the expiration is either smaller than the current time or more than 100 seconds ahead of it.
{% hint style="info" %}
**Notes**:
* Should use the endpoint address as <mark style="color:red;">`verifyingContract`</mark>.
* For signing, you should always use the data type specified in the typed data struct which might be different from the type sent in the request e.g: <mark style="color:red;">`expiration`</mark> should be an <mark style="color:red;">`uint64`</mark> for **Signing** but should be sent as a <mark style="color:red;">`string`</mark> in the final payload.
{% endhint %}
### **Response**
```json
{
"result": null,
"id": 10
}
```
@@ -0,0 +1,284 @@
# Events
## **Order Update**
**Update speed: real-time**
```json
{
"type": "order_update",
// timestamp of the event in nanoseconds
"timestamp": "1695081920633151000",
"product_id": 1,
// order digest
"digest": "0xf7712b63ccf70358db8f201e9bf33977423e7a63f6a16f6dab180bdd580f7c6c",
// remaining amount to be filled.
// will be `0` if the order is either fully filled or cancelled.
"amount": "82000000000000000",
// any of: "filled", "cancelled", "placed"
"reason": "filled",
// an optional `order id` that can be provided when placing an order
"id": 100
}
```
### Example Scenarios:
Let's assume your initial order amount is 100 units and each match occurs for an amount of 10 units.
{% hint style="info" %}
**Note**: The following events only include <mark style="color:red;">`amount`</mark> and <mark style="color:red;">`reason`</mark> for simplicity.
{% endhint %}
#### Scenario 1: Limit Order Partially Fills and Gets Placed
Your limit order matches against existing orders in the book.
You will receive the following events over websocket, each with the same timestamp but in sequential order:
* Event 1: `(90, "filled")` — 10 units of your order are filled.
* Event 2: `(80, "filled")` — Another 10 units are filled.
* Event 3: `(80, "placed")` — The remaining 80 units are placed on the book.
#### Scenario 2: Immediate-Or-Cancel (IOC) Order Partially Fills
Your IOC order matches against existing orders but is not completely filled.
The events you will receive are as follows:
* Event 1: `(90, "filled")` — 10 units are filled.
* Event 2: `(80, "filled")` — Another 10 units are filled.
* Event 3: `(0, "cancelled")` — The remaining order is cancelled. **Note**: If your IOC order is fully filled, the last event you will receive is `(0, "filled")`.
#### Scenario 3: Resting Limit Order Gets Matched
Your existing, or "resting," limit order matches against an incoming order.
You will receive the following event: `(90, "filled")` — 10 units of your resting limit order are filled.
#### Scenario 4: Resting Limit Order Gets Cancelled
Your resting limit order could be cancelled for various reasons, such as manual cancellation, expiration, failing health checks, or self-trade prevention.
In any of these cases, you will receive: `(0, "cancelled")`
#### **Scenario 5: IOC order doesn't cross the book or FOK order fails to be entirely filled**
In any of these cases, you will receive: `(0, "cancelled")`
## **Trade**
**Update speed: real-time**
```json
{
"type": "trade",
"timestamp": "1676151190656903000", // timestamp of the event in nanoseconds
"product_id": 1,
"price": "1000", // price the trade happened at, multiplied by 1e18
// both taker_qty and maker_qty have the same value;
// set to filled amount (min amount of taker and maker) when matching against book
"taker_qty": "1000",
"maker_qty": "1000",
"is_taker_buyer": true
}
```
## **Best Bid Offer**
**Update speed: real-time**
```json
{
"type": "best_bid_offer",
"timestamp": "1676151190656903000", // timestamp of the event in nanoseconds
"product_id": 1,
"bid_price": "1000", // the highest bid price, multiplied by 1e18
"bid_qty": "1000", // quantity at the highest bid, multiplied by 1e18.
// i.e. if this is USDT0 with 6 decimals, one USDT0
// would be 1e12
"ask_price": "1000", // lowest ask price
"ask_qty": "1000" // quantity at the lowest ask
}
```
## **Fill**
**Update speed: real-time**
```json
{
"type": "fill",
"timestamp": "1676151190656903000", // timestamp of the event in nanoseconds
"product_id": 1,
// the subaccount that placed this order
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
// hash of the order that uniquely identifies it
"order_digest": "0xf4f7a8767faf0c7f72251a1f9e5da590f708fd9842bf8fcdeacbaa0237958fff",
// order appendix containing execution type, reduce-only flag, etc.
"appendix": "4096",
// the amount filled, multiplied by 1e18
"filled_qty": "1000",
// the amount outstanding unfilled, multiplied by 1e18
"remaining_qty": "2000",
// the original order amount, multiplied by 1e18
"original_qty": "3000",
// fill price
"price": "24991000000000000000000",
// true for `taker`, false for `maker`
"is_taker": true,
"is_bid": true,
// the amount of fee paid, multiplied by 1e18
"fee": "100",
// the submission_idx of the transaction (n_submissions - 1)
// can use to map `fills` to historical `matches`.
"submission_idx": 100,
// an optional `order id` that can be provided when placing an order
"id": 100
}
```
## **Position Change**
**Update speed: real-time**
```json
{
"type":"position_change",
"timestamp": "1676151190656903000", // timestamp of event in nanoseconds
"product_id":1,
// subaccount who's position changed
"subaccount":"0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43706d00000000000000000000",
// whether this position change is for an isolated margin position
"isolated": false,
// new amount for this product
"amount":"51007390115411548",
// new quote balance for this product; zero for everything except non lp perps
// the negative of the entry cost of the perp
"v_quote_amount":"0",
// any of: "deposit_collateral", "match_orders", "withdraw_collateral", "transfer_quote", "settle_pnl", "mint_nlp", "burn_nlp", "liquidate_subaccount"
"reason": "deposit_collateral"
}
```
{% hint style="info" %}
**Note:** that it is possible that back to back <mark style="color:red;">`position_change`</mark> events have the same fields except for <mark style="color:red;">`timestamp`</mark>. Additionally, <mark style="color:red;">`position_change`</mark> events are not sent on interest and funding payments, and also are not sent on actions done through slow mode (except deposits). The full list of actions that will trigger a <mark style="color:red;">`PositionChange`</mark> event are:
* Minting or burning NLP tokens
* Liquidating a subaccount
* Matching orders
* Depositing or withdrawing spot
* Settling PNL
{% endhint %}
## **Book Depth**
**Update speed: once every 50ms**
```json
{
"type":"book_depth",
// book depth aggregates a number of events once every 50ms
// these are the minimum and maximum timestamps from
// events that contributed to this response
"min_timestamp": "1683805381879572835",
"max_timestamp": "1683805381879572835",
// the max_timestamp of the last book_depth event for this product
"last_max_timestamp": "1683805381771464799",
"product_id":1,
// changes to the bid side of the book in the form of [[price, new_qty]]
"bids":[["21594490000000000000000","51007390115411548"]],
// changes to the ask side of the book in the form of [[price, new_qty]]
"asks":[["21694490000000000000000","0"],["21695050000000000000000","0"]]
}
```
{% hint style="info" %}
**Note**: To keep an updated local orderbook, do the following:
1. Subscribe to the `book_depth` stream and queue up events.
2. Get a market data snapshot by calling [MarketLiquidity](https://docs.vertexprotocol.com/developer-resources/api/gateway/queries/market-liquidity). The snapshot contains a `timestamp` in the response.
3. Apply events with `max_timestamp` > snapshot `timestamp`.
4. When you receive an event where its `last_max_timestamp` is not equal to the `max_timestamp` of the last event you've received, it means some events were lost and you should repeat 1-3 again.
{% endhint %}
## **Liquidation**
**Update speed: real-time**
```json
{
"type": "liquidation",
"timestamp": "1234567890000", // timestamp of the event in nanoseconds
// single element for regular liquidations, two elements for spread liquidations (spot_id, perp_id)
"product_ids": [1],
// liquidator subaccount (32 bytes)
"liquidator": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
// liquidatee subaccount (32 bytes)
"liquidatee": "0x8b6fd3859f7065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
// amount liquidated (positive for long, negative for short)
"amount": "1000000000000000000",
// price at which liquidation occurred
"price": "50000000000000000000"
}
```
## Latest Candlestick
**Update speed: real-time**
```json
{
"type": "latest_candlestick",
"timestamp": 1234567890000,
"product_id": 1,
"granularity": 60,
"open_x18": "50000000000000000000",
"high_x18": "51000000000000000000",
"low_x18": "49000000000000000000",
"close_x18": "50500000000000000000",
"volume": "1000000000000000000"
}
```
## Funding Payment
**Update speed: real-time at the time of payment (payments happen hourly).**
```json
{
"type": "funding_payment",
"timestamp": 1234567890000,
"product_id": 1,
// funding payment amount (positive = receive, negative = pay)
"payment_amount": "1000000000000000000",
// open interest at time of funding
"open_interest": "50000000000000000000",
// cumulative funding values, multiplied by 1e18
"cumulative_funding_long_x18": "100000000000000000",
"cumulative_funding_short_x18": "-100000000000000000",
// time delta over which the funding payment was calculated
"dt": 3600000
}
```
## Funding Rate
**Update speed: real-time (updates occur every 20 seconds).**
{% hint style="info" %}
**Note**: The `funding_rate_x18` and `update_time` values are identical to those returned by the [Funding Rate](https://docs.nado.xyz/developer-resources/api/archive-indexer/funding-rate) indexer endpoint.
{% endhint %}
```json
{
"type": "funding_rate",
// timestamp when the event was generated, in nanoseconds
"timestamp": "1234567890123456789",
"product_id": 1,
// latest 24hr funding rate, multiplied by 1e18
"funding_rate_x18": "50000000000000000",
// timestamp when the funding rate was updated, in seconds
"update_time": "1234567890"
}
```
@@ -0,0 +1,3 @@
# Rate limits
Each IP address is restricted to a maximum of **100** active websocket connections. Additionally, a **single wallet address** can be authenticated by up to **5** websocket connections, regardless of the originating IP address. Connections exceeding these limits will be automatically disconnected.
@@ -0,0 +1,538 @@
# Streams
## Available Streams
See below the available streams you can subscribe to:
```rust
pub enum StreamSubscription {
// pass `null` product_id to subscribe to all products
OrderUpdate { product_id: Option<u32>, subaccount: H256 },
Trade { product_id: u32 },
BestBidOffer { product_id: u32 },
// pass `null` product_id to subscribe to all products
Fill { product_id: Option<u32>, subaccount: H256 },
// pass `null` product_id to subscribe to all products
PositionChange { product_id: Option<u32>, subaccount: H256},
BookDepth { product_id: u32 },
// pass `null` product_id to subscribe to all products
Liquidation { product_id: Option<u32> },
LatestCandlestick {
product_id: u32,
// time interval in seconds (e.g., 60, 300, 900, 3600)
granularity: i32
},
FundingPayment { product_id: u32 },
// pass `null` product_id to subscribe to all products
FundingRate { product_id: Option<u32> }
}
```
## **Subscribing to a stream**
{% tabs %}
{% tab title="Order Update" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: Yes.*
{% hint style="info" %}
**Note**: Set <mark style="color:red;">`product_id`</mark> to <mark style="color:red;">`null`</mark> to subscribe to order updates across all products for the subaccount.
{% endhint %}
```json
{
"method": "subscribe",
"stream": {
"type": "order_update",
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
"product_id": 1
},
"id": 10
}
```
**Subscribe to all products:**
```json
{
"method": "subscribe",
"stream": {
"type": "order_update",
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
"product_id": null
},
"id": 10
}
```
{% endtab %}
{% tab title="Trade" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: No.*
```json
{
"method": "subscribe",
"stream": {
"type": "trade",
"product_id": 0
},
"id": 10
}
```
{% endtab %}
{% tab title="Best Bid Offer" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: No.*
```json
{
"method": "subscribe",
"stream": {
"type": "best_bid_offer",
"product_id": 0
},
"id": 10
}
```
{% endtab %}
{% endtabs %}
{% tabs %}
{% tab title="Fill" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: No.*
{% hint style="info" %}
**Note**: Set <mark style="color:red;">`product_id`</mark> to <mark style="color:red;">`null`</mark> to subscribe to fills across all products for the subaccount.
{% endhint %}
```json
{
"method": "subscribe",
"stream": {
"type": "fill",
"product_id": 1,
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000"
},
"id": 10
}
```
**Subscribe to all products:**
```json
{
"method": "subscribe",
"stream": {
"type": "fill",
"product_id": null,
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000"
},
"id": 10
}
```
{% endtab %}
{% tab title="Position Change" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: No.*
{% hint style="info" %}
**Note**: Set <mark style="color:red;">`product_id`</mark> to <mark style="color:red;">`null`</mark> to subscribe to position changes across all products for the subaccount.
{% endhint %}
```json
{
"method": "subscribe",
"stream": {
"type": "position_change",
"product_id": 0,
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000"
},
"id": 10
}
```
**Subscribe to all products:**
```json
{
"method": "subscribe",
"stream": {
"type": "position_change",
"product_id": null,
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000"
},
"id": 10
}
```
{% endtab %}
{% tab title="Book Depth" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: No.*
```json
{
"method": "subscribe",
"stream": {
"type": "book_depth",
"product_id": 0
},
"id": 10
}
```
{% endtab %}
{% endtabs %}
{% tabs %}
{% tab title="Liquidation" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: No.*
{% hint style="info" %}
**Note**: Set <mark style="color:red;">`product_id`</mark> to <mark style="color:red;">`null`</mark> to subscribe to liquidations across all products.
{% endhint %}
```json
{
"method": "subscribe",
"stream": {
"type": "liquidation",
"product_id": 1
},
"id": 10
}
```
**Subscribe to all products:**
```json
{
"method": "subscribe",
"stream": {
"type": "liquidation",
"product_id": null
},
"id": 10
}
```
{% endtab %}
{% tab title="Latest Candlestick" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
{% hint style="info" %}
See all supportes <mark style="color:red;">`granularity`</mark> values in [Available Granularities](https://docs.nado.xyz/developer-resources/archive-indexer/candlesticks#available-granularities)
{% endhint %}
*Requires Authentication: No.*
```json
{
"method": "subscribe",
"stream": {
"type": "latest_candlestick",
"product_id": 1,
"granularity": 60
},
"id": 10
}
```
{% endtab %}
{% tab title="Funding Payment" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: No.*
```json
{
"method": "subscribe",
"stream": {
"type": "funding_payment",
"product_id": 2
},
"id": 10
}
```
{% endtab %}
{% tab title="Funding Rate" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
*Requires Authentication: No.*
{% hint style="info" %}
**Note**: Set <mark style="color:red;">`product_id`</mark> to <mark style="color:red;">`null`</mark> to subscribe to funding rate updates across all products.
{% endhint %}
```json
{
"method": "subscribe",
"stream": {
"type": "funding_rate",
"product_id": 2
},
"id": 10
}
```
**Subscribe to all products:**
```json
{
"method": "subscribe",
"stream": {
"type": "funding_rate",
"product_id": null
},
"id": 10
}
```
{% endtab %}
{% endtabs %}
### **Response**
```json
{
"result": null,
"id": 10
}
```
## **Unsubscribing**
{% tabs %}
{% tab title="Order Update" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
```json
{
"method": "unsubscribe",
"stream": {
"type": "order_update",
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
"product_id": 1
},
"id": 10
}
```
**Unsubscribe from all products:**
```json
{
"method": "unsubscribe",
"stream": {
"type": "order_update",
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
"product_id": null
},
"id": 10
}
```
{% endtab %}
{% tab title="Trade" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
```json
{
"method": "unsubscribe",
"stream": {
"type": "trade",
"product_id": 0
},
"id": 10
}
```
{% endtab %}
{% tab title="Best Bid Offer" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
```json
{
"method": "unsubscribe",
"stream": {
"type": "best_bid_offer",
"product_id": 0
},
"id": 10
}
```
{% endtab %}
{% tab title="Fill" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
```json
{
"method": "unsubscribe",
"stream": {
"type": "fill",
"product_id": 0,
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000"
},
"id": 10
}
```
{% endtab %}
{% tab title="Position Change" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
```json
{
"method": "unsubscribe",
"stream": {
"type": "position_change",
"product_id": 0,
"subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000"
},
"id": 10
}
```
{% endtab %}
{% tab title="Book Depth" %}
**Connect**
<mark style="color:orange;">`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`</mark>
**Message**
```json
{
"method": "unsubscribe",
"stream": {
"type": "book_depth",
"product_id": 0,
},
"id": 10
}
```
{% endtab %}
{% endtabs %}
### **Response**
```json
{
"result": null,
"id": 10
}
```
## **Listing subscribed streams**
```json
{
"method": "list",
"id": 10
}
```
### Response
```json
{
"result": [
{
"type": "default"
},
{
"type": "trade",
"product_id": 0
}
],
"id": 10
}
```