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

Transactions

Every write to Parcl Chain is a signed transaction carrying exactly one action. There is no atomic multi-instruction batching.

Signing runs inside a secure enclave. The enclave generates and holds your private key. Parcl never sees the plaintext key. Key export requires an authenticated browser session and is blocked for API keys (see authentication). The enclave signs each transaction in response to an authenticated request from you: your trading UI session or your API key. Without that authentication nobody can produce a signature for your account, including Parcl.

Submitting a transaction

Two ways to authorize the enclave to sign:

  1. Trading UI. Log in at devnet.v4.parcl.co. Your authenticated session authorizes the enclave to sign the transactions you submit through the app. This is the default flow for interactive trading.
  2. API key. Send the unsigned transaction body to POST /tx/sign-and-submit with your X-API-Key header. The key is your authorization token. See Authentication for key generation, scope, and rate limits.

Request body

When submitting via POST /tx/sign-and-submit:

{
  "transaction": { "PlaceOrder": { ... } },
  "nonce": 1713456789000,
  "timestamp": 1713456789
}
FieldWho sets itDescription
transactionYouThe action. Exactly one variant from the list below.
nonceYouu64 unix-milliseconds, strictly increasing per signer, within ~1 day of chain time. See Nonces.
timestampYouUnix seconds. Rejected if too far from chain time.

When you submit the request, your authentication unlocks the enclave to sign. The enclave adds your signer pubkey and the Ed25519 signature to the payload, then the transaction reaches the validator.

One action per transaction

The transaction field holds a single variant, not an array. To take two actions, submit two transactions. They may land in the same block, but the chain processes them sequentially, not atomically.

This is a deliberate design choice. It simplifies the mempool and matching order. It also means Solana-style atomic compound patterns (e.g., "place a buy and a sell in one signed payload") do not port over.

Field shape

The validator verifies signatures by re-serializing the transaction with serde_json and comparing bytes against the signed payload. This means the body you sign must include every field the variant declares. For optional fields you are not setting, send null rather than omitting the key. Omitting the key produces different bytes on the validator side after deserialize/re-serialize, and the signature will not verify (HTTP 400 "invalid signature").

The canonical bodies for each user-accessible variant are below. Copy these as a starting point.

PlaceOrder

{
  "transaction": {
    "PlaceOrder": {
      "account_id": 27,
      "market_id": 2,
      "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
    }
  },
  "nonce": 1777725953635,
  "timestamp": 1777725953
}
FieldTypeNotes
account_idu64Your trading account id.
market_idu16Target market.
side"Long" | "Short"
order_type"Limit" | "Market" | "StopLimit" | "StopMarket"
priceu64Price with PRICE_EXPO = -8 (i.e. 28000000000 = $280.00000000). For market orders, send 0.
sizeu64Size with SIZE_EXPO = -6 (i.e. 100000 = 0.1 units).
trigger_priceu64 | nullRequired for stop variants; send null otherwise.
reduce_onlybool
post_onlybool
time_in_force"GTC" | "IOC" | "FOK"
take_profitobject | nullAttached take-profit. Send null if not setting one. See attached order shape.
stop_lossobject | nullAttached stop-loss. Send null if not setting one. Same shape as take_profit.
max_slippage_bpsu16, optionalSlippage cap for market orders, in basis points. Omit the key entirely when unset; do not send null. Including null here will fail signature verification.
margin_mode"Cross" | "Isolated", optionalDefaults to "Cross". Omit the key when using the default; only include it when setting "Isolated". See Margin for the mode comparison.
leverageu16, optionalUser-selected leverage. 0 (or omitted) means "market max." Capped at floor(10000 / IMR_bps) per market. Omit when zero, same byte-symmetry rule as margin_mode.

Attached take-profit / stop-loss

When you set take_profit or stop_loss, use this shape:

{
  "trigger_price": 30000000000,
  "order_type": "StopMarket",
  "limit_price": null
}

limit_price is a required key. Send null for StopMarket and a price for StopLimit.

CancelOrder

{
  "transaction": {
    "CancelOrder": {
      "order_id": 12345
    }
  },
  "nonce": 1777725953636,
  "timestamp": 1777725953
}

CancelAllOrders

{
  "transaction": {
    "CancelAllOrders": {
      "market_id": null
    }
  },
  "nonce": 1777725953637,
  "timestamp": 1777725953
}

market_id filters cancellations to one market when set. null cancels every open order on the account. Send the field either way.

ModifyOrder

{
  "transaction": {
    "ModifyOrder": {
      "order_id": 12345,
      "new_price": 28100000000,
      "new_size": null
    }
  },
  "nonce": 1777725953638,
  "timestamp": 1777725953
}

new_price and new_size are independently optional. Send the keys you are not changing as null. Set at least one of them.

AdjustIsolatedMargin

Add or remove collateral on an open isolated position's bucket. Positive delta moves cross collateral into the bucket (lowers effective leverage). Negative delta moves bucket collateral back to cross (raises effective leverage).

{
  "transaction": {
    "AdjustIsolatedMargin": {
      "account_id": 27,
      "market_id": 2,
      "delta": 50000000
    }
  },
  "nonce": 1777725953639,
  "timestamp": 1777725953
}
FieldTypeNotes
account_idu64The account holding the isolated position.
market_idu16The market the position is in.
deltai128Signed USDC lamports (scale 6). +50000000 adds $50 to the bucket; -50000000 removes $50.

The validator rejects the transaction if there is no isolated position on the market, or if a positive delta exceeds free cross collateral. It also rejects a negative delta that would drop the bucket below the position's initial-margin requirement at its selected leverage. See Margin.

User transaction types

Five transaction types are available to signers today:

VariantPurpose
PlaceOrderSubmit a market, limit, stop market, or stop limit order.
CancelOrderCancel one order by ID.
CancelAllOrdersCancel all your open orders, optionally filtered by market.
ModifyOrderChange price or size of a resting order without losing queue priority when possible.
AdjustIsolatedMarginAdd or remove collateral on an open isolated position's bucket.

Both the trading UI and API keys submit the same five variants. The REST layer enforces API key scope. See Authentication.

Deposits and withdrawals

Bridge deposits are not user-signed transactions. You generate a deposit address in the trading UI, send USDC to it on Solana, and Parcl-controlled bridge signers attest to the deposit on-chain. The credit appears on your account once attestations from the configured bridge-signer quorum agree. Consensus validators process those attestations but do not gain bridge authority.

Withdrawals back to Solana go through the trading UI rather than direct transaction submission.

Admin, oracle, bridge, and multisig variants

Other transaction variants exist in the validator for admin operations, oracle price updates, bridge attestations, and multisig coordination. Only authorized signers can use them. Normal accounts cannot access them.

Nonces

Every transaction carries a nonce that prevents replay. Set it to the current time in milliseconds (Date.now()).

The validator tracks nonces per signing key and accepts one only when all three conditions hold:

  • The nonce falls within (T - 2 days, T + 1 day), where T is the block timestamp in milliseconds. The validator rejects a value far in the past or future.
  • The signer has not used the nonce before.
  • The nonce is greater than the smallest of the signer's 100 most recent nonces (once the signer has sent at least 100). Below that, the validator treats it as stale.

Set each nonce to the current millisecond. When you send more than one transaction inside the same millisecond, increment by one so each stays unique and ordered:

nonce = Math.max(Date.now(), lastNonce + 1);

Do not seed a counter once at startup and then increment it per transaction (nonce = Date.now() at boot, then nonce++). A counter that advances with transaction count rather than the clock drifts below the accepted window after enough uptime. The validator then rejects every transaction the signer sends. Read the clock on each nonce instead.

Each signing key keeps its own nonce sequence. Programmatic access uses an agent key, which the validator tracks separately from the account owner's key. If several bots share one key, they share one sequence, so route them through a single allocator or give each bot its own key.

Responses

POST /tx/sign-and-submit returns one of:

{ "status": "success", "hash": "0x...", "events": [ ... ] }
{ "status": "failed", "hash": "0x...", "events": [], "error": "..." }

events is always present (empty on failure). It contains all state changes the transaction produced: fills, order placements, cancellations, liquidations, funding updates, and so on. For a PlaceOrder, this is where you confirm what actually filled.