Skip to main content
EchoZero uses three distinct signing schemes depending on direction and transport. Using the wrong algorithm is the most common integration mistake.
DirectionHeadersTimestampPayload signed
REST API (opt-in)x-signature, x-timestampEpoch mstimestamp + METHOD + path + body
Inbound agent signalsX-EZ-Signature, X-EZ-TimestampEpoch secondstimestamp + "." + canonicalJson
Outbound signal.executionx-echozero-signature, x-echozero-timestampISO in payloadtimestamp + "." + jsonBody

Inbound agent HTTP signals

Endpoint: POST https://mcp.echozero.app/api/public/agent-signals/{agentId} No developer API key. Authenticate with the per-agent signing secret (ezw_...) revealed once at provision or rotation.
Always send a stable idempotencyKey on every inbound signal, including natural-language text and legacy payloads. Without it, a captured or retried signed request can execute the trade again. See Signal envelope.

Headers

HeaderRequiredDescription
Content-TypeYesapplication/json
X-EZ-TimestampYesUnix time in seconds
X-EZ-SignatureYesLowercase hex HMAC-SHA256

Signing algorithm

  1. Serialize the request body as canonical JSON - keys sorted alphabetically at every object level, undefined fields omitted.
  2. Build: payloadString = "${unixSeconds}.${canonicalJson}"
  3. Sign: HMAC_SHA256(signingSecret, payloadString) → lowercase hex
SECRET='ezw_...'
TS=$(date +%s)
CANON='{"text":"BUY SOL 500 USDC"}'
PAYLOAD="$TS.$CANON"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST "https://mcp.echozero.app/api/public/agent-signals/$AGENT_ID" \
  -H "Content-Type: application/json" \
  -H "X-EZ-Timestamp: $TS" \
  -H "X-EZ-Signature: $SIG" \
  -d '{"text":"BUY SOL 500 USDC"}'

SDK helpers

The echozero-sdk provides signInboundWebhook / sign_inbound_webhook in TypeScript, Python, Go, and Rust with canonical JSON built in.
import { signInboundWebhook } from '@echozero/sdk';

const body = { text: 'BUY SOL 500 USDC' };
const headers = await signInboundWebhook({
  signingSecret: process.env.EZW_SECRET!,
  body,
});
// headers['X-EZ-Timestamp'], headers['X-EZ-Signature']

Replay protection

  • Requests with timestamp skew beyond ±5 minutes are rejected with 401.
  • Use a unique idempotencyKey per signal so retries are safe no-ops.

Verify inbound (Python)

import hmac, hashlib, json

def verify_inbound(secret: str, body: dict, timestamp: str, signature: str) -> bool:
    # Use SDK canonical_json in production; see signature test vectors
    from echozero.hmac import canonical_webhook_body  # when SDK published
    canon = json.dumps(body, sort_keys=True, separators=(",", ":"))
    payload = f"{timestamp}.{canon}"
    expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
Golden vectors: Signature test vectors.

Outbound execution webhooks

When your agent has a webhookUrl, EchoZero POSTs execution results after trade attempts.

Signing secret

Use the agent webhook signing secret configured on the agent (webhookUrl + secret pair). Rotate by updating the agent record in Dev Portal or PATCH /api/v1/agents/{id}.

Delivery contract

PropertyValue
MethodPOST
Timeout10 seconds
RetriesNone (single attempt; implement idempotent handling on your side)
Eventssignal.execution, signal.execution.failed

Event catalog

{
  "event": "signal.execution",
  "signalId": "...",
  "developerAgentId": "...",
  "status": "executed",
  "executionResult": {
    "success": true,
    "txHash": "...",
    "executedAmountUsd": 100,
    "executedAt": "2026-07-06T12:00:00.000Z"
  },
  "timestamp": "2026-07-06T12:00:00.000Z"
}

Verify on your server

Headers:
HeaderDescription
x-echozero-timestampSame ISO timestamp as payload timestamp field
x-echozero-signatureHMAC_SHA256(webhookSecret, timestamp + "." + rawJsonBody)
import hmac, hashlib

def verify_outbound(secret: str, raw_body: bytes, timestamp: str, signature: str) -> bool:
    payload = f"{timestamp}.{raw_body.decode()}"
    expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
Reject requests outside a 5-minute timestamp window and treat duplicate signalId deliveries as idempotent.

Provider webhooks (Telegram, Discord)

When EchoZero hosts signal ingestion for Telegram or Discord bots, provider-specific secrets apply. These routes do not use developer API keys.

Telegram

Endpoint: POST /api/webhooks/telegram/signal-bot When the server has SIGNAL_TELEGRAM_WEBHOOK_SECRET configured, every request must include:
X-Telegram-Bot-Api-Secret-Token: <same value passed to Telegram setWebhook secret_token>
Configure this when registering your bot webhook via Telegram’s setWebhook API.

Discord

Endpoint: POST /api/webhooks/discord/signal-bot When SIGNAL_DISCORD_WEBHOOK_SECRET is set:
X-Discord-Signal-Webhook-Secret: <matching secret>
Used when Discord Gateway ingest is off or for webhook-based testing.

REST API HMAC (developer routes)

For authenticated developer API calls (/api/v1/*), optional HMAC uses your API secret with millisecond timestamps and method+path signing. See API keys and HMAC.
Do not use REST API HMAC headers (x-signature / x-timestamp) on inbound agent signal POSTs. Agent ingress requires X-EZ-Signature / X-EZ-Timestamp with the per-agent ezw_ secret.

Security checklist

  • Store signing secrets in a secrets manager - they are shown only once at creation/rotation.
  • Use HTTPS for all webhook URLs.
  • Enforce timestamp windows on every inbound webhook you receive.
  • Deduplicate by idempotencyKey (inbound) or signalId (outbound).
  • Rotate compromised secrets via rotateInboundWebhookSigningSecret or API key revocation in the Dev Portal.