Transactions
Every write to Parcl Chain is a signed transaction carrying exactly one action. There is no atomic multi-instruction batching.
Hosted accounts sign inside a secure enclave. The enclave generates the hosted key and does not return its plaintext to application servers. Key export requires an authenticated browser session and is blocked for API keys. See authentication.
Agent-key clients hold their own private keys and sign locally. They submit the signed envelope directly to the validator. See agent keys.
Submitting a transaction
Hosted accounts have two ways to authorize the enclave to sign:
- Trading UI. Log in at 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.
- API key. Send the unsigned
transactionbody toPOST /tx/sign-and-submitwith yourX-API-Keyheader. The key is your authorization token. See Authentication for key generation and scope, and request limits for the exact throttles.
Funded account creation
Verify the chain ID through GET /v1/node/info and read
account_activation_fee_usdc from GET /v1/treasury-config before depositing.
Check GET /v1/accounts/by-owner/{owner}/list for an existing personal account.
The owner is the 32-byte signing public key encoded as 64 lowercase hex characters.
Deposit at least 5,000,000 USDC base units through the matching Solana bridge.
Read GET /v1/bridge/state?owner={owner} until unclaimed_usdc covers both
5,000,000 base units and the activation fee. Then sign this action with the same
owner key, optionally using an eligible personal account ID as referred_by:
{
"CreateFundedAccount": {
"mode": "Cross",
"referred_by": 123
}
}Replace 123 with the resolved referrer account ID. Use the signed-envelope
format below and submit to the validator. This path requires no email account.
The retired CreateReferredAccount action cannot create an account. Omit referred_by to create independently without referral attribution.
Creation consumes the owner's confirmed balance, charges the configured activation
fee, and credits the remainder to the new account. A failed attempt leaves the
funds available for a corrected request. Before retrying, check
GET /v1/accounts/by-owner/{owner}/list for an entry with isVault: false.
Read that entry through GET /v1/accounts/{id}. If pendingActivation is false,
creation has completed. Otherwise, recheck funding and fee terms before continuing.
Hosted signup applies a separate referral requirement. Email invitations can waive that hosted requirement. Hosted account-creation requests must use the saved referral selection. No cohort proof or membership transaction is part of the signed action.
Request body
When submitting via POST /tx/sign-and-submit:
{
"transaction": { "PlaceOrder": { ... } },
"nonce": 1713456789000,
"timestamp": 1713456789
}| Field | Who sets it | Description |
|---|---|---|
transaction | You | The action. Exactly one variant from the list below. |
nonce | You | A u64, normally unique Unix milliseconds. Every envelope includes it. See Nonces. |
timestamp | You | Unix 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.
Direct signed envelope
Agent-key clients submit a signed envelope to the validator. This example uses
devnet. Mainnet uses parcl-v4:mainnet:chain-1 and devnet uses
parcl-v4:devnet:chain-2.
{
"transaction": { "CancelOrder": { "order_id": 12345 } },
"chain_id": "parcl-v4:devnet:chain-2",
"signer": [0, 0, 0],
"signature": [0, 0, 0],
"nonce": 1777725953635,
"timestamp": 1777725953
}The displayed byte arrays are abbreviated. signer must contain 32 bytes, and
signature must contain 64 bytes. Sign these UTF-8 bytes:
JSON.stringify([chainId, transaction, nonce, timestamp])Pin the expected chain ID in client configuration. Compare it with
GET /v1/node/info on the selected validator host before signing. The
validator rejects a missing chain ID or a signature for another chain with
HTTP 400.
The signed message binds the exact transaction JSON substring. Changing a
key, value, field order, or whitespace after signing invalidates the signature.
Build the transaction bytes once, sign them, and place those same bytes in the
submitted envelope.
One action per transaction
The transaction field holds one variant. To take two actions, submit two
transactions. They may land in the same block. The chain processes each action
sequentially.
Field shape
The validator authenticates the exact transaction JSON bytes from the signed
envelope. It parses and executes the value from those same bytes. A direct
client must sign and submit the same representation.
Fields with a null type in the tables below can be null or omitted. Fields
that use a protocol default, such as margin_mode and leverage, must be
omitted when unset. Sending null for a defaulted non-null field returns HTTP
400 because the transaction cannot be parsed.
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
}| Field | Type | Notes |
|---|---|---|
account_id | u64 | Your trading account id. |
market_id | u16 | Target market. |
side | "Long" | "Short" | |
order_type | "Limit" | "Market" | "StopLimit" | "StopMarket" | |
price | u64 | Price with PRICE_EXPO = -8 (i.e. 28000000000 = $280.00000000). For market orders, send 0. |
size | u64 | Size with SIZE_EXPO = -6 (i.e. 100000 = 0.1 units). |
trigger_price | u64 | null | Required for stop variants. Send null otherwise. |
reduce_only | bool | |
post_only | bool | |
time_in_force | "GTC" | "IOC" | "FOK" | |
take_profit | object | null | Attached take-profit. Send null if not setting one. See attached order shape. |
stop_loss | object | null | Attached stop-loss. Send null if not setting one. Same shape as take_profit. |
max_slippage_bps | u16 | null, optional | Slippage cap for market orders, in basis points. null and omission both parse as unset. |
margin_mode | "Cross" | "Isolated", optional | Defaults to "Cross". Omit the key when using the default. An explicit null returns HTTP 400 during transaction parsing. See Margin. |
leverage | u16, optional | User-selected leverage. 0 or omission means market maximum, capped at floor(10000 / IMR_bps). An explicit null returns HTTP 400 during transaction parsing. |
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. Attached exits support order_type: "StopMarket" with limit_price: null. Do not submit an attached StopLimit expecting a resting limit exit. The activation path creates a StopMarket conditional order. Standalone StopLimit orders remain supported through the top-level PlaceOrder.order_type field.
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
}| Field | Type | Notes |
|---|---|---|
account_id | u64 | The account holding the isolated position. |
market_id | u16 | The market the position is in. |
delta | i128 | Signed 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.
Trading transaction types
The trading API uses these five transaction types:
| Variant | Purpose |
|---|---|
PlaceOrder | Submit a market, limit, stop market, or stop limit order. |
CancelOrder | Cancel one order by ID. |
CancelAllOrders | Cancel all your open orders, optionally filtered by market. |
ModifyOrder | Change price or size of a resting order without losing queue priority when possible. |
AdjustIsolatedMargin | Add or remove collateral on an open isolated position's bucket. |
The trading UI and API keys can submit these variants. The UI also uses owner-only and account-management variants for other features. The REST layer enforces API-key scope before signing. See Authentication.
Deposits and withdrawals
Bridge deposits use Solana transactions. Sending USDC to the address shown by the app funds the linked Solana wallet; the user must then submit the bridge deposit instruction before Parcl Chain can credit collateral. See Solana bridge for the app and direct integration flows.
Withdrawals back to Solana go through the trading UI rather than API-key
submission. RequestWithdrawal records the token, amount, and immutable Solana
destination. USDC is debited from eligible margin collateral. PRCL is debited
from your available PRCL balance. The withdrawal window is 24
hours. The request can be cancelled while it is Pending, which restores the
exact debit. It cannot be cancelled after it becomes ReadyToClaim. The claim can then be submitted on Solana. The chain
rejects any bridge token other than USDC or PRCL.
Nonces
Every signed envelope carries a nonce. Use the current Unix time in
milliseconds and keep it unique for the signing key.
For user actions other than CancelOrder, 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), whereTis 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.
CancelOrder is the user-facing exception. The validator does not commit or check its nonce. Clients must still send the field and should use the same unique time-based allocation.
Set each nonce to the current millisecond. When you send more than one transaction inside the same millisecond, increment the last allocated value so each nonce stays unique and ordered.
Do not seed a counter once at startup and then increment it forever. A transaction-count-based counter can drift outside the accepted window. Read the clock for each allocation.
Each signing key keeps its own nonce window. Agent keys are separate from the account owner's key. Hosted API-key requests use the owner signer and share its window with browser submissions. If several clients share one signer, route them through one allocator or use separate approved agent keys.
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.