Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Agent keys

An agent key is a keypair you authorize to trade on your account. Your bot holds the private key and signs its own transactions, submitting them straight to the validator.

This is the fastest and the recommended way to trade programmatically. Compared to an API key, an agent key skips the enclave-signing hop entirely, and the key never leaves your infrastructure.

Which should I use?

Agent keyAPI key
Who signsYour bot, locallyA secure enclave holding your key
Key locationYour infrastructureAttested secure enclave
SubmitPOST /v1/tx (validator, direct)POST /tx/sign-and-submit (REST API)
LatencyLowestHigher (extra signing hop)
Best forMarket-making, EMS/venue adapters, latency-sensitive botsWebhooks, no-code tools, simple scripts that can't sign locally
Can withdrawNoNo

Neither path can withdraw funds off-platform. An API key can still transfer funds inside an account family and interact with vaults. Withdrawals require the authenticated owner flow in the trading app. If your integration can hold a key and sign locally, use an agent key.

Capabilities

An agent can PlaceOrder, CancelOrder, CancelAllOrders, ModifyOrder, and AdjustIsolatedMargin. It cannot deposit, withdraw, transfer, or manage other agent keys. Those require your account owner key.

A leaked agent key can submit malicious trades and lose account collateral. The validator rejects transfers and withdrawals signed by an agent key.

Scope

Agent authorization is family-inherited:

  • An agent approved on a master account can trade the master and every subaccount under it.
  • An agent approved on a specific subaccount can trade only that subaccount.

So a market maker running one book approves an agent on the master and trades the whole family. A desk that wants a key scoped to a single strategy approves it on that subaccount.

Approving an agent

Generate an ed25519 keypair, then authorize its public key with an ApproveAgent transaction signed by your account owner key. The mainnet and devnet Profile → Keys pages can generate the keypair and submit the transaction for you. The UI shows the private key once.

Programmatically, the owner submits ApproveAgent:

{
  "ApproveAgent": {
    "account_id": 27,
    "agent": [
      /* 32-byte ed25519 public key */
    ],
    "name": "MM bot 1",
    "expires_at": 1780000000,
  },
}

name and expires_at (integer unix seconds) are optional. The agent remains active strictly before that second and is inactive at or after it. Millisecond timestamps are invalid. Re-approving the same agent updates its name and expiry in place. An expired agent stops trading but stays in the list until revoked.

Trading with an agent key

Your bot signs the exact transaction JSON bytes that it submits to the validator's public /v1/tx endpoint. Mainnet uses https://validator.v4.parcl.co with chain ID parcl-v4:mainnet:chain-1. The example below uses devnet at https://v4-api.dev.parcllabs.com with chain ID parcl-v4:devnet:chain-2.

Set PARCL_AGENT_PRIVATE_KEY_HEX to the 32-byte private-key seed and PARCL_ACCOUNT_ID to the approved account.

TypeScript
// npm install @noble/ed25519
import * as ed25519 from "@noble/ed25519";
 
const validatorUrl =
  process.env.PARCL_VALIDATOR_URL ?? "https://v4-api.dev.parcllabs.com";
const chainId = "parcl-v4:devnet:chain-2";
const privateKeyHex = process.env.PARCL_AGENT_PRIVATE_KEY_HEX ?? "";
const accountId = Number(process.env.PARCL_ACCOUNT_ID);
if (
  !/^[0-9a-f]{64}$/i.test(privateKeyHex) ||
  !Number.isSafeInteger(accountId)
) {
  throw new Error("Set PARCL_AGENT_PRIVATE_KEY_HEX and PARCL_ACCOUNT_ID");
}
 
const privateKey = Uint8Array.from(privateKeyHex.match(/../g)!, (byte) =>
  Number.parseInt(byte, 16),
);
const signer = await ed25519.getPublicKeyAsync(privateKey);
const nodeInfo = (await fetch(`${validatorUrl}/v1/node/info`).then((response) =>
  response.json(),
)) as { chain_id?: string };
if (nodeInfo.chain_id !== chainId)
  throw new Error("Validator chain ID mismatch");
 
const transaction = {
  PlaceOrder: {
    account_id: accountId,
    market_id: 0,
    side: "Long",
    order_type: "Limit",
    price: 28000000000,
    size: 100000,
    trigger_price: null,
    reduce_only: false,
    post_only: true,
    time_in_force: "GTC",
    take_profit: null,
    stop_loss: null,
  },
};
const nonce = Date.now();
const timestamp = Math.floor(nonce / 1000);
const message = new TextEncoder().encode(
  JSON.stringify([chainId, transaction, nonce, timestamp]),
);
const signature = await ed25519.signAsync(message, privateKey);
 
const response = await fetch(`${validatorUrl}/v1/tx`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    transaction,
    chain_id: chainId,
    signer: Array.from(signer),
    signature: Array.from(signature),
    nonce,
    timestamp,
  }),
});
console.log(response.status, await response.json());

The validator verifies the signature against signer, confirms the agent has approval on account_id (directly or via its master), and applies the order. The response carries the resulting events. A valid signature for another chain ID returns HTTP 400. See Transactions for the signing-bytes format and the full set of transaction shapes.

Do not reconstruct or normalize transaction after signing. Changing its field order, whitespace, keys, or values changes the signed bytes. The validator parses the request before signature verification. A malformed transaction returns HTTP 400.

Revoking an agent

Submit RevokeAgent, signed by the account owner, or revoke it from the mainnet or devnet Profile → Keys page:

{
  "RevokeAgent": {
    "account_id": 27,
    "agent": [
      /* 32-byte pubkey */
    ],
  },
}

The agent can no longer trade once the transaction confirms. Revocation does not affect open positions or resting orders.

Listing an account's agents

GET /v1/accounts/{id} returns the account's approved agents:

{
  "accountId": 27,
  "agents": [
    { "pubkey": "0xabc…", "name": "MM bot 1", "expiresAt": 1780000000 }
  ]
}