Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pacifica-fi/docs-migrate/llms.txt

Use this file to discover all available pages before exploring further.

Pacifica authenticates every state-changing request using Ed25519 cryptographic signatures. GET requests and WebSocket subscriptions do not require signing — only POST requests that modify state (placing orders, managing positions, withdrawals, etc.) must carry a valid signature. This approach ties each request directly to the wallet that owns the account, with no API secret stored on a server.
Never expose your Ed25519 private key in client-side code, logs, environment variables committed to source control, or any other location accessible to third parties. Treat it with the same care as a seed phrase.
The official Pacifica Python SDK handles all signing steps automatically and is the recommended path for most integrations. The sections below explain the underlying mechanics for developers who need to implement signing in another language or want to understand exactly what is being signed.

Signing at a Glance

from pacifica import PacificaClient

client = PacificaClient(
    private_key="<your-ed25519-private-key>",
    account="<your-wallet-address>"
)
Instantiating the client with your private key and account address is all that is needed — the SDK constructs and attaches signatures transparently on every call.

How Signing Works

1

Assemble the signature header and payload

Every signed request starts with a signature header that identifies the operation type, carries the current timestamp in milliseconds, and optionally specifies an expiry window:
import time

timestamp = int(time.time() * 1_000)

signature_header = {
    "timestamp": timestamp,
    "expiry_window": 5_000,   # optional; defaults to 30_000 ms if omitted
    "type": "create_order",   # operation type — see table below
}
Combine the header with the operation payload under a "data" key:
operation_data = {
    "symbol": "BTC",
    "price": "100000",
    "amount": "0.1",
    "side": "bid",
    "tif": "GTC",
    "reduce_only": False,
    "client_order_id": "12345678-1234-1234-1234-123456789abc",
}

data_to_sign = {
    **signature_header,
    "data": operation_data,
}
2

Recursively sort all JSON keys

Deterministic serialisation requires that every key at every nesting level is sorted alphabetically before the message is serialised. This guarantees that two logically identical payloads always produce an identical byte sequence — and therefore an identical signature.
def sort_json_keys(value):
    if isinstance(value, dict):
        return {k: sort_json_keys(value[k]) for k in sorted(value.keys())}
    elif isinstance(value, list):
        return [sort_json_keys(item) for item in value]
    return value

sorted_message = sort_json_keys(data_to_sign)
After sorting, the example payload becomes:
{
  "data": {
    "amount": "0.1",
    "client_order_id": "12345678-1234-1234-1234-123456789abc",
    "price": "100000",
    "reduce_only": false,
    "side": "bid",
    "symbol": "BTC",
    "tif": "GTC"
  },
  "expiry_window": 5000,
  "timestamp": 1748970123456,
  "type": "create_order"
}
3

Serialise to compact JSON

Produce a compact JSON string with no whitespace and standardised separators:
import json

compact_json = json.dumps(sorted_message, separators=(",", ":"))
The resulting string looks like:
{"data":{"amount":"0.1","client_order_id":"12345678-1234-1234-1234-123456789abc","price":"100000","reduce_only":false,"side":"bid","symbol":"BTC","tif":"GTC"},"expiry_window":5000,"timestamp":1748970123456,"type":"create_order"}
4

Sign with Ed25519 and encode as Base58

Encode the compact JSON string as UTF-8 bytes, sign with the Ed25519 private key, and Base58-encode the resulting signature bytes for transmission:
import base58
from solders.keypair import Keypair

keypair = Keypair.from_bytes(base58.b58decode(PRIVATE_KEY))

message_bytes = compact_json.encode("utf-8")
signature = keypair.sign_message(message_bytes)
signature_b58 = base58.b58encode(bytes(signature)).decode("ascii")
# e.g. "5j1Vy9UqYUF2jKD9r2Lv5AoMWHJuW5a1mqVzEhC9SJL5GqbPkGEQKpW3UZmKXr4UWrHMJ5xHQFMJkZWE8J5VyA"
5

Build and submit the final request

Construct the final request body by merging the authentication fields with the original operation data (not the "data"-wrapped version used for signing):
final_request = {
    "account": str(keypair.pubkey()),
    "agent_wallet": None,
    "signature": signature_b58,
    "timestamp": signature_header["timestamp"],
    "expiry_window": signature_header["expiry_window"],
    # Original operation fields — not wrapped in "data":
    **operation_data,
}
Submit with a standard POST:
import requests

response = requests.post(
    "https://api.pacifica.fi/api/v1/orders/create",
    json=final_request,
    headers={"Content-Type": "application/json"},
)

Operation Types

Every signed request includes a "type" field in the signature header that identifies the operation being performed. The server uses this to reconstruct and verify the signed message.
Operation TypeAPI Endpoint
"create_order"POST /api/v1/orders/create
"create_stop_order"POST /api/v1/orders/stop/create
"cancel_order"POST /api/v1/orders/cancel
"cancel_all_orders"POST /api/v1/orders/cancel_all
"cancel_stop_order"POST /api/v1/orders/stop/cancel
"update_leverage"POST /api/v1/account/leverage
"update_margin_mode"POST /api/v1/account/margin
"set_position_tpsl"POST /api/v1/positions/tpsl
"withdraw"POST /api/v1/account/withdraw
"subaccount_initiate"POST /api/v1/account/subaccount/create
"subaccount_confirm"POST /api/v1/account/subaccount/create
"create_market_order"POST /api/v1/orders/create_market
"subaccount_transfer"POST /api/v1/account/subaccount/transfer
"bind_agent_wallet"POST /api/v1/agent/bind
"create_api_key"POST /api/v1/account/api_keys/create
"revoke_api_key"POST /api/v1/account/api_keys/revoke
"list_api_keys"POST /api/v1/account/api_keys
"create_lake"POST /api/v1/lake/create
"claim_lake_referral"POST /api/v1/lake/claim_referral_code
"deposit_to_lake"POST /api/v1/lake/deposit
"claim_lake_manager"POST /api/v1/lake/claim_manager
"withdraw_from_lake"POST /api/v1/lake/withdraw
"update_lake_deposit_cap"POST /api/v1/lake/update_deposit_cap
"add_lake_whitelist"POST /api/v1/lake/add_whitelist
"remove_lake_whitelist"POST /api/v1/lake/remove_whitelist
"add_lake_blacklist"POST /api/v1/lake/add_blacklist
"remove_lake_blacklist"POST /api/v1/lake/remove_blacklist
"add_lake_max_leverage"POST /api/v1/lake/add_max_leverage
"remove_lake_max_leverage"POST /api/v1/lake/remove_max_leverage
The batch order endpoint does not have its own operation type. Each individual action inside the batch is signed independently using its own operation type (e.g. "create_order", "cancel_order").

Request Fields Reference

account
string
required
The public key of the wallet that owns the account — always the main account’s public key, even when using an agent key.
signature
string
required
Base58-encoded Ed25519 signature over the deterministic compact JSON message. When using a hardware wallet, pass an object with "type": "hardware" and "value": "<base58-sig>" instead of a plain string.
timestamp
integer
required
Current Unix timestamp in milliseconds. The server rejects requests where timestamp + expiry_window < server_time.
expiry_window
integer
How long (in milliseconds) the signed message remains valid after timestamp. Defaults to 30000 (30 seconds) if omitted. Use a smaller value (e.g. 5000) to reduce replay risk.
agent_wallet
string
Public key of the API agent key being used to sign this request. Set to null when signing with the main account key directly.

API Agent Keys

API agent keys (also called “agent wallets”) allow a sub-key to sign requests on behalf of the main account. This mirrors the API key model used by most centralised exchanges and means the main wallet’s private key never needs to touch the trading program.
1

Generate an agent key

Agent keys can be created through the Pacifica web app or programmatically via the Python SDK:
# See: https://github.com/pacifica-fi/python-sdk/blob/main/rest/api_agent_keys.py
Each account supports up to 5 active API config keys at one time.
2

Use the agent key to sign

When constructing a POST request with an agent key:
  • Set "account" to the main account’s public key
  • Sign the message using the agent key’s private key
  • Set "agent_wallet" to the agent key’s public key
final_request = {
    "account": main_wallet_pubkey,       # main account address
    "agent_wallet": agent_wallet_pubkey, # agent key address
    "signature": agent_signed_sig_b58,   # signed with agent private key
    "timestamp": timestamp,
    **operation_data,
}
Agent keys work for both REST and WebSocket authenticated operations. Refer to the Python SDK example for a full market-order placement using an agent key.

Hardware Wallet Support

Pacifica supports hardware wallet signing via Ed25519 off-chain message signing. After constructing the compact JSON message bytes (step 4 above), prepend the Solana off-chain message header (\xffsolana offchain followed by version and length information) before signing. In the request body, pass the signature as an object rather than a plain string:
{
  "account": "6ETnufiec2CxVWTS4u5Wiq33Zh5Y3Qm6Pkdpi375fuxP",
  "signature": {
    "type": "hardware",
    "value": "2V4Y7Mpk..."
  },
  "timestamp": 1748970123456
}
See the Python SDK hardware wallet example for the full header construction.

Signing Error Reference

When a POST request carries an invalid signature, the server returns HTTP 400 with one of the following messages:
The signature field could not be parsed or validated:
  • Not valid Base58 encoding
  • Decoded bytes do not form a valid Ed25519 signature
  • Malformed signature data
The signed message itself is structurally invalid or has expired:
  • timestamp + expiry_window < current_server_time (message expired)
  • Payload cannot be serialised to JSON
  • Message structure is malformed
The account field does not represent a valid Ed25519 public key:
  • Address does not decode to a valid 32-byte public key
  • Public key bytes are malformed
The signature is structurally valid but does not match the reconstructed message:
  • Signature was produced by a different private key
  • Wrong operation type or payload was used during signing
  • Message content was modified between signing and submission
Signing errors can be ambiguous to debug. Following the step-by-step implementation above precisely — or using the Python SDK — avoids the vast majority of signing issues.

Build docs developers (and LLMs) love