## 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](https://dexra.dev/app/watches) 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**

```sh
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}}'
```

**TypeScript**

```ts
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.
```

**Python**

```python
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>/test` sends a `ping`.

Every webhook is a JSON POST with `X-Dexra-Event` (`changed`, `new_launches` or `ping`) and a signature:

```json
{
    "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:

**Node**

```ts
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))
  }
```

**Python**

```python
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"])
```

Source: https://dexra.dev/docs/watches
