Perpetual futures with an exchange-grade matching engine, and collateral that never stops being yours on-chain. Matching, risk and funding run off-chain and deterministically. Custody and settlement live on Canton, where every movement is a ledger fact you can check yourself.

Choose your starting point

Trade in the app

Set up your identity and account with browser passkey approvals you control.

Trading agents

Connect an owner-approved API key, with optional MCP tools.

Market makers

Use a server-issued bot key with REST and WebSockets. MCP is optional.

Shared API reference

Two public surfaces. Both generated from the same schemas the engine validates against.

REST API

82 operations. CCXT /v2 has 17 backed methods and 7 explicit HTTP 501 stubs. Edel is perpetual-only; spot is not offered.

WebSockets

18 channels. Order book, trades, marks, funding, and your own fills, positions and margin — pushed, never polled.

Base URLs

The Production venue on mainnet has not launched, so it publishes no hostnames yet. They appear here when the public venue opens.

Authentication

Start here — every private endpoint and account channel depends on a verified DFNS EndUser. Signing in to the private demo app itself never identifies a trader and never creates an Edel account.
Registration is available only when socialRegistrationProviders advertises a provider. Where offered, the direct flow is provider OIDC → DFNS social registration → WebAuthn passkey (FIDO2). Authentication creates a DFNS EndUser credential, a delegated signing key, and the durable Edel account mapping. It does not create a DFNS wallet or a Canton Party.

Read what this venue offers

GET /v1/auth/capabilities names the sign-in methods this venue offers now. It needs no credential. Read it at startup and again before you show a sign-in method, because a venue can change what it offers. Never cache it as a permanent answer.
A venue that offers Google returns:
Passkey login for an existing user can be available while registration is closed. The public API exposes no API-key, personal-access-token, or passkey credential-management routes. Login returns a DFNS EndUser bearer. Only short-lived realtime tickets can then be minted, rotated, and revoked through Edel’s public API.

Register only through an advertised provider

1

Create one stable browser attempt

Generate one opaque Idempotency-Key and retain it in tab-scoped session storage across refreshed Google tokens. The key identifies an attempt; the verified Google subject owns it.
2

Start direct social registration

Call POST /v1/auth/social/register only when socialRegistrationProviders contains google. The public request contains the Google OIDC ID token and stable attempt key only:
The response is external-auth-register-init/v1 with user, temporaryAuthenticationToken, and the DFNS WebAuthn creation challenge. The API supplies the configured organization and fixed Oidc provider kind.
Pass the full returned WebAuthn options to the browser; the abbreviated response above shows the fields guaranteed by the public contract plus representative DFNS options.
3

Create and submit the passkey

Use navigator.credentials.create() with that challenge. Convert the returned WebAuthn attestation to the exact public request below and call POST /v1/auth/register/complete. Re-send a fresh Google token for the same subject. The wire literal is Fido2:
referralCode is an optional 16-character lowercase hex field. The API resolves it before DFNS writes a credential. An invalid code reaches zero DFNS mutations; when affiliate-public policy is disabled or unreadable, a request containing the field is hidden with 404.
4

Persist the returned identity

Success returns external-auth-register-complete/v1 with the DFNS user and registered username. The API accepts only the initiated DFNS user and configured organization, marks credential completion durable before key provisioning, then creates exactly one DFNS-user-to-Edel-account mapping. The legacy walletState response field is not evidence that registration created a DFNS wallet or Canton Party.

Log in an existing user

1

Choose one composed login

For a passkey, send the exact registered DFNS username to POST /v1/auth/login/init, sign its challenge with navigator.credentials.get(), then send challengeIdentifier and a firstFactor whose kind is Fido2 to POST /v1/auth/login/complete. A caller-chosen username alone never registers or authenticates a new EndUser.
After navigator.credentials.get(), submit this public request shape:
2

Use Google when advertised

When socialLoginProviders contains google, the returning user may exchange the Google OIDC ID token directly for the same DFNS session:
Passkey completion and social login both return the same session shape:
3

Run the shortest API test

The live snapshot above offers passkey login to an already registered user. After passkey completion returns external-auth-login/v1, send its DFNS EndUser token as Authorization: Bearer <token> on every private REST request. Market-data routes need no credential. Minting a realtime ticket proves the bearer and returns the mapped accountId:
Use the bearer—not the short-lived WebSocket ticket—for authenticated REST calls:
Rotate the short-lived ticket before expiresAt; the ticket itself authorizes rotation:
Replace REALTIME_TICKET with the returned ticket. Revoke the ticket family when finished; presenting the current or immediately previous ticket returns 204 No Content:

Retry and recovery

These routes apply only to a registration attempt started while its provider was advertised; they do not provide lost-passkey credential recovery.
  • A conclusive rejection before an irreversible vendor result can retry with fresh Google or WebAuthn proof. A User Action failure before POST /keys creates no key-attempt fence.
  • An ambiguous registration result must not replay registration. Call POST /v1/auth/social/register/recover with the same Idempotency-Key and a fresh Google token. It uses matching DFNS login evidence and never repeats registration or passkey completion.
  • After DFNS returns the matching user and organization, key-provisioning failures never reopen registration. A conclusive POST /keys rejection may retry through authenticated login. An unknown result remains submitted_unknown: recovery lists only the exact owner/name and stays pending for manual resolution if absent; it never issues a blind second POST.
  • If only account mapping persistence failed, POST /v1/auth/mapping/recover re-introspects the bearer and rebuilds the deterministic mapping without accepting a client-supplied user or account id. Lost-passkey recovery is not implemented by either mapping recovery route.

Expected errors

Failures use the external-rest-error/v1 envelope. Branch on code, retryable, and recoveryActions; retain requestId when reporting a failure. Ticket failures also include realtimeTicketFailure with invalid, replayed, replaced, expired, revoked, or store_unavailable. A withdrawal payout claim is signed against a ledger time the venue fixes when it issues the passkey challenge, and the ledger accepts it only for about 60 seconds after that. Complete the claim (POST /v1/withdrawals/:withdrawalId/confirm with action: "complete") within 30 seconds of the challenge. A later complete does not fail: it answers 200 with status: "approval_required" and a new challenge for a fresh acceptance, which you sign and complete the same way. A complete that does fail answers 503 service_degraded with retryable: true and a withdrawalFailure naming the cause: A refused acceptance becomes claimable again once the venue proves it never reached the ledger, at most 10 minutes after its challenge. Until then initiate answers the withdrawal without a new challenge; read the status and start again later. A payout whose claim deadline passes unclaimed reads status: "offer_expired" and can no longer be claimed.

WebSockets

Sockets do not take your bearer token. A long-lived connection would park a long-lived credential, so account channels use short-lived, account-scoped tickets instead. Market channels need no ticket at all.
1

Mint a ticket

Call POST /v1/realtime/ticket with the bearer your REST calls send: the DFNS EndUser token, or an API key with read scope. The response carries the ticket, the accountId it is bound to and expiresAt in epoch milliseconds, and is never cacheable.
2

Authenticate the socket

Open the socket at the WebSocket URL under Base URLs and send the ticket in an auth frame:
Wait for auth_ok before you subscribe. It names the account the socket is now bound to and the expiresAt at which this socket session ends:
A ticket authenticates a socket once: sending it again, on any socket, is refused as replayed. A refused auth frame answers an error frame instead of auth_ok:
  • unauthorized: the ticket was refused; realtimeTicketFailure names why, with the values listed under Expected errors.
  • rate_limited: the socket sent too many auth frames in a short window. Wait before sending another.
  • auth_unavailable: the venue could not check the ticket just now. Try again shortly.
A refused auth frame can also close the socket with code 1008. If it does, open a new socket and start again with a new ticket.
3

Subscribe after auth_ok

Only after auth_ok arrives, send the subscribe envelope for each account channel, with the accountId from auth_ok:
A subscribe sent before auth_ok arrives, even one sent right after the auth frame, is refused with a realtime-error/v1 frame whose code is unauthorized. A subscription exists only once its realtime-subscription-accepted/v1 frame arrives; each channel page shows that frame.

Keep the socket authenticated

A socket session lasts until the expiresAt in its latest auth_ok. Tickets expire in minutes, so read expiresAt rather than assuming a lifetime. In the last 30 seconds before expiresAt, the socket sends one auth_expiring frame:
To keep account channels flowing, rotate and authenticate again before expiresAt:
  1. Call PUT /v1/realtime/ticket with the current ticket as its body. It takes no bearer: the ticket itself is the credential, so a long-running client never re-presents your session. It returns the next ticket of the same family. Always rotate with the newest ticket: a ticket that has been rotated can neither be rotated again nor authenticate a socket (replaced).
  2. Send the new ticket in a new auth frame on the same socket. The new auth_ok carries the later expiresAt, and the account subscriptions stay open. Rotating without this step does not extend the socket session.
If expiresAt passes first, the socket stays open and market subscriptions keep flowing, but it delivers no more account frames. It removes every account subscription and sends:
Rotating an expired ticket is refused as expired. Mint a new one with POST /v1/realtime/ticket, send it in an auth frame on the same socket, wait for auth_ok and subscribe again. DELETE /v1/realtime/ticket revokes the whole ticket family at once: the switch to reach for if a client is compromised. Every later rotation or auth frame with a ticket from that family is refused as revoked. private_subscriptions_dropped carries one of these reasons. Each one removes every account subscription on the socket and leaves the socket open.

Why build here

Collateral stays on the ledger

Deposits and withdrawals settle on Canton. Your balance is backed by an on-chain position, not an exchange IOU.

State you can replay

Every accepted order, fill and funding accrual is journaled in sequence. Venue state is reconstructed from that journal, not from a mutable row someone can edit.

Types that cannot drift

Request, response, envelope and channel payloads are projected from the venue’s own schemas. There is no handwritten copy to fall out of date with the running engine.

Fails closed, on purpose

Stale or disagreeing market data blocks risk-increasing orders instead of filling them against a price nobody trusts.

The trading model

Two order types, deliberately. limit executes at your price or better; market executes within the venue’s protective price band. Optional postOnly makes a limit order maker-only. reduceOnly guarantees an order can only shrink a position, never grow or flip it. Attach autoClose prices at placement and take-profit and stop-loss are armed together as an OCO pair — the first to fire cancels its sibling. Time-in-force is not a public field. The venue derives IOC for market orders and GTC for limit orders; postOnly keeps a limit maker-only.
Every monetary value crosses the wire as an integer atom string, with a formatted decimal sibling for display. No floating point touches a balance.

Rate limits and back-pressure

600 authenticated requests per 60 seconds by default, for each API key or signed-in user. The window starts with your first request and resets 60 seconds later; it does not roll. Minting a realtime ticket counts against the same budget. A venue can set a different limit; a staging or production venue never allows more than 6,000 per window. Exceed it and you get 429 with the code rate_limited, retryable: true, and a retry_after recovery action. The response has no Retry-After header: wait for your window to reset, at most 60 seconds, then retry the same request, with the same Idempotency-Key when it has one. Never retry an order with a new clientOrderId because of a rate limit. Requests whose credential fails verification are also limited: 600 per 60 seconds by default from one network address. A request that authenticates does not count against that limit. Every rejection is a typed error rather than an opaque status, so a client can branch on the code instead of parsing prose. Two habits that keep you well inside any limit:
  • Stream instead of polling. Order books, marks, fills and margin all arrive on WebSocket channels. Repeatedly polling REST for state that is already pushed is the most common way integrators hit a limit.
  • Reuse one socket. Subscribe to many channels on a single connection rather than opening one per channel.
Rejections that are not rate limits use the same envelope, so retryable tells you whether a retry can ever succeed. A validation failure is not retryable no matter how long you wait.

WebSocket channels

Connect at /streams/v1/ws. Every channel, with its auth, snapshot source and source status, is listed by GET /streams/v1/contract on the same host, also served at /streams/v1/catalog. Market data — open to everyone. market.trades · market.book · market.ticker · market.price · market.funding · market.status · market.candles Your account — authenticated. account.orders · account.fills · account.durability · account.positions · account.pnl · account.margin · account.balance · account.withdrawals · account.reconciliation · account.alerts · account.earnings Open the WebSockets tab for the payload shape, subscribe frame and example of each channel. Account channels need a ticket: see WebSockets.

Contract versions

  • REST — Edel PERPS External REST API 0.1.0, 82 operations.
  • CCXT /v2 — 17 backed methods and 7 HTTP 501 stubs. Stubbed methods remain has[method] = false; spot is not offered.
  • WebSockets — Edel PERPS Realtime WebSocket API realtime-contract/v1, 18 channels.
Both documents are machine-readable and regenerated from the repository contracts on every build, so this site cannot describe an endpoint the venue does not serve.
Built by Edel.