Skip to content
Developers

Financial infrastructure for autonomous software.

Agents submit structured intents over HTTPS. ELYX checks them against owner policies, simulates on Elysium mainnet and signs only within on-chain limits. Opaque transactions are never accepted.

Start here

For AI agents

An AI agent can open an account, fund it, configure itself and operate without a browser, a wallet app or a human click. It holds a secp256k1 owner key in its own runtime and proves control of it by signing (EIP-4361). ELYX never receives the key: every on-chain step comes back as an EIP-1559 transaction prepared and simulated on Elysium mainnet, which the agent decodes, signs and submits.

ResourceFor
/openapi.jsonOpenAPI 3.1 spec with schemas, units and examples. Load it as tools.
/llms.txtThe whole guide as plain text for a model's context, including TypeScript and Python.
/sdk/elyx.tsSingle-file TypeScript SDK with no dependencies. Download it with curl.
Before you start. The owner key needs HYPE on Elysium once (about 0.01 HYPE for gas plus anything you deposit; bridge at elysium.kinetiq.xyz). There is no test network: practice with POST /api/v1/intents/preview, which evaluates every policy and quotes fees without creating or signing anything. To keep a person in control, add their wallet to coOwners and set threshold to 2.
autonomous onboarding, curl
sh
# 1. Prove control of the owner key (secp256k1, EIP-4361). Nothing else is needed: no browser, no wallet app.curl -X POST "$ELYX_URL/api/v1/auth/challenge" -H "Content-Type: application/json"   -d '{ "address": "0x<owner address>" }'# -> { "message": "...", "nonceToken": "..." }   personal_sign the message (EIP-191), 0x signaturecurl -X POST "$ELYX_URL/api/v1/auth/token" -H "Content-Type: application/json"   -d '{ "address": "0x...", "message": "...", "signature": "0x...", "nonceToken": "...", "organizationName": "My agent" }'# -> { "token": "eot_..." }   the organization exists now# 2. Create the Safe account. The response is an EIP-1559 transaction prepared and simulated on Elysium.curl -X POST "$ELYX_URL/api/v1/accounts" -H "Authorization: Bearer $VOT" -H "Content-Type: application/json"   -d '{ "name": "Agent Ops", "type": "AGENT", "coOwners": [], "threshold": 1,        "vaults": [{ "name": "Operating Vault", "type": "OPERATING", "description": "" }] }'# -> { "id": "ptx_...", "transaction": { "chainId": 99901, "to": "0x...", "data": "0x...", "value": "0", ... }, "simulation": { "ok": true } }# 3. Sign the transaction with the owner key and submit the raw signed bytes.curl -X POST "$ELYX_URL/api/v1/tx/ptx_.../submit" -H "Authorization: Bearer $VOT" -H "Content-Type: application/json"   -d '{ "transaction": "0x02..." }'# -> { "signature": "0x<tx hash>", "result": { "accountId": "acc_...", "vaultId": "vlt_...", "vaultAddress": "0x..." } }   confirmed on Elysium# 4. Same pattern for deposits, POST /api/v1/agents, top-ups, withdrawals and executing approved Safe transactions.
autonomous onboarding, TypeScript
ts
// curl -o elyx.ts "$ELYX_URL/sdk/elyx.ts"   (single file, no dependencies; viem only for the key)import { Elyx, signInWithKey, viemSigner } from "./elyx";import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";const owner = viemSigner(privateKeyToAccount(generatePrivateKey()));   // persist the key securely, then fund the address with HYPEconst { token } = await signInWithKey(process.env.ELYX_URL!, owner, { organizationName: "My agent" });const elyx = new Elyx({ apiKey: token, baseUrl: process.env.ELYX_URL!, moduleAddress: "0x<ELYX module>" });const { result: { accountId, vaultId, vaultAddress } } = await elyx.owner.createAccount({ name: "Agent Ops" }, owner);await elyx.owner.deposit(vaultId, { asset: "HYPE", amount: "0.05" }, owner, { vaultAddress });const payee = await elyx.owner.createCounterparty({ name: "Vendor", type: "BUSINESS", address: "0x..." });const onboarding = await elyx.owner.createAgent(  { accountId, vaultId, name: "Atlas", kind: "PAYMENT", limits: [{ asset: "HYPE", amount: "0.02" }], period: "DAY", counterpartyIds: [payee.id], gasHype: "0.01" },  owner,  { vaultAddress },);if (onboarding.status !== "CONFIRMED") throw new Error("co-owners must sign first");const { agentId } = onboarding.result;const { execution } = await elyx.intents.submit({ agentId, action: "PAYMENT", asset: "HYPE", amount: "0.003", counterpartyId: payee.id });console.log(await elyx.executions.wait(execution.id));

Already have credentials? GET /api/v1/context returns your agent id, vault ids, allowed actions, remaining on-chain allowance and the counterparties you can pay, in one call.

Reference

Units

FieldUnitExample
Request amount (intents, payments, deposits, limits)Decimal string in token units"12.5" USDC
Balances, intent.amount, limits, simulation deltasInteger string in the smallest unit (USDC 6 decimals, HYPE 18)"12500000" = 12.5 USDC
amountUsd, policy amounts, allowancesInteger USD cents250000 = $2,500.00
Fees (feeQuote, preview fee)Integer string micro-USD (1e-6)"1879000" = $1.879
maxSlippageBpsBasis points50 = 0.50%
01

Quickstart

The base URL is the origin of your deployment. Every request carries a bearer credential. Both kinds are created in the developer dashboard, shown once and stored by ELYX only as SHA-256 hashes.

CredentialFormatScope
API keyBearer ek_live_...Organization. Read, submit intents and payments, pause agents, manage counterparties and webhooks. Cannot vote on approvals or change policies.
Agent sessionBearer eas_...One agent: its vaults, allowed actions, a USD spending allowance and an expiry. Can submit intents, read its own state and request changes.
1Set your environment
shell
sh
# The base URL is the origin of your ELYX deploymentexport ELYX_URL="https://your-elyx-deployment"# API key (organization scope), created in /developers/dashboardexport ELYX_API_KEY="ek_live_..."# Agent session token (one agent, its vaults and allowed actions)export ELYX_AGENT_TOKEN="eas_..."# Find your agentId, vaultId and counterparty ids with one callcurl "$ELYX_URL/api/v1/context" -H "Authorization: Bearer $ELYX_AGENT_TOKEN"
2Preview the decision and fee
preview
sh
curl -X POST "$ELYX_URL/api/v1/intents/preview" \  -H "Authorization: Bearer $ELYX_AGENT_TOKEN" \  -H "Content-Type: application/json" \  -d '{    "agentId": "agt_...",    "action": "SWAP",    "asset": "USDC",    "outputAsset": "HYPE",    "amount": "250",    "maxSlippageBps": 50  }'
3Submit the intent
submit
sh
curl -X POST "$ELYX_URL/api/v1/intents" \  -H "Authorization: Bearer $ELYX_AGENT_TOKEN" \  -H "Content-Type: application/json" \  -H "idempotency-key: rebalance-2026-10-05-001" \  -d '{    "agentId": "agt_...",    "action": "SWAP",    "asset": "USDC",    "outputAsset": "HYPE",    "amount": "250",    "maxSlippageBps": 50,    "justification": "Rebalance to target HYPE weight"  }'# 202 Accepted (200 with "replayed": true for a repeated idempotency key)# { "execution": { "id": "exe_...", "state": "CREATED", ... }, "replayed": false }
4Follow the execution

Poll until the state is terminal or AWAITING_APPROVAL, or subscribe to webhooks.

poll
sh
curl "$ELYX_URL/api/v1/executions/exe_..." \  -H "Authorization: Bearer $ELYX_AGENT_TOKEN"
02

Agent Accounts

An account is a Safe smart account on Elysium mainnet. Owners are wallets that sign its transactions; the threshold sets how many must sign. Each agent receives a dedicated executor key, derived server-side and never stored, registered as a delegate of the ELYX spending limit module on its vault. It is never a Safe owner.

Creating an agent, changing its spending limits and revoking it are Safe transactions signed by owner wallets: in the app, or from an agent runtime holding an owner key through the SDK. Through the API you read agents and pause or resume them.

GET /api/v1/agents/:id
sh
curl "$ELYX_URL/api/v1/agents/agt_..." \  -H "Authorization: Bearer $ELYX_AGENT_TOKEN"# {#   "agent": { "id": "agt_...", "status": "ACTIVE", "chainStatus": "ACTIVE",#              "executor": "0x<executor address>", "primaryVaultId": "vlt_...", ... },#   "limits": [ { "asset": "USDC", "amount": "...", "period": "DAY",#                 "destinations": ["..."], "remaining": "...", ... } ],#   "wallet": { "balances": [ ... ], "totalUsd": 0 },#   "gasWei": "0"# }
PATCH /api/v1/agents/:id
sh
# Pause or resume an agent (API key)curl -X PATCH "$ELYX_URL/api/v1/agents/agt_..." \  -H "Authorization: Bearer $ELYX_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "status": "PAUSED" }'
A paused agent keeps its on-chain limits, but ELYX signs nothing for it until it is resumed. Revoked agents cannot be resumed.
03

Vaults

A vault is a Safe owned by the account's owners (the first vault is the account itself). Funds stay in the vault under owner control. Agents draw from it only through their on-chain spending limits (amount per period, per asset, to allow-listed destinations) or through owner-signed Safe transactions.

GET /api/v1/vaults
sh
curl "$ELYX_URL/api/v1/vaults" \  -H "Authorization: Bearer $ELYX_API_KEY"# [ { "id": "vlt_...", "name": "...", "type": "PAYMENT", "address": "<vault PDA>",#     "status": "ACTIVE", "balances": [ { "asset": "USDC", "amount": "<base units>",#     "decimals": 6, "usdValue": 0 } ], "totalUsd": 0, ... } ]

Token amounts are base-unit strings. USD values are integer cents. Vaults are created, funded and frozen by owners in the app. A frozen vault blocks every intent that draws from it.

04

Policies

Policies are deterministic rules evaluated before anything is signed. There is no model in the loop: the same intent and state always produce the same decision. USD amounts are cents. Empty scopes apply to every agent and vault in the organization.

Rule kindParameters
DAILY_LIMITamountUsd
MAX_TRANSACTIONamountUsd
APPROVAL_THRESHOLDamountUsd
ALLOWED_ASSETS / BLOCKED_ASSETSassets
ALLOWED_PROTOCOLS / BLOCKED_PROTOCOLSprotocols
ALLOWED_COUNTERPARTIES / BLOCKED_COUNTERPARTIEScounterpartyIds
ALLOWED_ACTIONSactions
TIME_WINDOWstartHourUtc, endHourUtc, days
EXECUTION_FREQUENCYmaxPerHour
MAX_SLIPPAGEbps
COUNTERPARTY_DAILY_LIMITasset, amountUsd

Every evaluation returns a decision object. It is attached to the execution as policy and returned by the preview endpoint as evaluation.

PolicyEvaluation
json
{  "decision": "REQUIRE_APPROVAL",  "triggeredPolicies": [    {      "policyId": "pol_...",      "reason": "TRANSACTION_ABOVE_THRESHOLD",      "outcome": "REQUIRE_APPROVAL",      "message": "..."    }  ],  "approvalRequirement": {    "rule": { "kind": "THRESHOLD", "required": 1, "of": ["OWNER"] },    "approvalsRequired": 1  },  "evaluatedPolicyIds": ["pol_..."],  "evaluatedAt": "<ISO 8601>"}
  • ALLOW executes through the agent's spending limit.
  • REQUIRE_APPROVAL waits for owners. For vault funds the agent drafts a Safe transaction that owners sign (EIP-712, no gas) and ELYX executes.
  • BLOCK stops the execution. Nothing is signed.

Policies are changed by owners. Agents propose changes through change requests (see Approvals).

05

Execution

ActionWhat it doesRequires
SWAPSwap from the agent's working wallet, routed through KyberSwap.outputAsset, maxSlippageBps
PAYMENTPay a counterparty from a vault.counterpartyId
TRANSFERMove value from a vault to a counterparty.counterpartyId
ALLOCATEMove capital from a vault to the agent's working wallet.No extra field
RETURNReturn capital from the working wallet to the vault.No extra field

Body fields: agentId, vaultId (optional, defaults to the agent's primary vault), action, asset, amount (decimal token amount as a string), and optionally outputAsset, counterpartyId, maxSlippageBps (0 to 5000), memo, justification.

POST /api/v1/intents/preview
json
{  "amountUsd": 25000,             // cents, priced server-side  "evaluation": { "decision": "ALLOW", "triggeredPolicies": [], ... },  "fee": {                        // USD micro-units (1e-6), as strings    "networkFee": "...",    "executionFee": "...",    "providerFee": "...",    "totalFee": "..."  },  "route": "KYBER",               // SPENDING_LIMIT | SAFE_TX | AGENT_WALLET | KYBER  "estimatedTime": "About 15 seconds"}

Execution states

CREATEDIntent recorded with its idempotency key.
VALIDATINGAgent, vault, session scope and balance checks.
POLICY_CHECKDeterministic policy evaluation.
AWAITING_APPROVALPolicy requires owners to approve. Execution resumes after the decision.
APPROVEDAllowed by policy, or approved by owners.
SIMULATINGTransaction built and simulated against mainnet, then compared with the intent.
READYSimulation matched the intent. Ready to sign.
SUBMITTEDSigned and sent to Elysium.
CONFIRMEDConfirmed on-chain.
SETTLEDFinal. Settlement recorded with transaction hash, block and fee.
BLOCKEDFinal. Policy, simulation mismatch or rejection. Nothing was signed.
FAILEDFinal. Check error and fundsMayHaveMoved before retrying.
CANCELLEDFinal. Cancelled before submission.
Idempotency: send an idempotency-key header (8 to 128 characters). The same key with the same body returns the original execution with replayed: true. The same key with a different body returns 409 IDEMPOTENCY_CONFLICT. Retry network failures with the same key.
06

Payments

A payment is a PAYMENT intent from a vault to a registered counterparty, with optional reference, invoice and memo. Payable assets: USDC, HYPE. The destination must be on the agent's on-chain allow-list.

POST /api/v1/payments
sh
curl -X POST "$ELYX_URL/api/v1/payments" \  -H "Authorization: Bearer $ELYX_API_KEY" \  -H "Content-Type: application/json" \  -H "idempotency-key: inv-0042-payment" \  -d '{    "agentId": "agt_...",    "counterpartyId": "cp_...",    "asset": "USDC",    "amount": "980.00",    "reference": "PO-118",    "invoice": "INV-0042",    "memo": "October hosting"  }'# { "paymentId": "pay_...", "execution": { "id": "exe_...", "state": "CREATED", ... } }
/api/v1/payments requires an API key. Agent session tokens pay by submitting a PAYMENT intent to /api/v1/intents.
07

Approvals

Approvals are decided by organization members signing with their wallets (owners by default), never by API keys or agents. For vault funds, owners sign the Safe transaction itself (EIP-712 typed data, no gas). For swaps from an agent's working wallet and for change requests, owners sign an off-chain approval message with their wallet.

GET /api/v1/approvals
sh
curl "$ELYX_URL/api/v1/approvals?status=PENDING" \  -H "Authorization: Bearer $ELYX_API_KEY"

Agents ask for changes instead of working around limits. Types: POLICY_CHANGE, COUNTERPARTY, PROTOCOL, LIMIT_INCREASE, AGENT_PERMISSION.

POST /api/v1/agents/:id/requests
sh
curl -X POST "$ELYX_URL/api/v1/agents/agt_.../requests" \  -H "Authorization: Bearer $ELYX_AGENT_TOKEN" \  -H "Content-Type: application/json" \  -d '{    "type": "LIMIT_INCREASE",    "title": "Raise daily limit",    "requestedChange": "Daily limit to 5,000 USDC",    "currentRule": "Daily limit 2,000 USDC",    "justification": "Settlement volume doubled this week."  }'# Returns the created approval request (status PENDING) for owners to decide.
08

SDK

The SDK is one dependency-free TypeScript file for Node 18+, Bun, Deno, browsers and edge runtimes. Download it from /sdk/elyx.ts (curl -o elyx.ts $ELYX_URL/sdk/elyx.ts). It is not on npm, so there is nothing to install.

agent.ts
ts
// curl -o elyx.ts "$ELYX_URL/sdk/elyx.ts"import { Elyx, ElyxError } from "./elyx";const elyx = new Elyx({  apiKey: process.env.ELYX_AGENT_TOKEN!, // ek_live_... or eas_...  baseUrl: process.env.ELYX_URL!,});const agent = elyx.agent("agt_...");// Check the decision and fee first. Nothing is created or signed.const preview = await elyx.intents.preview({  agentId: agent.id, action: "PAYMENT", asset: "USDC", amount: "120", counterpartyId: "cp_...",});if (preview.evaluation.decision !== "BLOCK") {  const { execution } = await agent.pay(    { counterpartyId: "cp_...", asset: "USDC", amount: "120", memo: "INV-0042" },    { idempotencyKey: "inv-0042" },  );  const final = await elyx.executions.wait(execution.id, { timeoutMs: 90_000 });  // SETTLED | BLOCKED | FAILED | CANCELLED | AWAITING_APPROVAL}try {  await agent.swap({ from: "USDC", to: "HYPE", amount: "250", maxSlippageBps: 50 });} catch (e) {  if (e instanceof ElyxError) console.error(e.status, e.code, e.message);}
surface
ts
new Elyx({ apiKey, baseUrl })elyx.intents.submit(body, { idempotencyKey? })elyx.intents.preview(body)elyx.executions.get(id)elyx.executions.list({ agentId?, state?, limit? })elyx.executions.wait(id, { timeoutMs?, intervalMs? })elyx.payments.create(body, { idempotencyKey? })   // API keyelyx.approvals.list(status?)elyx.agents.list()elyx.agents.get(id)elyx.agents.requestChange(agentId, body)elyx.webhooks.verify(rawBody, header, secret)      // async, Web Cryptoelyx.context()                                     // ids, scope, limits, unitssignInWithKey(baseUrl, ownerSigner)                 // -> { token: "eot_..." }elyx.owner.createAccount(body, signer)             // prepare, sign, submit, confirmelyx.owner.deposit(vaultId, body, signer)elyx.owner.createCounterparty(body)elyx.owner.createAgent(body, signer)elyx.owner.issueSession(body)                      // -> { token: "eas_..." }elyx.owner.approve(approvalId, signer)elyx.owner.withdraw(vaultId, body, signer)elyx.owner.run(path, body, signer)                 // any endpoint returning a prepared transactionsignPreparedTransaction(base64, signer)             // adds one signature, keeps the restconst agent = elyx.agent(agentId)agent.swap({ from, to, amount, maxSlippageBps })agent.pay({ counterpartyId, asset, amount, memo })agent.allocate({ asset, amount })agent.returnCapital({ asset, amount })agent.request({ type, title, requestedChange, justification })
A BLOCKED execution is not an error. The request succeeded and the policy engine decided not to sign. Read execution.policy.triggeredPolicies.
09

API

JSON over HTTPS. Errors return { "error": { "code", "message" } }. Agent session tokens only see their own agent's executions, approvals and detail.

MethodPathDescriptionCredential
POST/api/v1/auth/challengeStart sign-in for an owner key. Public.None
POST/api/v1/auth/tokenSigned challenge for an owner token (eot_...). Public.None
GET/api/v1/contextIds, scope, on-chain limits, units and rate limits for this credential.Any
POST/api/v1/tx/:id/submitSubmit an owner-signed prepared transaction.Owner
POST/api/v1/accountsCreate a Safe account. Returns a prepared transaction.Owner
POST/api/v1/agentsCreate and authorize an agent. Returns a prepared transaction.Owner
POST/api/v1/vaults/:id/depositDeposit from the owner wallet. Prepared transaction.Owner
POST/api/v1/vaults/:id/withdrawOwner withdrawal, step 1 of 2. Prepared transaction.Owner
GET/api/v1/approvals/:id/safe-typed-dataEIP-712 typed data an owner signs for a Safe transaction (no gas).Owner
POST/api/v1/approvals/:id/safe-voteRecord an owner's EIP-712 signature or signed rejection.Owner
POST/api/v1/approvals/:id/executeOwner executes an approved Safe transaction. Prepared transaction.Owner
POST/api/v1/developers/sessionsIssue an agent session token (eas_...).Owner
POST/api/v1/intentsSubmit an intent. Header idempotency-key.Key, Agent
POST/api/v1/intents/previewPolicy decision, fee quote and route. Read-only.Key, Agent
GET/api/v1/executionsList executions. ?agentId= &state= &limit=Key, Agent
GET/api/v1/executions/:idOne execution with steps, policy, simulation, signature.Key, Agent
POST/api/v1/paymentsCreate a payment to a counterparty.Key
GET/api/v1/paymentsList payments.Key
GET/api/v1/approvalsList approvals. ?status=PENDINGKey, Agent
POST/api/v1/agents/:id/requestsAgent change request for owners to decide.Key, Agent
GET/api/v1/agentsList agents.Key, Agent
GET/api/v1/agents/:idAgent, on-chain limits, working wallet.Key, Agent
PATCH/api/v1/agents/:idPause or resume: { status }.Key
GET/api/v1/vaultsVaults with live balances.Key
GET/api/v1/policiesPolicies.Key
GET/api/v1/counterpartiesCounterparties.Key
POST/api/v1/webhooksCreate an endpoint. Returns the secret once.Key
PATCH/api/v1/webhooks/:idEnable or disable: { enabled }.Key
GET/api/v1/usageUsage and fees, last 30 days.Key

Errors

StatusCodeMeaning
400VALIDATION_ERRORBody failed validation. The message lists each field.
401UNAUTHORIZEDMissing, invalid, revoked or expired credential.
403FORBIDDENCredential lacks permission, or the session scope does not allow the intent.
404NOT_FOUNDResource does not exist in your organization.
409IDEMPOTENCY_CONFLICTIdempotency key reused with a different body.
429RATE_LIMITEDToo many requests. Retry-After header and error.retryAfterSeconds give the delay.
409TX_TAMPEREDThe submitted transaction differs from the prepared one. Nothing was sent.
503PRICE_UNAVAILABLEUSD pricing unavailable. Nothing was created.

Rate limits

ScopeLimitNotes
Owner token / signed-in user120 per minutePer member.
API key600 per minutePer key.
Agent sessionConfigured per tokenDefault 30 per minute, set when the token is issued.
Organization2,000 per minuteAcross all credentials.
Loop protection12 actions per minute, 3 identical intents per 5 minutes, 500 per dayPer agent. Tripping it blocks the intent and pauses the agent until an owner resumes it.

429 responses carry a Retry-After header and error.retryAfterSeconds. Full machine-readable description: /openapi.json.

10

Webhooks

Endpoints must be public HTTPS URLs. Each delivery is a POST with a JSON body, signed with your endpoint secret. Failed deliveries are retried twice (after 1 and 4 seconds) with an 8 second timeout per attempt. Delivery results appear in the developer dashboard.

POST /api/v1/webhooks
sh
curl -X POST "$ELYX_URL/api/v1/webhooks" \  -H "Authorization: Bearer $ELYX_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "url": "https://example.com/elyx/webhooks",    "events": ["agent.execution.settled", "agent.execution.blocked", "approval.required"]  }'# { "id": "whk_...", "secret": "whsec_..." }   The secret is shown once.
agent.execution.requestedAn intent was submitted.
agent.execution.approvedOwners approved an execution.
agent.execution.blockedPolicy or simulation blocked an execution.
agent.execution.settledAn execution settled on Elysium.
payment.createdA payment was created.
payment.settledA payment settled.
approval.requiredAn approval request needs owners.
approval.completedAn approval request was decided.
policy.changedA policy was created or changed.
vault.fundedA vault received a deposit.
vault.frozenA vault was frozen.

Signature

Header elyx-signature: t=<unix>,v1=<hex hmac_sha256(secret, "<t>.<body>")>. Compute the HMAC over the timestamp, a period and the raw body, compare in constant time and reject timestamps more than 300 seconds from now.

delivery
http
POST /elyx/webhookscontent-type: application/jsonuser-agent: ELYX-Webhooks/1elyx-signature: t=1791200000,v1=5f2c...e81a{ "id": "whd_...", "type": "agent.execution.settled", "createdAt": "<ISO 8601>", "data": { ... } }
app/elyx/webhooks/route.ts
ts
import { createHmac, timingSafeEqual } from "node:crypto";const TOLERANCE_S = 300;export function verifyElyxSignature(rawBody: string, header: string | null, secret: string): boolean {  if (!header) return false;  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2) as [string, string]));  const t = Number(parts.t);  if (!Number.isInteger(t) || !parts.v1) return false;  // Reject old or future timestamps (replay protection)  if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_S) return false;  const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"), "hex");  const received = Buffer.from(parts.v1, "hex");  return expected.length === received.length && timingSafeEqual(expected, received);}// Next.js route handlerexport async function POST(req: Request) {  const rawBody = await req.text(); // verify the exact bytes you received  if (!verifyElyxSignature(rawBody, req.headers.get("elyx-signature"), process.env.ELYX_WEBHOOK_SECRET!)) {    return new Response("invalid signature", { status: 400 });  }  const event = JSON.parse(rawBody) as { id: string; type: string; data: Record<string, unknown> };  // Deduplicate on event.id: deliveries can be retried.  return new Response("ok");}

With the SDK: await elyx.webhooks.verify(rawBody, header, secret) returns { ok: true } or { ok: false, reason }.