# Parcl V4 > Parcl V4 appchain documentation ## Quickstart Get trading in under 5 minutes. You'll need an Ed25519 keypair and some test USDC. ### 1. Create an account Sign up at [dist-lac-seven.vercel.app](https://dist-lac-seven.vercel.app). The onboarding flow mints test USDC on Solana, bridges it to the appchain, and creates your margin account. Takes about 15 seconds. After onboarding, go to Settings (top-right) and copy your **Appchain Public Key**. You'll need this for API calls. ### 2. Get your account ID ```bash curl https://v4-api.dev.parcllabs.com/v1/accounts/by-owner/YOUR_PUBKEY_HEX ``` Response: ```json { "accountId": 12, "owner": "35504e68...", "collateral": "10000000000", "positions": [], "openOrders": [] } ``` The `accountId` is what you use for placing orders. ### 3. Check available markets ```bash curl https://v4-api.dev.parcllabs.com/v1/markets ``` Returns an array of markets. Each has a `marketId` (0 = NYC, 1 = USA, 2 = Austin), current prices, and fee rates. ### 4. Read the orderbook ```bash curl https://v4-api.dev.parcllabs.com/v1/markets/0/orderbook ``` Returns bids and asks with price and size at each level. ### 5. Place an order Orders are Ed25519-signed transactions submitted via `POST /v1/tx`. See [signing transactions](/trading/signing) for the full flow. The short version: you construct a JSON transaction, serialize it with your nonce and timestamp, sign the bytes with your Ed25519 private key, and POST the signed payload. ### 6. Stream real-time data Connect to the WebSocket for live orderbook updates, fills, and price changes: ```javascript const ws = new WebSocket("wss://v4-api.dev.parcllabs.com/v1/ws"); ws.onopen = () => { ws.send(JSON.stringify({ type: "subscribe", channels: [ { channel: "orderbook", market_id: 0 }, { channel: "trades", market_id: 0 } ] })); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); console.log(msg.type, msg); }; ``` See [WebSocket reference](/api/websocket) for all channels and message formats. ## Markets Parcl V4 trades perpetual futures on real estate price indices. Each market tracks a Parcl index that measures residential real estate prices in a specific geography. ### Available markets | ID | Name | Index | Description | | -- | ------ | ------------ | ------------------------------------- | | 0 | NYC | Parcl NYC | New York City residential real estate | | 1 | USA | Parcl USA | US national residential real estate | | 2 | Austin | Parcl Austin | Austin, TX residential real estate | ### Price format All prices use 8 decimal places internally. A price of `58035000000` means $580.35. To convert: `display_price = raw_price / 100,000,000` Sizes use 6 decimal places. A size of `1000000` means 1.0 units. ### Oracle Prices are published by Parcl's oracle service once per day at approximately 9:05 AM ET. The oracle reads from Parcl's real estate price feed database and signs `UpdateOraclePrice` transactions that go through consensus like any other transaction. Each market shows: * **Oracle price (index)** — the latest published settlement price * **Mark price** — the current mid-market price from the order book * **Valid until** — when the current oracle price expires (next day's settlement time) When the oracle price expires, it means a new price is expected. Trading continues with the stale price until the update arrives. ### Funding Funding settles hourly. The rate is based on the gap between mark price and oracle price — if mark > oracle, longs pay shorts, and vice versa. This pulls the mark price back toward the oracle over time. The funding rate is applied to all open positions at the funding timestamp. It hits your collateral directly (like Binance and Hyperliquid, not deferred). ### Leverage Maximum leverage is 10x on all markets. The initial margin requirement is 10% (1000 bps) and maintenance margin is 5% (500 bps). If your account equity falls below maintenance margin, you get liquidated. ### Fees Fees are currently 0% (devnet competition). When enabled, fees are volume-based with tiered rates — higher volume traders pay lower fees. See `/v1/fee-tiers` for current configuration. Fee schedule: `GET /v1/fee-tiers` ## Signing transactions Every transaction on Parcl V4 is signed with Ed25519. The signing flow: build the transaction JSON, serialize it with a nonce and timestamp, sign the bytes, and submit. ### Transaction format A signed transaction has five fields: ```json { "transaction": { "PlaceOrder": { ... } }, "signer": [1, 2, 3, ...], "signature": [4, 5, 6, ...], "nonce": 1712345678901, "timestamp": 1712345678901 } ``` * `transaction` — the operation (PlaceOrder, CancelOrder, etc.) * `signer` — your Ed25519 public key as a 32-element byte array * `signature` — the Ed25519 signature as a 64-element byte array * `nonce` — a unique number (use `Date.now()` — must be unique per tx) * `timestamp` — Unix timestamp in milliseconds ### Signing message The message to sign is the JSON serialization of `[transaction, nonce, timestamp]`: ```javascript const message = JSON.stringify([transaction, nonce, timestamp]); const signature = ed25519.sign(message, privateKey); ``` The validator deserializes and re-serializes to verify, so your JSON must match the exact serde format the Rust validator expects. Use the same field order as shown in the transaction types below. ### Submitting ``` POST /v1/tx Content-Type: application/json { "transaction": { ... }, "signer": [...], "signature": [...], "nonce": 1712345678901, "timestamp": 1712345678901 } ``` Response: ```json { "status": "success", "hash": "abc123...", "events": [...] } ``` Or on failure: ```json { "status": "failed", "error": "insufficient margin" } ``` ### Transaction types #### PlaceOrder ```json { "PlaceOrder": { "account_id": 12, "market_id": 0, "side": "Long", "order_type": "Market", "price": 58000000000, "size": 1000000, "trigger_price": null, "reduce_only": false, "post_only": false, "time_in_force": "GTC" } } ``` * `side` — `"Long"` or `"Short"` * `order_type` — `"Market"`, `"Limit"`, `"StopLimit"`, `"StopMarket"` * `price` — 8-decimal scaled. For market orders, use 0 (fills at best available) * `size` — 6-decimal scaled. `1000000` = 1.0 units * `time_in_force` — `"GTC"` (good til cancel), `"IOC"` (immediate or cancel), `"FOK"` (fill or kill) * `trigger_price` — for stop orders, the oracle price that triggers the order * `reduce_only` — if true, only reduces existing position (won't open a new one) * `post_only` — if true, rejects if it would fill immediately (maker-only) Optional attached TP/SL: ```json { "PlaceOrder": { ... "take_profit": { "trigger_price": 60000000000, "order_type": "Market", "limit_price": null }, "stop_loss": { "trigger_price": 55000000000, "order_type": "Market", "limit_price": null } } } ``` #### CancelOrder ```json { "CancelOrder": { "order_id": 42 } } ``` #### CancelAllOrders ```json { "CancelAllOrders": { "market_id": 0 } } ``` Pass `null` for `market_id` to cancel across all markets. #### ModifyOrder ```json { "ModifyOrder": { "order_id": 42, "new_price": 59000000000, "new_size": null } } ``` Fields are optional — only set what you want to change. #### AddCollateral ```json { "AddCollateral": { "account_id": 12, "amount": 5000000000 } } ``` Amount in USDC with 6 decimals. `5000000000` = $5,000. #### RemoveCollateral ```json { "RemoveCollateral": { "account_id": 12, "amount": 1000000000 } } ``` #### Example: TypeScript signing ```typescript import { ed25519 } from "@noble/ed25519"; async function signAndSubmit( privateKey: Uint8Array, transaction: object ) { const publicKey = await ed25519.getPublicKeyAsync(privateKey); const nonce = Date.now(); const timestamp = Date.now(); // Build signing message (must match Rust serde format) const message = JSON.stringify([transaction, nonce, timestamp]); const messageBytes = new TextEncoder().encode(message); const signature = await ed25519.signAsync(messageBytes, privateKey); const body = { transaction, signer: Array.from(publicKey), signature: Array.from(signature), nonce, timestamp, }; const res = await fetch("https://v4-api.dev.parcllabs.com/v1/tx", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); return res.json(); } // Place a market buy on NYC const result = await signAndSubmit(myPrivateKey, { PlaceOrder: { account_id: 12, market_id: 0, side: "Long", order_type: "Market", price: 0, size: 100000, // 0.1 units trigger_price: null, reduce_only: false, post_only: false, time_in_force: "IOC", }, }); console.log(result); // { status: "success", hash: "abc123...", events: [...] } ``` ## REST API Base URL: `https://v4-api.dev.parcllabs.com` All responses are JSON. Prices use 8 decimal places, sizes use 6, collateral (USDC) uses 6. ### Markets #### GET /v1/markets Returns all markets. ```bash curl https://v4-api.dev.parcllabs.com/v1/markets ``` ```json [ { "marketId": 0, "name": "NYC", "oraclePrice": "58035000000", "markPrice": "58177000000", "openInterestLong": "37460000", "openInterestShort": "37460000", "maxOpenInterest": "1000000000", "fundingRate": "-15800", "lastFundingTimestamp": 1712345678, "fundingIntervalSecs": 3600, "makerFeeBps": 0, "takerFeeBps": 0, "initialMarginRatioBps": 1000, "maintenanceMarginRatioBps": 500, "oracleValidUntil": 1712433900, "status": "Active", "tickSize": "1000000" } ] ``` #### GET /v1/markets/{id} Returns a single market by ID. #### GET /v1/markets/{id}/orderbook ```json { "bids": [ { "price": "58100000000", "size": "500000" }, { "price": "58050000000", "size": "1200000" } ], "asks": [ { "price": "58200000000", "size": "300000" }, { "price": "58250000000", "size": "800000" } ] } ``` #### GET /v1/markets/{id}/funding Returns funding rate history. #### GET /v1/markets/{id}/trades Returns recent trades. ### Accounts #### GET /v1/accounts/{id} ```json { "accountId": 12, "owner": "35504e68...", "mode": "Cross", "collateral": "21929491932", "positions": [ { "marketId": 0, "side": "Long", "size": "64560188", "entryPrice": "58211606845", "unrealizedPnl": "-120297107", "cumulativeFunding": "0", "lastOraclePrice": "58090000000", "openedAt": 1712345678000 } ], "openOrders": [], "createdAt": 1712300000000, "volume30d": 37581522814 } ``` #### GET /v1/accounts/by-owner/{pubkey} Same response as above. Looks up by the owner's hex public key. #### GET /v1/accounts/{id}/positions Returns just the positions array. #### GET /v1/accounts/{id}/orders Returns just the open orders array. ### Oracle #### GET /v1/oracle/{market_id} Current oracle state for a market. ```json { "price": "58035000000", "date": 20260406, "validUntil": 1712433900, "status": "Active" } ``` #### GET /v1/oracle/{market_id}/history Returns recent oracle price history. ### Exchange #### GET /v1/insurance-fund ```json { "balance": 119059010, "totalDeposited": 140000000000 } ``` #### GET /v1/fee-tiers ```json { "tier_count": 5, "tiers": [ { "min_volume_30d": 0, "maker_fee_bps": -1, "taker_fee_bps": 35 }, { "min_volume_30d": 10000000000, "maker_fee_bps": -1, "taker_fee_bps": 30 } ] } ``` #### GET /v1/node/info ```json { "protocol_version": 3, "binary_version": 3, "build_tag": "dev", "block_height": 73000, "block_timestamp": 1712345678943, "state_root": "4d419b60..." } ``` ### Transactions #### POST /v1/tx Submit a signed transaction. See [signing transactions](/trading/signing) for the full format. Request body: signed transaction JSON. Response on success: ```json { "status": "success", "hash": "9dd57899...", "events": [ { "type": "Fill", "market_id": 0, "price": "58100000000", "size": "100000", ... } ] } ``` Response on failure: ```json { "status": "failed", "error": "insufficient margin" } ``` The `events` array contains all state changes caused by the transaction — fills, order placements, cancellations, liquidations, etc. ### Staking #### GET /v1/staking/validators Returns all registered validators with stake, commission, uptime, and status. #### GET /v1/staking/info Returns network staking stats: total staked, active validators, epoch info, APY. #### GET /v1/staking/balance/{pubkey} Returns the PRCL staking balance for a public key. #### GET /v1/staking/delegations/{pubkey} Returns all delegations by a public key. ## WebSocket Endpoint: `wss://v4-api.dev.parcllabs.com/v1/ws` The WebSocket provides real-time data: orderbook updates, fills, price changes, and block production. Connect, subscribe to channels, and receive events. ### Connecting ```javascript const ws = new WebSocket("wss://v4-api.dev.parcllabs.com/v1/ws"); ``` The server sends a heartbeat ping every 5 seconds. Send `{ "type": "ping" }` to get a pong back with current block height and latency info. ### Subscribing Send a subscribe message with the channels you want: ```json { "type": "subscribe", "channels": [ { "channel": "orderbook", "market_id": 0 }, { "channel": "trades", "market_id": 0 } ] } ``` Response: ```json { "type": "subscribed", "channels": [ { "channel": "orderbook", "market_id": 0 }, { "channel": "trades", "market_id": 0 } ] } ``` To unsubscribe: ```json { "type": "unsubscribe", "channels": [ { "channel": "orderbook", "market_id": 0 } ] } ``` ### Channels #### orderbook Full orderbook snapshot on subscribe, then incremental updates on every change. Subscribe: `{ "channel": "orderbook", "market_id": 0 }` Snapshot (sent immediately on subscribe): ```json { "type": "orderbookSnapshot", "market_id": 0, "bids": [["58100000000", "500000"], ...], "asks": [["58200000000", "300000"], ...] } ``` Updates (on each order/fill/cancel): ```json { "type": "orderbookUpdate", "market_id": 0, "bids": [["58100000000", "450000"]], "asks": [] } ``` A size of `"0"` means that price level was removed. #### trades Recent trades for a market. Subscribe: `{ "channel": "trades", "market_id": 0 }` ```json { "type": "trade", "market_id": 0, "price": "58150000000", "size": "100000", "side": "Long", "timestamp": 1712345678000 } ``` #### fills Your fills (requires your account ID). Fires when your orders get filled. Subscribe: `{ "channel": "fills", "account_id": 12 }` ```json { "type": "fill", "market_id": 0, "price": "58150000000", "size": "100000", "side": "Long", "fee": "203000", "order_id": 42, "timestamp": 1712345678000 } ``` #### orders Your order updates (placed, canceled, modified). Subscribe: `{ "channel": "orders", "account_id": 12 }` #### positions Your position changes. Subscribe: `{ "channel": "positions", "account_id": 12 }` #### funding Funding rate updates for a market. Fires hourly when funding settles. Subscribe: `{ "channel": "funding", "market_id": 0 }` ```json { "type": "funding", "market_id": 0, "funding_rate": "-15800", "timestamp": 1712345678000 } ``` #### oracle / oracleUpdates Oracle price updates for a market. Subscribe: `{ "channel": "oracleUpdates", "market_id": 0 }` ```json { "type": "oracleUpdate", "market_id": 0, "price": "58035000000", "previous_price": "57890000000", "valid_until": 1712433900 } ``` #### explorer Block production info. Fires on every finalized block. Subscribe: `{ "channel": "explorer" }` ```json { "type": "blockFinalized", "height": 73001, "hash": "abc123...", "proposer": "e1e7af3f...", "timestamp": 1712345678943, "tx_count": 3 } ``` ### Ping/pong Send a ping to measure latency and get current block info: ```json { "type": "ping" } ``` Response: ```json { "type": "pong", "blockHeight": 73001, "blockTimeMs": 198, "timestamp": 1712345678943 } ``` ### Example: full orderbook bot ```javascript const WebSocket = require("ws"); const ws = new WebSocket("wss://v4-api.dev.parcllabs.com/v1/ws"); let bids = new Map(); // price -> size let asks = new Map(); ws.on("open", () => { ws.send(JSON.stringify({ type: "subscribe", channels: [{ channel: "orderbook", market_id: 0 }], })); }); ws.on("message", (data) => { const msg = JSON.parse(data); if (msg.type === "orderbookSnapshot") { bids = new Map(msg.bids.map(([p, s]) => [p, s])); asks = new Map(msg.asks.map(([p, s]) => [p, s])); console.log(`Snapshot: ${bids.size} bids, ${asks.size} asks`); } if (msg.type === "orderbookUpdate") { for (const [price, size] of msg.bids) { if (size === "0") bids.delete(price); else bids.set(price, size); } for (const [price, size] of msg.asks) { if (size === "0") asks.delete(price); else asks.set(price, size); } const bestBid = [...bids.keys()].sort().pop(); const bestAsk = [...asks.keys()].sort()[0]; console.log(`Best bid: ${bestBid}, Best ask: ${bestAsk}`); } }); ```