Use these vectors to verify your signer matches EchoZero and the official SDK.
Inbound vs outbound: Inbound agent signals sign canonical JSON (sorted keys, known field set). Outbound signal.execution webhooks sign the raw request body string. REST HMAC signs timestamp + METHOD + path + body.
REST HMAC (POST)
| Field | Value |
|---|
| Secret | test_secret |
| Timestamp (ms) | 1710000000000 |
| Method | POST |
| Path | /api/api-keys |
| Body | {"name":"SDK HMAC Test"} |
| String to sign | 1710000000000POST/api/api-keys{"name":"SDK HMAC Test"} |
Expected x-signature | 274c9ff280eadf751530e9e7fce2c2a573d8676b13b7108fe353407df7cc9e00 |
Inbound webhook (text + idempotencyKey)
| Field | Value |
|---|
| Secret | test_secret |
| Timestamp (s) | 1710000000 |
| Body | {"idempotencyKey":"sdk-test-1","text":"BUY SOL 500 USDC"} |
| Canonical JSON | {"idempotencyKey":"sdk-test-1","text":"BUY SOL 500 USDC"} |
| Payload | 1710000000.{"idempotencyKey":"sdk-test-1","text":"BUY SOL 500 USDC"} |
Expected X-EZ-Signature | 1d30d896fc609e62bcf9be991c1dc1177a9c63219909869ef2846bad4685de3b |
Inbound webhook (structured buy, unknown fields stripped)
| Field | Value |
|---|
| Secret | secret |
| Timestamp (s) | 1710000000 |
| Body fields | eventType, idempotencyKey, reasoning, tokenAddress, amount |
| Extra fields | Ignored for signing (e.g. unknownField) |
Expected X-EZ-Signature | be420f61d91e6b871481774c62f972f0aafa6f5f1a727ba1e4a32558784f77c3 |
Canonical JSON for that vector:
{"amount":500,"eventType":"buy","idempotencyKey":"test-1","reasoning":"test","tokenAddress":"So11111111111111111111111111111111111111112"}
Inbound canonicalization rules
- Only signing-eligible keys are included (
text, idempotencyKey, and structured fields from the #1098 schema).
- Object keys are sorted alphabetically at every nesting level.
- Unknown top-level fields are omitted from the canonical form.
- Numbers must be finite JSON numbers (no
NaN / Infinity).
Match the SDK implementation: inbound_canonical.rs (reference for all languages).
Outbound signal.execution
| Field | Value |
|---|
| Secret | webhook_secret |
| Timestamp | 2026-07-06T12:00:00.000Z |
| Raw body | {"event":"signal.execution","signalId":"sig_1","developerAgentId":"agent_1","status":"executed","timestamp":"2026-07-06T12:00:00.000Z"} |
| Payload | {timestamp}.{raw_body} |
| Algorithm | HMAC-SHA256 → lowercase hex |
Expected x-echozero-signature | 249d84b9e89206f6aee4b124f728c945b805666b3102d76b913a17f1bf568574 |
Verify with the same raw bytes you receive on the wire (do not re-serialize JSON).
Inbound webhook (nested buy with triggers + context)
| Field | Value |
|---|
| Secret | test_secret |
| Timestamp (s) | 1710000000 |
| Canonical JSON | see below |
Expected X-EZ-Signature | 3afa3bf158361edcb46dbeaee6b128f7cd1c78baa28824f2355de1223671e67a |
{"amount":500,"chain":"solana","context":{"indicators":["VWAP","RSI"],"marketCondition":"trending","price":148.5},"eventType":"buy","idempotencyKey":"nested-vector-001","instrument":{"tokenAddress":"So11111111111111111111111111111111111111112"},"reasoning":"Nested canonicalization test.","symbol":"SOL","triggers":{"slPrice":{"targetType":"price","value":165},"tps":[{"label":"TP1","sizePct":50,"targetType":"price","value":180}]}}
Nested objects must be sorted at every level (slPrice before tps; targetType before value inside each TP). This is where cross-language signing bugs usually appear.
curl self-test (REST)
SECRET="test_secret"
TS="1710000000000"
REQ_PATH="/api/api-keys"
BODY='{"name":"SDK HMAC Test"}'
SIG=$(printf '%s' "${TS}POST${REQ_PATH}${BODY}" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
echo "$SIG"
# expect 274c9ff280eadf751530e9e7fce2c2a573d8676b13b7108fe353407df7cc9e00