Docs
Launch App

Pangolin Overview

Introduction to Pangolin
Audits
Tokens & Governance
Official Links & Communities
Become a Partner
Support
Roadmap
Privacy Policy & Legal Notices

Guides

Swapping Tokens
Providing Liquidity
Managing Liquidity Positions
Withdrawing Liquidity
Earning Rewards
Understanding SAR

Developers

Contracts & Integration Reference
APIs
Bug Bounty
Token Listing
Create A Multisig
Honeycomb↗

PangoPerps

API fundamentals
API Authentication
Error Codes
Public Info API
User API
Trading API
PangoPerpsAPI fundamentals

API Authentication

#API Authentication

Use Ed25519 request signatures to access private PangoPerps API endpoints. Public market data does not require these authentication headers.

#Create a signed request

Each authenticated request includes the account ID, the public half of a registered API key, a current timestamp, and a signature made with that key’s private half.

#1. Add the account ID

After the account has been registered, include its account ID in the orderly-account-id request header.

#2. Add the API public key

Put the registered Ed25519 public key in the orderly-key header. Encode the key in Base58 and include the ed25519: key-type prefix, for example:

text
ed25519:<base58-public-key>

The secret key is used locally to sign the request. Never send the secret key to the API.

#3. Set the request timestamp

Read the current time in Unix milliseconds and send it in orderly-timestamp. The server rejects requests whose timestamp is more than 300 seconds away from server time.

#4. Build the signing message

Concatenate the following fields, without separators:

  1. Timestamp in milliseconds, such as 1649920583000.
  2. HTTP method in uppercase, such as POST.
  3. Request path and query string, without the base URL, such as /v1/orders?symbol=PERP_BTC_USDC.
  4. If the request has a body, append its JSON string.

For example:

text
1649920583000POST/v1/order{"symbol":"PERP_ETH_USDC","order_type":"LIMIT","order_price":1521.03,"order_quantity":2.11,"side":"BUY"}

The serialized body used to create the signature must be the same body sent with the request. Include the query string in the same order and form used in the final URL.

#5. Sign the message

Sign the UTF-8 bytes of the normalized message with the Ed25519 private key. Encode the signature using Base64 URL-safe encoding and send it in orderly-signature.

#6. Set the content type

Use the content type that matches the HTTP method:

#7. Send the request

Once the signature is ready, send the request with the required headers below. The timestamp, HTTP method, request path, query string, and serialized body must match the values used to create the signature.

Content-Type — use application/x-www-form-urlencoded for GET and DELETE requests, and application/json for POST and PUT requests.

  • orderly-account-id — the registered account ID.
  • orderly-key — the registered public API key.
  • orderly-signature — the Ed25519 signature encoded with Base64 URL-safe encoding.
  • orderly-timestamp — the request timestamp in Unix milliseconds.

The public key string may include the ed25519: prefix, as in ed25519:<base58-public-key>. When a signer helper expects raw key bytes, pass the key without that display prefix. Keep the private key local; never send it with the request.

#Security checks

Every authenticated request must pass all four checks below.

#Request timestamp

The request is rejected when orderly-timestamp differs from the API server time by more than 300 seconds.

#Signature verification

orderly-signature must be a valid Ed25519 signature of the normalized request content, created with the private key associated with the registered public key.

#API key validity

The orderly-key value must identify a key that has been registered, is associated with the account in orderly-account-id, and has not expired.

#Scope authorization

The key must include the scope required by the endpoint. Use read for private account-data endpoints and trading for order and trading endpoints. These permissions are independent: read does not grant trading access, and trading does not grant read access. Integrations that need both private account data and trading actions should register a key with read,trading.

#Minimal TypeScript example

This single-file example signs and sends a testnet market order. Store a real secret securely; the placeholder below is for illustration only.

typescript
import { ed25519 } from "@noble/curves/ed25519";
import { base58 } from "@scure/base";

async function main() {
  const orderlyAccountId = "YOUR_ACCOUNT_ID";
  const orderlySecret = "YOUR_ORDERLY_SECRET"; // base58-encoded secret key
  const baseUrl = "https://testnet-api.orderly.org";
  const url = new URL(baseUrl + "/v1/order");
  const method = "POST";
  const body = JSON.stringify({
    symbol: "PERP_ETH_USDC",
    order_type: "MARKET",
    order_quantity: 0.01,
    side: "BUY"
  });
  const timestamp = Date.now();
  const privateKey = base58.decode(orderlySecret);
  const message = String(timestamp) + method + url.pathname + url.search + body;
  const signatureBytes = ed25519.sign(new TextEncoder().encode(message), privateKey);
  const publicKeyBytes = ed25519.getPublicKey(privateKey);
  const response = await fetch(url, {
    method,
    body,
    headers: {
      "Content-Type": "application/json",
      "orderly-account-id": orderlyAccountId,
      "orderly-key": "ed25519:" + base58.encode(publicKeyBytes),
      "orderly-timestamp": String(timestamp),
      "orderly-signature": Buffer.from(signatureBytes).toString("base64url")
    }
  });
  const data = await response.json();
  console.log(data);
}

main();
Method Content-Type
GET, DELETE application/x-www-form-urlencoded
POST, PUT application/json

#7. Send the request headers

Header Value
Content-Type Content type for the selected method.
orderly-account-id Account ID associated with the request.
orderly-key Registered public key, Base58 encoded with the ed25519: prefix.
orderly-signature Ed25519 signature of the normalized request, Base64 URL-safe encoded.
orderly-timestamp Request timestamp in milliseconds.

#TypeScript example

This example signs and sends a market order from a PangoPerps account. Store the secret outside source code, such as in a server-side environment variable.

ts
import { ed25519 } from "@noble/curves/ed25519";
import { base58 } from "@scure/base";

async function main() {
  const accountId = process.env.PANGOPERPS_ACCOUNT_ID!;
  const secretKey = base58.decode(process.env.PANGOPERPS_API_SECRET!);
  const baseUrl = "https://testnet-api.orderly.org";
  const url = new URL("/v1/order", baseUrl);
  const method = "POST";
  const body = JSON.stringify({
    symbol: "PERP_ETH_USDC",
    order_type: "MARKET",
    order_quantity: 0.01,
    side: "BUY"
  });
  const timestamp = Date.now();
  const message = `${timestamp}${method}${url.pathname}${url.search}${body}`;
  const signature = ed25519.sign(new TextEncoder().encode(message), secretKey);
  const publicKey = ed25519.getPublicKey(secretKey);

  const response = await fetch(url, {
    method,
    body,
    headers: {
      "Content-Type": "application/json",
      "orderly-account-id": accountId,
      "orderly-key": `ed25519:${base58.encode(publicKey)}`,
      "orderly-timestamp": String(timestamp),
      "orderly-signature": Buffer.from(signature).toString("base64url")
    }
  });

  console.log(await response.json());
}

main();

#Python example

This example uses requests, base58, and the cryptography package. Compact JSON is used so the body included in the signing message is exactly the body sent over HTTP.

python
import base64
import json
import os
import time
from urllib.parse import urlparse

import base58
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey


BASE_URL = "https://testnet-api.orderly.org"
ACCOUNT_ID = os.environ["PANGOPERPS_ACCOUNT_ID"]
SECRET_KEY = Ed25519PrivateKey.from_private_bytes(
    base58.b58decode(os.environ["PANGOPERPS_API_SECRET"])
)


def signed_request(method: str, url: str, body: dict | None = None):
    timestamp = str(int(time.time() * 1000))
    parsed = urlparse(url)
    path_and_query = parsed.path
    if parsed.query:
        path_and_query += "?" + parsed.query

    body_text = "" if body is None else json.dumps(body, separators=(",", ":"))
    message = timestamp + method.upper() + path_and_query + body_text
    signature = base64.urlsafe_b64encode(
        SECRET_KEY.sign(message.encode("utf-8"))
    ).decode("ascii")
    public_key = SECRET_KEY.public_key().public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )

    headers = {
        "orderly-timestamp": timestamp,
        "orderly-account-id": ACCOUNT_ID,
        "orderly-key": "ed25519:" + base58.b58encode(public_key).decode("ascii"),
        "orderly-signature": signature,
        "Content-Type": (
            "application/x-www-form-urlencoded"
            if method.upper() in ("GET", "DELETE")
            else "application/json"
        ),
    }
    return requests.request(
        method.upper(),
        url,
        headers=headers,
        data=body_text if body_text else None,
    )


order = {
    "symbol": "PERP_ETH_USDC",
    "order_type": "MARKET",
    "order_quantity": 0.01,
    "side": "BUY",
}
response = signed_request("POST", BASE_URL + "/v1/order", order)
print(response.json())

#Java example

The example below shows the main steps with OkHttp and the net.i2p.crypto.eddsa Ed25519 implementation. The API key secret is read from the environment. Use a Base58 utility supported by your Java project to decode the secret and encode the public key.

java
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;

import net.i2p.crypto.eddsa.EdDSAEngine;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class PangoPerpsAuthenticationExample {
    public static void main(String[] args) throws Exception {
        String baseUrl = "https://testnet-api.orderly.org";
        String accountId = System.getenv("PANGOPERPS_ACCOUNT_ID");
        byte[] secretBytes = Base58.decode(System.getenv("PANGOPERPS_API_SECRET"));

        var parameters = EdDSANamedCurveTable.getByName(
            EdDSANamedCurveTable.ED_25519
        );
        var privateKeySpec = new EdDSAPrivateKeySpec(secretBytes, parameters);
        EdDSAPrivateKey privateKey = new EdDSAPrivateKey(privateKeySpec);

        String path = "/v1/order";
        String method = "POST";
        String body = "{\"symbol\":\"PERP_ETH_USDC\","
            + "\"order_type\":\"MARKET\","
            + "\"order_quantity\":0.01,\"side\":\"BUY\"}";
        String timestamp = Long.toString(Instant.now().toEpochMilli());
        String message = timestamp + method + path + body;

        EdDSAEngine signer = new EdDSAEngine();
        signer.initSign(privateKey);
        byte[] signatureBytes = signer.signOneShot(
            message.getBytes(StandardCharsets.UTF_8)
        );
        String signature = Base64.getUrlEncoder()
            .withoutPadding()
            .encodeToString(signatureBytes);

        String publicKey = Base58.encode(privateKey.getAbyte());
        OkHttpClient client = new OkHttpClient();
        RequestBody requestBody = RequestBody.create(
            body,
            MediaType.parse("application/json")
        );
        Request request = new Request.Builder()
            .url(baseUrl + path)
            .header("Content-Type", "application/json")
            .header("orderly-account-id", accountId)
            .header("orderly-key", "ed25519:" + publicKey)
            .header("orderly-timestamp", timestamp)
            .header("orderly-signature", signature)
            .post(requestBody)
            .build();

        try (Response response = client.newCall(request).execute()) {
            System.out.println(response.body().string());
        }
    }
}

Base58.decode and Base58.encode above stand for the Base58 codec used by your Java application.

#cURL request template

Generate the timestamp, public key, and signature with your signing code, then insert them into the request. A signature is specific to the request contents and timestamp, so the placeholder below is not a reusable signature.

sh
curl --request POST \
  --url https://testnet-api.orderly.org/v1/order \
  --header 'Content-Type: application/json' \
  --header 'orderly-account-id: <pango-account-id>' \
  --header 'orderly-key: ed25519:<base58-public-key>' \
  --header 'orderly-signature: <base64url-signature>' \
  --header 'orderly-timestamp: <timestamp-in-milliseconds>' \
  --data '{
    "symbol": "PERP_ETH_USDC",
    "order_type": "MARKET",
    "order_quantity": 0.01,
    "side": "BUY"
  }'

#Request checks

An authenticated request is accepted only when each of these checks succeeds:

#Timestamp

The value in orderly-timestamp must be within 300 seconds of the API server’s current time.

#Signature

The orderly-signature value must be a valid Ed25519 signature of the normalized request string, created with the private key paired with the supplied public key.

#Key status

The public key sent in orderly-key must be registered to the account and must not have expired or been revoked.

#Scope

The API key must include the scope needed by the endpoint. Use read for private account data and trading for trading actions. The permissions are independent: one does not imply the other. An integration that reads private account data and also trades needs both read and trading scopes.

PreviousAPI fundamentalsNext Error Codes

On this page

  • API Authentication
  • Create a signed request
  • 7. Send the request
  • Security checks
  • Minimal TypeScript example
  • TypeScript example
  • Python example
  • Java example
  • cURL request template
  • Request checks

Try it on Pangolin

Everything in these docs is live in the app.

SwapLiquidityStakePerps

Need help?

DiscordTelegramX