Watches and webhooks
A watch runs a check on a schedule and posts to your webhook when the answer changes, so an agent does not have to poll. Create one in the cabinet or with POST /account/watches:
| kind | Runs | Notifies when |
|---|---|---|
token | token_deep_check, 6 credits, every 60 min (at least 15) | the verdict changes, top-10 concentration (as listed or merged) or the largest entity moves by the threshold (5 points), or the number of linked wallets changes |
wallet | wallet_check, 6 credits, every 60 min (at least 15) | the verdict, behaviour class or known exchange changes, or the wallet creates tokens |
pool | pool_health, 6 credits, every 60 min (at least 15) | TVL moves by the threshold in percent (10), or liquidity flow, sandwich or bot share moves by it in points |
launches | new_launches, 1 credit, every 2 min (at least 1) | new launches appear, each once, optionally above min_liquidity_usd |
curl -X POST "https://api.dexra.dev/account/watches" \
-H "Authorization: Bearer dxr_…" -H "content-type: application/json" \
-d '{"kind":"token","target":"DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263","webhook_url":"https://your-agent.example/hooks/dexra","options":{"threshold":5}}'const watch = await dexra.createWatch({
kind: 'token',
target: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
webhook_url: 'https://your-agent.example/hooks/dexra',
options: { threshold: 5 },
})
// Keep watch.secret: it is shown once.watch = dexra.create_watch(
kind="token",
target="DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
webhook_url="https://your-agent.example/hooks/dexra",
threshold=5,
)
# Keep watch["secret"]: it is shown once.- The answer carries
secret(whsec_…) once; keep it to verify webhooks. - Each run uses credits like a call and shows in the call log as
watch. Without credits left a run is skipped, never paid per call. - The first run records a baseline; later runs notify on change. Free may have 1 watch, plans 10.
- Webhooks must be public https URLs; redirects are not followed.
POST /account/watches/<id>/testsends aping.
Every webhook is a JSON POST with X-Dexra-Event (changed, new_launches or ping) and a signature:
Event
{
"event": "changed",
"watch": { "id": 7, "kind": "token", "target": "DezX…" },
"at": "2026-10-16T19:22:57Z",
"tool": "token_deep_check",
"changes": [{ "field": "top10_merged_pct", "before": 31.2, "after": 44.8 }],
"verdict": "avoid",
"summary": "…",
"result": {}
}X-Dexra-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + raw body with your secret. Check it on the raw body and reject old timestamps:
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(secret, header, rawBody) {
const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')))
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(part.split("=", 1) for part in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])