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

# API Authentication

Private requests use an Ed25519 API signing key authorized by your wallet. After key creation, ordinary account reads and trades do not require a fresh wallet signature. Never substitute a wallet private key for an API secret.

## Signing format

Concatenate, with no separators:

```
timestamp_in_milliseconds + UPPERCASE_METHOD + pathname_and_query + exact_body_if_present
```

Sign the UTF-8 bytes using Ed25519 and encode the signature as base64url. Include query parameters in exactly the same order/encoding as the outgoing URL. Serialize JSON once; sign and send the same string.

| Header               | Value                                                                         |
| -------------------- | ----------------------------------------------------------------------------- |
| `orderly-account-id` | Trading account ID                                                            |
| `orderly-key`        | `ed25519:` plus base58 public key                                             |
| `orderly-timestamp`  | Timestamp in milliseconds                                                     |
| `orderly-signature`  | Base64url Ed25519 signature                                                   |
| `Content-Type`       | GET/DELETE: `application/x-www-form-urlencoded`; POST/PUT: `application/json` |

These protocol field names cannot be rebranded. The secret is used locally and is never transmitted as a header. Keep your clock synchronized and generate a new timestamp/signature for each request. The current protocol reference documents a 300-second timestamp tolerance; do not rely on that window for execution timing.

## Read-only Node.js example

Requires Node.js 20+. In a separate private integration project:

```bash
npm install @noble/curves@1.9.7 @scure/base@1.2.6
```

Save as `read-account.mjs`. Supply `FLUVION_ACCOUNT_ID`, `FLUVION_API_KEY`, `FLUVION_API_SECRET` via a secure environment. Do not put literal credentials in this file. Set `FLUVION_NETWORK=mainnet` explicitly for production; otherwise it defaults to testnet.

```javascript
import { ed25519 } from '@noble/curves/ed25519';
import { base58 } from '@scure/base';

const network = process.env.FLUVION_NETWORK ?? 'testnet';
if (!['mainnet', 'testnet'].includes(network)) throw new Error('Invalid network');
const base = network === 'mainnet'
  ? 'https://api.orderly.org'
  : 'https://testnet-api.orderly.org';
const account = process.env.FLUVION_ACCOUNT_ID;
const expectedKey = process.env.FLUVION_API_KEY;
const encodedSecret = process.env.FLUVION_API_SECRET;
if (!account || !expectedKey || !encodedSecret) throw new Error('Missing credentials');
const secret = base58.decode(encodedSecret.replace(/^ed25519:/, ''));
try {
  if (secret.length !== 32) throw new Error('Expected a 32-byte API seed');
  const key = `ed25519:${base58.encode(ed25519.getPublicKey(secret))}`;
  if (key !== expectedKey) throw new Error('API key and secret do not match');
  const path = '/v1/positions';
  const timestamp = String(Date.now());
  const signature = ed25519.sign(
    new TextEncoder().encode(`${timestamp}GET${path}`), secret,
  );
  const response = await fetch(base + path, {
    method: 'GET', redirect: 'error', signal: AbortSignal.timeout(15000),
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'orderly-account-id': account,
      'orderly-key': key,
      'orderly-timestamp': timestamp,
      'orderly-signature': Buffer.from(signature).toString('base64url'),
    },
  });
  const result = await response.json();
  if (!response.ok || result.success !== true) {
    throw new Error(`API failed: HTTP ${response.status}, code ${result.code ?? 'unknown'}`);
  }
  console.log({
    success: true,
    openPositions: result.data.rows.filter(row => Number(row.position_qty) !== 0).length,
  });
} finally {
  secret.fill(0);
}
```

Zeroing the decoded byte array is best effort, not a guarantee of wiping JavaScript strings from memory. This example deliberately does not log account details or place orders.
