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. Two public surfaces. Both generated from the same schemas the engine validates against.

REST API

75 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

These hostnames are placeholders while the public venue is being stood up. Confirm the live endpoints with us before you point production traffic at them.

Authentication

Start here — every private endpoint and account channel depends on a verified DFNS EndUser. Clerk/Vercel protects access to the private Demo only. Its subject, email, and session never identify a trader, call DFNS, or create the dfns_user_accounts mapping.
Registration is available only when socialRegistrationProviders advertises a provider. Where composed, 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.

Configuration checklist

No secret belongs in browser code or this guide. The API requires DFNS_API_URL, DFNS_ORG_ID, DFNS_CRED_ID, DFNS_PRIVATE_KEY, and DFNS_AUTH_TOKEN together. Durable registration also requires DATABASE_URL and EDEL_SECRETS_MASTER_KEY. Direct Google flows require DFNS_SOCIAL_AUTH_PROVIDERS=google and GOOGLE_OIDC_CLIENT_ID; the Demo receives only the public VITE_GOOGLE_CLIENT_ID. Its Vercel build also requires a non-browser GOOGLE_OIDC_CLIENT_ID copied from the target API runtime; the build fails without both values or when they differ. The DFNS organization must have Google social registration/login enabled, the correct WebAuthn relying-party origins, and the reviewed User Action and Keys permissions.
GET /v1/auth/capabilities is the runtime source of truth. Existing-user passkey login may be available while direct Google registration is hidden. Delegated username/email registration, EndUser email OTP, and lost-passkey recovery are explicitly unavailable until their own product identity and recovery authorities are selected.
A Google-composed environment returns this capability shape:
This shape offers direct Google registration and login plus existing-user passkey login. It still does not offer delegated username/email registration, EndUser email OTP, or lost-passkey recovery. Treat it as a contract example, not a cached deployment verdict. Read capabilities at startup and before presenting an authentication method because composition can change. 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 Roman's 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.

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.
1

Mint a ticket

POST /v1/realtime/ticket with your session. The response carries the ticket and its expiry, and is never cacheable.
2

Connect and subscribe

Present the ticket on the socket, then subscribe to your account channels. Market channels need no ticket at all.
3

Rotate before expiry

PUT /v1/realtime/ticket exchanges a live ticket for the next one, and takes no bearer — the ticket itself is the credential, so a long-running client never re-presents your session.
Tickets expire in minutes. DELETE /v1/realtime/ticket revokes the whole family at once — the switch to reach for if a client is compromised.

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 requests per minute per credential on authenticated endpoints, counted in a rolling 60-second window and keyed to your session token. Minting a realtime ticket counts against the same budget. Exceed it and you get 429 with the canonical code rate_limited, retryable: true, and a retry_after recovery action. 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. 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 Authentication.

Contract versions

  • REST — Edel PERPS External REST API 0.1.0, 75 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.