@1247/orderly-api-authentication
Orderly Network: API Authentication
This skill covers both authentication layers in Orderly Network: wallet signatures (EIP-712 for EVM, Ed25519 message signing for Solana) for account registration and key management, and Ed25519 signatures for API request authentication.
When to Use
- Setting up new Orderly accounts and API keys (EVM or Solana)
- Building server-side trading bots
- Implementing direct API calls
- Understanding the two-layer authentication flow
- Debugging signature issues
Prerequisites
- A Web3 wallet (MetaMask, WalletConnect for EVM; Phantom, Solflare for Solana)
- A Broker ID (e.g.,
woofi_dex, or your own) - Node.js 18+ installed (for programmatic usage)
- Understanding of EIP-712 typed data signing (EVM) or Ed25519 message signing (Solana) and Ed25519 cryptography
Authentication Overview
Orderly Network uses a two-layer authentication system supporting both EVM and Solana wallets:
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: Wallet Authentication │
│ ───────────────────────────── │
│ • Account registration │
│ • API key management (add/remove keys) │
│ • Privileged operations (withdrawals, admin) │
│ │
│ EVM: EIP-712 typed data signing │
│ Solana: Ed25519 message signing │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Layer 2: API Authentication (Ed25519) │
│ ───────────────────────────────────── │
│ • Trading operations (place/cancel orders) │
│ • Reading account data (positions, balances) │
│ • WebSocket connections │
│ │
│ Signed by: Ed25519 key pair │
│ Key type: Locally-generated Ed25519 key pair │
└─────────────────────────────────────────────────────────────┘
Authentication Flow
1. User connects wallet
2. Wallet signs EIP-712 message to register account
3. Account ID is created
4. User generates Ed25519 key pair
5. Wallet signs EIP-712 message to authorize the Ed25519 key
6. Ed25519 key is used for all subsequent API calls
Environment Configuration
| Environment | API Base URL | WebSocket URL |
|---|---|---|
| Mainnet | https://api.orderly.org |
wss://ws.orderly.org/ws/stream |
| Testnet | https://testnet-api.orderly.org |
wss://testnet-ws.orderly.org/ws/stream |
Note: These API base URLs work for both EVM and Solana wallets. Orderly's API is omnichain - the same endpoints handle both chains.
Getting Supported Chains
Don't hardcode chain IDs. Fetch them dynamically for your broker:
// Get supported chains for your broker
const response = await fetch(`https://api.orderly.org/v1/public/chain_info?broker_id=${BROKER_ID}`);
const { data } = await response.json();
// data.chains contains supported chain_ids
// Use these chain IDs for EIP-712 domain configuration
EIP-712 Domain Configuration
Orderly uses two different EIP-712 domains depending on the operation:
| Domain Type | Use Case | Mainnet | Testnet |
|---|---|---|---|
| Off-chain | Account registration, API key management | 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC |
0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC |
| On-chain | Withdrawals, internal transfers, settle PnL | 0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203 |
0x1826B75e2ef249173FC735149AE4B8e9ea10abff |
Important: The on-chain
verifyingContractis the Ledger contract on Orderly L2. This is a single contract for all chains (not per-chain). Vault contracts exist on each supported EVM chain for deposits, but the Ledger is the source of truth for on-chain operations.
Off-Chain Domain (Registration, API Keys)
Used for operations that don't directly interact with smart contracts:
const OFFCHAIN_DOMAIN = {
name: 'Orderly',
version: '1',
chainId: 421614, // Connected chain ID (e.g., Arbitrum Sepolia)
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
};
On-Chain Domain (Withdrawals, Transfers)
Used for operations that interact with the Ledger contract on Orderly L2:
const ONCHAIN_DOMAIN = {
name: 'Orderly',
version: '1',
chainId: 42161, // Connected chain ID
verifyingContract: isTestnet
? '0x1826B75e2ef249173FC735149AE4B8e9ea10abff'
: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203',
};
Part 1: EIP-712 Wallet Authentication
Wallet authentication is required for account-level operations that need proof of ownership.
When to Use EIP-712
- Account Registration: Creating a new Orderly account
- API Key Management: Adding or removing Ed25519 API keys
- Withdrawals: Requesting token withdrawals from the vault
- Admin Operations: Setting IP restrictions, managing account settings
Account Registration
Step 1: Check Existing Account
Before registration, verify if the wallet already has an account:
const BROKER_ID = 'woofi_dex'; // Your broker ID
const walletAddress = '0x...'; // User's wallet address
const response = await fetch(
`https://testnet-api.orderly.org/v1/get_account?broker_id=${BROKER_ID}&user_address=${walletAddress}`
);
const data = await response.json();
// If data.success is true, account already exists
// If not, proceed with registration
Step 2: Fetch Registration Nonce
Retrieve a unique nonce required for registration (valid for 2 minutes):
const nonceResponse = await fetch('https://testnet-api.orderly.org/v1/registration_nonce');
const { data: nonce } = await nonceResponse.json();
console.log('Registration nonce:', nonce);
Step 3: Sign Registration Message
Create and sign an EIP-712 typed message:
// Registration Message Type
const REGISTRATION_TYPES = {
Registration: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'registrationNonce', type: 'uint256' },
],
};
// Create the message
const registerMessage = {
brokerId: BROKER_ID,
chainId: 421614,
timestamp: Date.now(),
registrationNonce: nonce,
};
// Sign with wallet (e.g., MetaMask) - Use OFFCHAIN_DOMAIN for registration
const signature = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [
walletAddress,
{
types: REGISTRATION_TYPES,
domain: OFFCHAIN_DOMAIN,
message: registerMessage,
primaryType: 'Registration',
},
],
});
Step 4: Submit Registration
Send the signed payload to create the Orderly Account ID:
const registerResponse = await fetch('https://testnet-api.orderly.org/v1/register_account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: registerMessage,
signature: signature,
userAddress: walletAddress,
}),
});
const result = await registerResponse.json();
console.log('Account ID:', result.data.account_id);
// Store this account ID - you'll need it for API authentication
API Key Management (Orderly Key)
Once you have an account, you need to register Ed25519 keys for API access.
Generate Ed25519 Key Pair
import { getPublicKeyAsync, utils } from '@noble/ed25519';
// Generate 32-byte private key (cryptographically secure)
const privateKey = utils.randomPrivateKey();
// Derive public key
const publicKey = await getPublicKeyAsync(privateKey);
// Encode public key as base58 (required by Orderly)
function encodeBase58(bytes: Uint8Array): string {
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let result = '';
let num = 0n;
for (const byte of bytes) {
num = num * 256n + BigInt(byte);
}
while (num > 0n) {
result = ALPHABET[Number(num % 58n)] + result;
num = num / 58n;
}
return result;
}
const orderlyKey = `ed25519:${encodeBase58(publicKey)}`;
// Convert bytes to hex string for storage
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
console.log('Orderly Key:', orderlyKey);
console.log('Private Key (hex):', bytesToHex(privateKey));
// STORE PRIVATE KEY SECURELY - NEVER SHARE IT
Sign Add Orderly Key Message
Associate the Ed25519 key with your account via EIP-712:
const ADD_KEY_TYPES = {
AddOrderlyKey: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'orderlyKey', type: 'string' },
{ name: 'scope', type: 'string' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'expiration', type: 'uint64' },
],
};
const addKeyMessage = {
brokerId: BROKER_ID,
chainId: 421614,
orderlyKey: orderlyKey,
scope: 'read,trading', // Permissions: read, trading, asset (comma-separated)
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year from now
};
// Use OFFCHAIN_DOMAIN for API key management
const addKeySignature = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [
walletAddress,
{
types: ADD_KEY_TYPES,
domain: OFFCHAIN_DOMAIN,
message: addKeyMessage,
primaryType: 'AddOrderlyKey',
},
],
});
Submit Orderly Key
Register the API key:
const keyResponse = await fetch('https://testnet-api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: addKeyMessage,
signature: addKeySignature,
userAddress: walletAddress,
}),
});
const keyResult = await keyResponse.json();
console.log('Key registered:', keyResult.success);
Orderly Key Scopes
When registering an API key, specify permissions:
| Scope | Permissions |
|---|---|
read |
Read positions, orders, balance |
trading |
Place, cancel, modify orders |
asset |
Deposit, withdraw, internal transfer |
Multiple scopes can be combined comma-separated: 'read,trading,asset'
Remove Orderly Key
To remove a key (requires Ed25519 authentication with another valid key):
// POST /v1/client/remove_orderly_key
const removeResponse = await signAndSendRequest(
accountId,
privateKey, // Must be a different valid key
'https://api.orderly.org/v1/client/remove_orderly_key',
{
method: 'POST',
body: JSON.stringify({
orderly_key: 'ed25519:...', // Key to remove
}),
}
);
Solana Wallet Authentication
Solana wallets use native Ed25519 message signing (not EIP-712) for account operations. Solana wallets already use Ed25519 keys natively, making the signing process simpler but requiring different message formatting.
Solana vs EVM Authentication
| Aspect | EVM Wallets | Solana Wallets |
|---|---|---|
| Signing Method | EIP-712 typed data | Plain message signing |
| Key Type | secp256k1 | Ed25519 (native) |
| Account Lookup | /v1/get_account |
/v1/get_account?chain_type=SOL |
| Message Format | Structured JSON types | Raw bytes via adapter |
| Signature | Ethereum signature | Ed25519 signature |
Account Lookup
Check if a Solana wallet already has an Orderly account:
import { PublicKey } from '@solana/web3.js';
const BROKER_ID = 'woofi_dex';
const solanaAddress = '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU'; // Base58 address
// Solana accounts require chain_type=SOL parameter
const response = await fetch(
`https://testnet-api.orderly.org/v1/get_account?` +
`address=${solanaAddress}&` +
`broker_id=${BROKER_ID}&` +
`chain_type=SOL`
);
const data = await response.json();
// data.data.account_id contains the Orderly account ID
// Account ID format is different from EVM (not a keccak256 hash)
Message Signing with Solana Adapter
Orderly provides a Solana adapter to generate properly formatted messages:
import { DefaultSolanaWalletAdapter } from '@orderly.network/default-solana-adapter';
import { Connection, clusterApiUrl, Keypair } from '@solana/web3.js';
import { signAsync } from '@noble/ed25519';
import bs58 from 'bs58';
// Setup wallet adapter
const walletAdapter = new DefaultSolanaWalletAdapter();
// Initialize with wallet details
walletAdapter.active({
address: solanaAddress,
provider: {
connection: new Connection(clusterApiUrl('devnet')), // or 'mainnet-beta'
signMessage: async (msg: Uint8Array) => {
// Sign with Solana wallet (Ed25519)
return await signAsync(msg, privateKeyBytes.slice(0, 32));
},
sendTransaction: async (tx, conn) => {
tx.sign([senderKeypair]);
return conn.sendTransaction(tx);
},
},
chain: {
id: network === 'mainnet' ? 900900900 : 901901901, // Solana chain IDs
},
});
Registration Flow
Step 1: Fetch Registration Nonce
const nonceResponse = await fetch('https://testnet-api.orderly.org/v1/registration_nonce');
const { data: nonce } = await nonceResponse.json();
Step 2: Generate and Sign Registration Message
// Generate registration message using adapter
const registerMessage = await walletAdapter.generateRegisterMessage({
brokerId: BROKER_ID,
timestamp: Date.now(),
registrationNonce: nonce,
});
// Sign with Solana wallet (raw message bytes, not EIP-712)
const signature = await wallet.signMessage(registerMessage.message);
Step 3: Submit Registration
const registerResponse = await fetch('https://testnet-api.orderly.org/v1/register_account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: registerMessage.message,
signature: signature,
userAddress: solanaAddress,
chainType: 'SOL', // Required for Solana
}),
});
const result = await registerResponse.json();
console.log('Account ID:', result.data.account_id);
API Key Management (Orderly Key)
Generate Ed25519 Key Pair
Same as EVM - locally generate an Ed25519 key pair:
import { getPublicKeyAsync, utils } from '@noble/ed25519';
import bs58 from 'bs58';
// Generate key pair
const privateKey = utils.randomPrivateKey();
const publicKey = await getPublicKeyAsync(privateKey);
const orderlyKey = `ed25519:${bs58.encode(publicKey)}`;
Sign Add Orderly Key Message
// Generate add key message using adapter
const addKeyMessage = await walletAdapter.generateAddKeyMessage({
brokerId: BROKER_ID,
orderlyKey: orderlyKey,
scope: 'read,trading',
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year
});
// Sign with Solana wallet
const signature = await wallet.signMessage(addKeyMessage.message);
Submit Orderly Key
const keyResponse = await fetch('https://testnet-api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: addKeyMessage.message,
signature: signature,
userAddress: solanaAddress,
chainType: 'SOL',
}),
});
Withdrawal Signing
Withdrawals require wallet signature on both EVM and Solana:
// Fetch withdraw nonce
const nonceRes = await fetch(`${BASE_URL}/v1/withdraw_nonce`);
const {
data: { withdraw_nonce },
} = await nonceRes.json();
// Generate withdraw message
const withdrawMessage = await walletAdapter.generateWithdrawMessage({
brokerId: BROKER_ID,
receiver: solanaAddress,
token: 'USDC',
amount: '1000',
timestamp: Date.now(),
nonce: Number(withdraw_nonce),
});
// Sign with Solana wallet
const signature = await wallet.signMessage(withdrawMessage.message);
// Submit withdrawal request
const res = await fetch(`${BASE_URL}/v1/withdraw_request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: withdrawMessage.message,
signature: signature,
userAddress: solanaAddress,
verifyingContract: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203', // Mainnet
// verifyingContract: '0x1826B75e2ef249173FC735149AE4B8e9ea10abff', // Testnet
}),
});
Settle PnL Signing
// Fetch settle nonce
const nonceRes = await fetch(`${BASE_URL}/v1/settle_nonce`);
const {
data: { settle_nonce },
} = await nonceRes.json();
// Generate settle message
const settleMessage = await walletAdapter.generateSettleMessage({
brokerId: BROKER_ID,
timestamp: Date.now(),
settlePnlNonce: settle_nonce,
});
// Sign with Solana wallet
const signature = await wallet.signMessage(settleMessage.message);
// Submit settle request
const res = await fetch(`${BASE_URL}/v1/settle_pnl`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: settleMessage.message,
signature: signature,
userAddress: solanaAddress,
verifyingContract: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203',
}),
});
Solana-Specific Configuration
| Environment | Solana Chain ID | Solana Cluster | Orderly Vault Address | Verifying Contract |
|---|---|---|---|---|
| Mainnet | 900900900 | mainnet-beta |
ErBmAD61mGFKvrFNaTJuxoPwqrS8GgtwtqJTJVjFWx9Q |
0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203 |
| Testnet | 901901901 | devnet |
9shwxWDUNhtwkHocsUAmrNAQfBH2DHh4njdAEdHZZkF2 |
0x1826B75e2ef249173FC735149AE4B8e9ea10abff |
Note: API base URLs are the same for EVM and Solana. See the Environment Configuration section at the top of this skill.
Important Differences
Account ID Generation
- EVM:
keccak256(address, keccak256(brokerId)) - Solana: Returned from
/v1/get_accountAPI (not a hash)
Message Signing
- EVM: Uses
eth_signTypedData_v4with structured EIP-712 types - Solana: Uses raw message bytes signed with Ed25519
No Domain Separator
Solana doesn't use EIP-712 domain configuration:
// EVM - requires domain
domain: {
name: 'Orderly',
version: '1',
chainId: 42161,
verifyingContract: '0x...',
}
// Solana - no domain, just raw message
const message = await walletAdapter.generateRegisterMessage({...});
Part 2: Ed25519 API Authentication
Once you have registered an Ed25519 key via wallet signing (EIP-712 for EVM or Ed25519 message signing for Solana), you use that key for all API operations.
Required Headers
| Header | Description |
|---|---|
orderly-timestamp |
Unix timestamp in milliseconds |
orderly-account-id |
Your Orderly account ID |
orderly-key |
Your public key prefixed with ed25519: |
orderly-signature |
Base64url-encoded Ed25519 signature |
Generating Ed25519 Key Pair
import { getPublicKeyAsync, utils } from '@noble/ed25519';
// Generate private key (32 cryptographically secure random bytes)
const privateKey = utils.randomPrivateKey();
// Derive public key
const publicKey = await getPublicKeyAsync(privateKey);
// Encode public key as base58 (required by Orderly)
function encodeBase58(bytes: Uint8Array): string {
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const BASE = 58n;
let num = 0n;
for (const byte of bytes) {
num = num * 256n + BigInt(byte);
}
let result = '';
while (num > 0n) {
result = ALPHABET[Number(num % BASE)] + result;
num = num / BASE;
}
// Handle leading zeros
for (const byte of bytes) {
if (byte === 0) {
result = '1' + result;
} else {
break;
}
}
return result;
}
const orderlyKey = `ed25519:${encodeBase58(publicKey)}`;
// Convert bytes to hex string (browser & Node.js compatible)
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
console.log('Private Key (hex):', bytesToHex(privateKey));
console.log('Public Key (base58):', orderlyKey);
// STORE PRIVATE KEY SECURELY - NEVER SHARE IT
Signing Requests
Message Construction
function buildSignMessage(timestamp: number, method: string, path: string, body?: string): string {
// Message format: timestamp + method + path + body
// Note: No spaces or separators between parts
return `${timestamp}${method}${path}${body || ''}`;
}
// Examples
const timestamp = Date.now();
// GET request (no body)
const getMessage = buildSignMessage(timestamp, 'GET', '/v1/positions');
// POST request (with body)
const body = JSON.stringify({
symbol: 'PERP_ETH_USDC',
side: 'BUY',
order_type: 'LIMIT',
order_price: '3000',
order_quantity: '0.1',
});
const postMessage = buildSignMessage(timestamp, 'POST', '/v1/order', body);
Creating the Signature
import { signAsync } from '@noble/ed25519';
async function signRequest(
timestamp: number,
method: string,
path: string,
body: string | undefined,
privateKey: Uint8Array
): Promise<string> {
const message = buildSignMessage(timestamp, method, path, body);
// Sign with Ed25519
const signatureBytes = await signAsync(new TextEncoder().encode(message), privateKey);
// Encode as base64url (NOT base64)
// Convert to base64, then make it URL-safe
const base64 = btoa(String.fromCharCode(...signatureBytes));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
Sign and Send Request Helper
For a simple, standalone authentication helper that always works correctly with query parameters and proper Content-Type headers:
import { getPublicKeyAsync, signAsync } from '@noble/ed25519';
import { encodeBase58 } from 'ethers';
export async function signAndSendRequest(
orderlyAccountId: string,
privateKey: Uint8Array | string,
input: URL | string,
init?: RequestInit | undefined
): Promise<Response> {
const timestamp = Date.now();
const encoder = new TextEncoder();
const url = new URL(input);
let message = `${String(timestamp)}${init?.method ?? 'GET'}${url.pathname}${url.search}`;
if (init?.body) {
message += init.body;
}
const orderlySignature = await signAsync(encoder.encode(message), privateKey);
return fetch(input, {
headers: {
'Content-Type':
init?.method !== 'GET' && init?.method !== 'DELETE'
? 'application/json'
: 'application/x-www-form-urlencoded',
'orderly-timestamp': String(timestamp),
'orderly-account-id': orderlyAccountId,
'orderly-key': `ed25519:${encodeBase58(await getPublicKeyAsync(privateKey))}`,
'orderly-signature': Buffer.from(orderlySignature).toString('base64url'),
...(init?.headers ?? {}),
},
...(init ?? {}),
});
}
This helper function:
- Properly parses the URL to extract both pathname and search (query) parameters
- Correctly sets Content-Type based on HTTP method (GET/DELETE use
application/x-www-form-urlencoded, others useapplication/json) - Constructs the signature message with timestamp + method + pathname + search + body
- Returns the fetch response for further processing
Usage Examples
const baseUrl = 'https://api.orderly.org';
const accountId = '0x123...';
const privateKey = new Uint8Array(32); // Your private key
// GET request with query parameters
const positions = await signAndSendRequest(accountId, privateKey, `${baseUrl}/v1/positions`);
const positionsData = await positions.json();
// GET request with query params
const orders = await signAndSendRequest(
accountId,
privateKey,
`${baseUrl}/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE`
);
const ordersData = await orders.json();
// POST request with body
const order = await signAndSendRequest(accountId, privateKey, `${baseUrl}/v1/order`, {
method: 'POST',
body: JSON.stringify({
symbol: 'PERP_ETH_USDC',
side: 'BUY',
order_type: 'LIMIT',
order_price: '3000',
order_quantity: '0.1',
}),
});
const orderData = await order.json();
// DELETE request
const cancel = await signAndSendRequest(
accountId,
privateKey,
`${baseUrl}/v1/order?order_id=123&symbol=PERP_ETH_USDC`,
{ method: 'DELETE' }
);
Error Handling Helper
class OrderlyApiError extends Error {
code: number;
details: any;
constructor(response: any) {
super(response.message || 'API Error');
this.code = response.code;
this.details = response;
}
}
// Usage with error handling
async function apiRequest(
accountId: string,
privateKey: Uint8Array,
url: string,
init?: RequestInit
) {
const response = await signAndSendRequest(accountId, privateKey, url, init);
const result = await response.json();
if (!result.success) {
throw new OrderlyApiError(result);
}
return result.data;
}
Query Parameters
Query parameters must be included in the signature message. The URL is parsed to extract both pathname and search parameters:
// Correct - query params are parsed from the URL
const url = new URL('/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE', baseUrl);
// Message: timestamp + method + pathname + search
// Result: "1234567890123GET/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE"
// Wrong - query params added separately after signing
const path = '/v1/orders';
const signature = await sign(timestamp, 'GET', path);
const url = `${path}?symbol=PERP_ETH_USDC`; // Signature mismatch!
Common Errors
Signature Mismatch (Code 10016)
Cause: Signature doesn't match expected value
Check:
1. Message format: timestamp + method + path + body (no spaces)
2. Method is uppercase: GET, POST, DELETE, PUT
3. Path includes query parameters
4. Body is exact JSON string (same whitespace)
5. Signature is base64url encoded (not base64)
Timestamp Expired (Code 10017)
Cause: Timestamp is too old or too far in the future
Solution:
- Ensure server clock is synchronized
- Timestamp must be within ±30 seconds
- Generate timestamp immediately before signing
Invalid Orderly Key (Code 10019)
Cause: Public key format incorrect
Solution:
- Must be prefixed with 'ed25519:'
- Public key must be base58 encoded
- Key must be registered to account
Orderly Key Scopes
When registering an API key, specify permissions:
| Scope | Permissions |
|---|---|
read |
Read positions, orders, balance |
trading |
Place, cancel, modify orders |
asset |
Deposit, withdraw, internal transfer |
// When adding key via EIP-712 signing
const addKeyMessage = {
brokerId: 'woofi_dex',
chainId: 42161,
orderlyKey: 'ed25519:...',
scope: 'read,trading', // Multiple scopes comma-separated
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year
};
Security Best Practices
Store Private Keys Securely
// NEVER hardcode private keys
// BAD:
const privateKey = new Uint8Array([1, 2, 3, ...]);
// GOOD: Load from environment
const privateKeyHex = process.env.ORDERLY_PRIVATE_KEY;
// Convert hex string to Uint8Array (browser & Node.js compatible)
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
}
return bytes;
}
const privateKey = hexToBytes(privateKeyHex);
// BETTER: Use secure key management
// AWS KMS, HashiCorp Vault, etc.
Key Rotation
Rotate your API keys periodically for security:
// Generate new key pair
const newPrivateKey = utils.randomPrivateKey();
const newPublicKey = await getPublicKeyAsync(newPrivateKey);
// Register new key (requires wallet signature via EIP-712)
// POST /v1/orderly_key - No Ed25519 auth required
const orderlyKey = `ed25519:${encodeBase58(newPublicKey)}`;
const timestamp = Date.now();
const expiration = timestamp + 31536000000; // 1 year
const addKeyMessage = {
brokerId: 'your_broker_id',
chainId: 42161, // Arbitrum mainnet
orderlyKey: orderlyKey,
scope: 'read,trading', // Comma-separated scopes
timestamp: timestamp,
expiration: expiration,
};
// Sign with wallet (EIP-712)
const addKeySignature = await wallet.signTypedData({
domain: {
name: 'Orderly',
version: '1',
chainId: 42161,
verifyingContract: '0x...', // Contract address
},
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
AddOrderlyKey: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'orderlyKey', type: 'string' },
{ name: 'scope', type: 'string' },
{ name: 'timestamp', type: 'uint256' },
{ name: 'expiration', type: 'uint256' },
],
},
primaryType: 'AddOrderlyKey',
message: addKeyMessage,
});
const registerResponse = await fetch('https://api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: addKeyMessage,
signature: addKeySignature,
userAddress: walletAddress,
}),
});
const registerResult = await registerResponse.json();
if (!registerResult.success) {
throw new Error(`Failed to register key: ${registerResult.message}`);
}
// Update your application config
config.privateKey = newPrivateKey;
config.orderlyKey = orderlyKey;
// Remove old key using authenticated request
// POST /v1/client/remove_orderly_key - Requires Ed25519 auth
const oldOrderlyKey = `ed25519:${encodeBase58(oldPublicKey)}`;
const removeResponse = await signAndSendRequest(
accountId,
newPrivateKey, // Use the NEW key to authenticate
'https://api.orderly.org/v1/client/remove_orderly_key',
{
method: 'POST',
body: JSON.stringify({
orderly_key: oldOrderlyKey,
}),
}
);
const removeResult = await removeResponse.json();
if (!removeResult.success) {
throw new Error(`Failed to remove old key: ${removeResult.message}`);
}
IP Restrictions
// Set IP restriction for key
POST /v1/client/set_orderly_key_ip_restriction
Body: {
orderly_key: 'ed25519:...',
ip_list: ['1.2.3.4', '5.6.7.8'],
}
// Get current restrictions
GET /v1/client/orderly_key_ip_restriction?orderly_key={key}
// Reset (remove) restrictions
POST /v1/client/reset_orderly_key_ip_restriction
Body: { orderly_key: 'ed25519:...' }
WebSocket Authentication
WebSocket also requires Ed25519 authentication:
const ws = new WebSocket(`wss://ws-private-evm.orderly.org/v2/ws/private/stream/${accountId}`);
ws.onopen = async () => {
const timestamp = Date.now();
const message = timestamp.toString();
const signature = await signAsync(new TextEncoder().encode(message), privateKey);
// Convert to base64url (browser & Node.js compatible)
const base64 = btoa(String.fromCharCode(...signature));
const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
ws.send(
JSON.stringify({
id: 'auth_1',
event: 'auth',
params: {
orderly_key: orderlyKey,
sign: base64url,
timestamp: timestamp,
},
})
);
};
Testing Authentication
// Verify key is valid
GET /v1/get_orderly_key?orderly_key={key}
// Response
{
"success": true,
"data": {
"account_id": "0x...",
"valid": true,
"scope": "read,trading",
"expires_at": 1735689600000
}
}
Supported Chains
| Chain | Chain ID | Mainnet | Testnet |
|---|---|---|---|
| Arbitrum | 42161 / 421614 | ✅ | ✅ |
| Optimism | 10 / 11155420 | ✅ | ✅ |
| Base | 8453 / 84532 | ✅ | ✅ |
| Ethereum | 1 / 11155111 | ✅ | ✅ |
| Solana | 900900900 / 901901901 | ✅ | ✅ |
| Mantle | 5000 / 5003 | ✅ | ✅ |
Common Issues
EIP-712 Errors
"Nonce expired" error
- Nonces are valid for 2 minutes only
- Fetch a new nonce and retry
"Account already exists" error
- The wallet is already registered with this broker
- Use
/v1/get_accountto retrieve existing account info
"Invalid signature" error
- Ensure the EIP-712 domain matches exactly (name, version, chainId, verifyingContract)
- Check chain ID matches your network
- Verify the message structure matches the types
- Use
eth_signTypedData_v4noteth_signTypedData
Ed25519 Errors
Signature Mismatch (Code 10016)
Cause: Signature doesn't match expected value
Check:
1. Message format: timestamp + method + path + body (no spaces)
2. Method is uppercase: GET, POST, DELETE, PUT
3. Path includes query parameters
4. Body is exact JSON string (same whitespace)
5. Signature is base64url encoded (not base64)
Timestamp Expired (Code 10017)
Cause: Timestamp is too old or too far in the future
Solution:
- Ensure server clock is synchronized
- Timestamp must be within ±30 seconds
- Generate timestamp immediately before signing
Invalid Orderly Key (Code 10019)
Cause: Public key format incorrect
Solution:
- Must be prefixed with 'ed25519:'
- Public key must be base58 encoded
- Key must be registered to account
Authentication Comparison
| Aspect | EIP-712 Wallet Auth | Ed25519 API Auth |
|---|---|---|
| Purpose | Account operations, key management | Trading, reading data |
| Signer | User's Web3 wallet | Locally-generated Ed25519 key |
| Key type | Ethereum private key | Ed25519 key pair |
| Endpoints | /v1/register_account, /v1/orderly_key |
All other endpoints |
| Signature type | EIP-712 typed data | Raw Ed25519 + base64url |
| Scope | Create/manage API keys | Use API keys for trading |
Related Skills
- orderly-trading-orders - Using authenticated endpoints
- orderly-websocket-streaming - WebSocket authentication
- orderly-sdk-react-hooks - React SDK for simplified auth
- orderly-deposit-withdraw - Fund your account