---
name: hookpulse
description: Operate HookPulse (webhook/cron dead-man) as an agent — endpoints, ingest, events, billing x402, contact. Use when working with HookPulse or wendel-hookpulse.
---

# HookPulse — skill para agentes

**Live:** https://hookpulse.net  
**Descoberta:** `GET /api/` · `/llms.txt` · `/openapi.json` · `GET /api/` traz `quota` e `mcp`  
**SEO:** `canonical`/`og:*`/`/robots.txt`/`/sitemap.xml` · **IndexNow = CI pós-smoke** (não rodar na mão; ver `AGENTS.md` § SEO + IndexNow)  


**UI (humanos):** rotina → frequência/alerta → integração escolhida → estado. Agentes começam em `/developers` e consomem contratos, MCP e llms.
**Catálogo:** `src/lib/apidocs.js`  
**MCP (remoto, recomendado):** `POST https://hookpulse.net/mcp` — Streamable HTTP, JSON-RPC 2.0.
Pluga direto no cliente MCP; não precisa deste repositório. Confira com `GET https://hookpulse.net/mcp`.
**MCP (stdio, local):** `node ~/src/mm/scripts/mcp/server.mjs --product hookpulse`

Paridade UI/API/skill/MCP no mesmo commit: `AGENTS.md` do produto e `AGENTS-API.md` da raiz.

## Auth

- `POST /api/guest` → `X-Guest-Token: hp_…` (token assinado pela conta da casa; não há tabela de
  convidados aqui). O token antigo, sem assinatura, vale enquanto ainda for dono de algo sem conta.
- Conta MM (pessoa, no navegador): entra em `/conta/global` (código ou link por e-mail, senha ou
  Google); o id da conta já é o dono aqui. A sessão é cookie HttpOnly e escrita leva
  `X-CSRF-Token` de `/api/auth/bootstrap`. `POST /api/auth/start` e `/verify` respondem 410.
- Claim: a tela da conta faz sozinha logo depois de entrar — `POST /api/auth/claim` com a sessão
  da conta e o cookie `guest` deste navegador (ou `guest_token` no corpo). Passa monitores,
  equipamentos e medidas, painel de status e chave de ingest para a conta numa transação; depois
  disso o token não é dono de mais nada. Repetir não faz mal (move zero).
- Contato: `POST /api/contact` com `name`, `email` e `message` — grátis para gente e agente, sem captcha nem pagamento; uma mensagem a cada 10 s por rede (a que chega antes espera a vez).

## Trial — 90 dias grátis por conta (loop inteiro por API)

A conta MM tem **90 dias sem paywall de USO**, contados do PRIMEIRO USO dela no HookPulse (uma
vez por conta; trocar o e-mail da conta não renova): endpoints além da franquia e intervalos
rápidos saem de graça (rate limit/teto continuam). Não há linha de conta neste produto; a sessão
é da página `/conta/global` no navegador (não há bearer de conta para agentes):

```js
// no navegador, já com a sessão da conta MM neste produto
const me = await fetch("https://hookpulse.net/api/me", { credentials: "same-origin" }).then(r => r.json());
// me.trial = {days, active, days_left, ends_at}; 402 vira 201/200 enquanto o trial durar
```

⚠️ **Exceção:** o **aviso por e-mail** ($0.10 por destino a partir do 2º) CONTINUA pago no
trial e com a cobrança desligada — cada miss vira envio SES para endereço que você escolheu, custo que cresce com o uso.
O caminho barato de avisos continua sendo `alert_url` (webhook grátis).
Estado: `trial` em `GET /api/billing` (com Bearer) ou `GET /api/me`. Guest não tem trial.

## Operações (MCP ↔ HTTP)

| Tool | HTTP |
|------|------|
| `api_index` | `GET /api/` |
| `create_guest` | `POST /api/guest` |
| `list_endpoints` | `GET /api/endpoints` |
| `create_endpoint` | `POST /api/endpoints` — **`name` é obrigatório** (11º+, interval&lt;90s ou agenda apertada → 402) |
| `get_endpoint` | `GET /api/endpoints/:id` |
| `delete_endpoint` | `DELETE /api/endpoints/:id` |
| `list_events` | `GET /api/endpoints/:id/events` |
| `ping_ingest` | `GET /in/:id` |
| `signal_start` | `GET /in/:id/start` — começou (NÃO limpa o relógio do dead-man) |
| `signal_fail` | `GET /in/:id/fail` — terminou mal; o cron avisa em ≤ 5 min |
| `signal_exit_code` | `GET /in/:id/<0-255>` — o `$?` do script; 0 é sucesso |
| `ingest_key` | `GET /api/ingest-key` — a chave que deixa o ping criar o próprio monitor |
| `ingest_key_rotate` | `DELETE /api/ingest-key` — gira; a anterior para de criar |
| `ping_slug` | `GET /in/:chave/:slug` — cria no primeiro ping, dentro da franquia |
| `import_crontab` | `POST /api/import/crontab` — proposta; `?apply=1` cria |
| `list_environments` | `GET /api/ambientes` — os equipamentos identificados, com as medidas compatíveis de cada um |
| `create_environment` | `POST /api/ambientes` — abre a identificação; devolve `passos` e `sonda` (um comando só) |
| `get_environment` | `GET /api/ambientes/:id` — estado da identificação e capacidades detectadas |
| `report_environment` | `POST /api/ambientes/:id/relatorio` — manda o relatório; a URL é a credencial e vale uma hora |
| (sonda) | `GET /api/ambientes/:id/sonda.sh` — o mesmo script que `sonda.envia.posix`, indentado e comentado (`?modo=mostra` = a variante que só imprime) |
| `update_environment` | `PATCH /api/ambientes/:id` — renomeia o equipamento E as medidas dele |
| `delete_environment` | `DELETE /api/ambientes/:id` — apaga o equipamento e todas as medidas numa chamada |
| `list_coletas` | `GET /api/coletas` — duas vistas: `coletas` (uma por medida, com `ativa`) e `equipamentos` (uma por máquina, com UM comando que faz todas e UMA linha de cron) |
| `create_coleta` | `POST /api/coletas` — `nome` do equipamento ("pc home"), `grupo` opcional; devolve `comando` |
| `update_collection` | `PATCH /api/coletas/:id` — renomeia, ou `ativa:false` para parar de ler sem apagar |
| `delete_collection` | `DELETE /api/coletas/:id` — apaga uma medida; a resposta traz `remover`, a linha do crontab |
| (frota) | `GET /api/coletas/serie?n=60` — as últimas leituras de TODOS os seus equipamentos numa chamada |
| `serie_coleta` | `GET /api/coletas/:id/serie?n=60` — as últimas leituras de um; autoriza pela SESSÃO, nunca pelo id |
| `billing` | `GET /api/billing` |
| `list_templates` | `GET /api/templates` (curl/cron/n8n + JSON do miss **e** da recuperação) |
| `status_feed_url` | `GET /api/status-feed` → `{token, json, rss}` — URL pública com o status de TODOS |
| `status_feed_rotate` | `DELETE /api/status-feed` — gira o token; a URL anterior morre na hora |
| (público) | `GET /s/:token` (página HTML) · `GET /s/:token.json` · `GET /s/:token.rss` — sem header nenhum |
| `contact` | `POST /api/contact` — grátis para gente e agente, sem captcha nem pagamento; uma mensagem a cada 10 s por rede (a que chega antes espera a vez) |
| (operador) | `GET /api/metrics` Bearer `METRICS_TOKEN` |

Fluxo típico: guest → create endpoint (`alert_to` opcional) → cron `curl ingest_url` → list events.

**Agendar é comando, não `crontab -e`.** Cada item de `GET /api/coletas` — e cada `equipamentos[]`,
que é o mesmo para a máquina inteira — traz `comandos.instalar` (põe a linha de cron; idempotente),
`comandos.conferir` (responde NA MÁQUINA se ela está lá) e `comandos.remover` (tira). Nenhum
reescreve um crontab que não conseguiu ler. Leitura chegando é a prova de que o cron rodou; a falta
dela não distingue "removeram" de "máquina caída" — não desligue coleta por silêncio, pergunte à
máquina com o `conferir`.

**Séries de dispositivos: peça a frota inteira, não um por vez.** `GET /api/coletas/serie` devolve
todos os seus equipamentos numa chamada, cada item com `api` para a série individual; os nomes,
grupos e comandos ficam em `GET /api/coletas`, que também é uma chamada só — inclusive os canais de
coletor (`inputs[]`), com o nome e o grupo do equipamento. Renomear ou trocar o grupo é
`PATCH /api/ambientes/:id`, e vale para todos os canais e medidas dele.

**Canais de coletor (Alloy, Telegraf, OpenTelemetry, NCPA, collectd, SNMPv3): a frota numa chamada.**
`GET /api/inputs/serie` devolve todos os seus canais com estado, último dado autenticado e números — a
mesma resposta que a nossa tela recebe pelo socket. Por equipamento: `POST /api/ambientes/:id/inputs`
cria o canal ou gira a credencial (e troca o `monitoring`; a configuração no equipamento para de valer
até ser trocada), `PATCH /api/ambientes/:id/inputs/:collector` com `{active}` pausa ou retoma sem apagar,
e `DELETE` na mesma rota revoga e remove. `monitoring` é `system` (a máquina inteira) ou as partes —
`host` (CPU, memória, carga, tempo ligado), `disk`, `network`, `mysql`, `asterisk` —, dentro do que o
coletor do canal sabe coletar (`collects`). Revogar não cala a máquina: `GET /api/ambientes/:id/inputs`
traz em cada canal `uninstall.remove` (tira do equipamento o serviço e a configuração do HookPulse, sem
girar credencial) e `uninstall.verify`; ofereça os dois junto do `DELETE`. A configuração em uso — com a
credencial, cada arquivo em `files` (caminho, dono, modo e o comando `put` que o põe no lugar) e os passos
em `steps` — fica guardada, cifrada: `GET /api/ambientes/:id/inputs/:collector/setup` a devolve de novo ao
dono, sem girar nada; a lista e a frota nunca a trazem. Um por vez bate no teto
de leitura da origem de ingestão e volta 429 — foi por isso que a rota da frota existe, e é o mesmo
que a nossa tela pede pelo socket. Item que não pôde ser lido vem com `erro` e o prazo dele, nunca
com série vazia. Depois, consulte uma vez por minuto, sem sobrepor
pedidos. `poll_after_sec` indica a espera mínima; nos primeiros 60 segundos após cadastrar,
uma série ainda vazia pode indicar 5 segundos. Respostas são privadas e reaproveitadas por até
60 segundos. Em 429/503, obedeça `Retry-After` ou `retry_after_sec`, aumente o intervalo após
falhas repetidas e preserve a última leitura como histórico. Trocar `n` não remove os limites.

**`name` é obrigatório na criação e não pode ser apagado no PATCH.** Ele é o que o alerta tem para
dizer O QUE parou — vai no assunto do e-mail, no `alt` do badge e na linha do painel público —, então
o servidor não inventa nome nenhum. Nome ausente, vazio ou em branco responde **400
`nome_obrigatorio`**; o que não é texto, é só pontuação (`---`) ou carrega caractere de controle e de
formato responde **400 `nome_invalido`**. Qualquer alfabeto vale, aparamos as pontas e o teto é 80
caracteres. Pedido recusado **não cria linha nenhuma**, nem o convidado.

Na criação (e só nela) a linha também guarda **de onde veio a chamada**: user-agent, origem (esquema
e host, nunca o caminho), país/região e a **rede** (ASN + operador, ex.: `14618 / Amazon Data
Services`). É dado de operação, para separar varredura de uso real — a rede é o campo que decide,
porque país diz onde o pacote entrou e a rede diz de quem ele é. **Não sai em resposta nenhuma** e
não muda nada no que você envia. Está na página de privacidade.

## Importar um crontab

```bash
crontab -l | curl -s -XPOST "https://hookpulse.net/api/import/crontab" \
  -H "X-Guest-Token: $TOKEN" -H 'content-type: text/plain' --data-binary @-
# proposta: recognized[], ignored[] (com o motivo), existing[], would_create, free_tier
# depois de conferir, repita com ?apply=1
```

- **Sem `apply=1` nada é escrito.** A proposta diz linha a linha o que viraria monitor e o que foi
  ignorado: `comentario`, `variavel`, `reboot` (não dá para saber quando esperar), `cron_invalido`,
  `nao_reconhecida`.
- `@daily`/`@hourly`/`@weekly`/`@monthly`/`@yearly`/`@midnight` viram a expressão equivalente.
- **O comando vira o nome, com senha e token redigidos antes** — o nome viaja no e-mail de alerta,
  no feed público e no badge.
- `apply=1` é **tudo ou nada**, com a mesma régua do `POST /api/endpoints`: estourou a franquia,
  402 com `accepts[]` e nada criado. Linha cujo monitor já existe entra em `existing`, não duplica.
- Zero linha de agenda → 400 `crontab_sem_agenda`.

## Frota inteira sem criar monitor na mão

```bash
KEY=$(curl -s https://hookpulse.net/api/ingest-key -H "X-Guest-Token: $TOKEN" | jq -r .key)
curl -fsS "https://hookpulse.net/in/$KEY/nightly-backup"   # cria no 1º ping; created: true
curl -fsS "https://hookpulse.net/in/$KEY/nightly-backup"   # o MESMO monitor; created: false
```

- **Uma chave por dono**, prefixo `hpk_`. `DELETE /api/ingest-key` gira: a anterior para de **criar**
  (404), e os monitores já criados continuam funcionando. A chave sobrevive ao login — o crontab
  que você já publicou não muda.
- **O slug é o nome e a identidade.** Normalizado para minúsculas, `[a-z0-9-]`, 40 chars; colisão
  resolve para o MESMO monitor, nunca para um segundo. Sobrou vazio → 400 `slug_invalido`.
- **Criação só dentro da franquia grátis** (ou do trial). Estourou → **402 com `accepts[]`, sem
  escrever nada** — nem o evento. Monitor que já existe continua pingando normalmente.
- `?interval=` define o intervalo do monitor novo; sem ele, o padrão do produto.
- O dono vem da CHAVE: nada no pedido diz de quem é o monitor.

## Sinais explícitos: `/start`, `/fail` e o código de saída

Sem eles, "rodou e falhou" é invisível para nós — só vemos silêncio, e silêncio só vira alerta
quando o intervalo inteiro passa. A integração inteira cabe em duas linhas:

```bash
curl -fsS "https://hookpulse.net/in/$ID/start"
./backup.sh; curl -fsS "https://hookpulse.net/in/$ID/$?"   # 0 = sucesso, 1..255 = falha
```

- **`start` NÃO é prova de vida.** Não move `last_event_at` nem zera `miss_count`: rotina que
  começa e trava continua virando miss na hora certa. Ele só carimba `last_start_at`.
- **Sucesso fecha a execução:** move o relógio, zera o silêncio, grava `last_duration_ms` (se houve
  `start`) e limpa a pendência.
- **`fail` também não é prova de vida.** Grava o evento, carimba a pendência, e **quem avisa é o
  cron de 5 minutos** — o ingest não manda e-mail nem faz `fetch` de saída. O alerta sai com
  `reason: "fail"` e `text` "Failed: …", no MESMO cooldown do miss (`alert_repeat_sec`).
- **Rotina que começou e não terminou:** `max_duration_sec` (60..86400; `null`/`0` desliga) faz o
  cron avisar com `reason: "too_long"` e `text` "Still running: …" quando a execução aberta pelo
  `start` passa do teto. Sucesso e falha FECHAM a execução (zeram `last_start_at`), então não há
  como o aviso sair de uma execução que já terminou. Quem detecta é o tick de 5 minutos — por isso
  o piso é 60 s, e não 5 s: teto menor que o tick prometeria uma precisão que não existe.
  Precedência: silêncio > pendência explícita > execução longa; nada se perde, o aviso da longa sai
  no tick seguinte.
- **Duas execuções que se sobrepõem:** mande `?rid=` no `start` E no fechamento. Sem ele,
  `last_start_at` é uma coluna só — a execução B sobrescreve a A, e quando a A termina medimos a
  duração da B. Com ele, o fechamento só mede quando o `rid` casa com o que está aberto; um `rid`
  diferente **não apaga o `last_start_at`**, senão o teto do item 3.1 perderia de vista a execução
  que continua rodando. Fechamento SEM `rid` fecha o que estiver aberto (o cliente que instrumentou
  metade só tem uma execução em mente). Régua `[A-Za-z0-9._-]{1,64}`; fora dela o `rid` é
  **ignorado, nunca 400** — recusar um ping transformaria uma rotina viva em alerta de queda.
- Código fora de 0–255 → **400 `exit_code_fora_da_faixa`**; sufixo desconhecido (`/in/:id/pausa`)
  → 404. `?rid=` já é aceito no formato (item 3.5), sem efeito ainda.
- `GET /api/endpoints/:id` mostra `last_start_at`, `last_duration_ms`, `max_duration_sec` e
  `alert_pending`;
  `/events` mostra `kind`, `exit_code` e `rid` de cada evento.
- Grátis: ping e sinal não são cobrados.

## Estatística de duração

`GET /api/endpoints/:id` traz `duration: { samples, p50_ms, max_ms }` — só a FICHA, nunca a lista:
uma consulta por monitor numa frota de 500 seriam 500 consultas para desenhar uma tela.

- A amostra é o `events.duration_ms`, gravado no evento que **FECHA** a execução. Sem tabela nova e
  sem escrita a mais: coluna a mais numa escrita que já existia, e o D1 cobra a linha, não a coluna.
- O `p50` é a mediana **baixa**, sem interpolar: assim ele é sempre uma duração que aconteceu de
  verdade. Interpolar entre duas amostras num conjunto de quatro promete precisão que não existe.
- `null` na coluna significa "sem duração medida" e **não vira amostra de 0 ms** — `Number(null)`
  é zero, e uma amostra dessas faria a ficha dizer que o backup ficou mais rápido justamente
  quando paramos de medir.

## Prometheus

`GET /s/:token/metrics` → `text/plain; version=0.0.4`, `no-store`, **zero escrita** (scrape de 15 s
não pode virar conta de D1). Três séries rotuladas por monitor, com o id PUBLICADO — nunca o de
ingest, que cala o alarme: `hookpulse_last_ping_age_seconds` (gauge), `hookpulse_late` (gauge 0/1)
e `hookpulse_miss_total` (counter).

- Monitor que **nunca pingou não recebe amostra de idade**: zero ali leria como "pingou agora", que
  é a mentira exata que faz o alerta de infra não disparar. Ele continua nas outras duas séries.
- Nome de monitor é texto do usuário, e **uma aspa solta faz o Prometheus recusar o alvo INTEIRO**,
  não a linha — `\`, `"` e quebra de linha são escapados antes de tudo.
- Relógio adiantado no cliente daria idade negativa e data ilegível daria `NaN`: o piso é zero e o
  ilegível não vira amostra.

## Repetição do alerta

`alert_repeat_sec` diz de quanto em quanto tempo o MESMO incidente volta a avisar. Padrão 86400 —
o de sempre, então nenhum monitor antigo muda. Piso **3600** porque cada repetição é um e-mail pelo
SES e a conta é de quem hospeda: um cooldown de 60 s numa queda de um dia são 1.440 e-mails para um
monitor só. Teto de 30 dias, que é o jeito de dizer "avise uma vez e cale a boca"; `null` volta ao
padrão, e `0` é **400 `alert_repeat_invalido`** — aqui zero significaria "repita sem parar".

O `interval_sec` continua sendo o piso de baixo: entre duas conferências de um monitor de 48 h não
houve nada de novo para contar. Por isso a ficha devolve o valor **EFETIVO**, como o `grace_sec` já
faz — o que o cliente pediu fica guardado e volta a valer se o intervalo baixar.

## Pausa e janela de manutenção

```bash
curl -s -XPATCH https://hookpulse.net/api/endpoints/$ID -H "X-Guest-Token: $TOKEN" \
  -H 'content-type: application/json' -d '{"paused_until":"2026-09-08T03:00:00Z"}'
# despausar: {"paused_until": null}
```

- Enquanto a data estiver no futuro, o monitor **nem entra no lote do cron** e não alerta.
  `GET` devolve `paused_until` e `state: "paused"`.
- **Teto de 30 dias** (400 `pausa_longa`); data no passado ou ilegível → 400 `pausa_invalida`.
- **Pausa não zera nada**: `miss_count` e `last_event_at` ficam como estavam, então o silêncio
  anterior à manutenção continua visível.
- **Não remova o sinal para "pausar"** — silêncio é exatamente o que provoca miss.
- `state` numa palavra: `ok` · `late` · `waiting` (nunca pingou) · `paused` · `inactive`.

## Recuperação: o aviso de que voltou

Depois de um alerta REAL, o próximo ping de sucesso gera `type: "hookpulse.recovery"` com
`down_since`, `recovered_at` e `downtime_sec`, nos mesmos destinos do alerta.

- **Tipo próprio, não um `reason` dentro do miss:** integração que filtra por `type` não pode
  mostrar "está tudo bem" com cara de incidente. Slack/Discord recebem só o campo deles.
- **Só existe depois de queda anunciada.** Monitor que nunca caiu não recebe "voltou", e dois
  sucessos seguidos geram **um** aviso.
- **Não passa pelo cooldown** — o `alerted_at` que ele leria é o da queda que acabou de
  ser anunciada. Ao mandar a recuperação, o cron ZERA o `alerted_at`: a próxima queda avisa na hora.
- Custa **um** envio SES a mais por incidente. Formato exato: `GET /api/templates`
  (`miss_json` e `recovery_json`).

## Agenda: `cron` + `tz` + `grace_sec` (em vez de `interval_sec`)

Um backup das 03:00 não precisa mais virar "intervalo de 24 h" e receber o alerta quase um dia
atrasado. Mande **expressão cron de 5 campos**, fuso IANA e tolerância:

```bash
curl -s -XPOST https://hookpulse.net/api/endpoints -H "X-Guest-Token: $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"name":"backup noturno","cron":"0 3 * * *","tz":"America/Sao_Paulo","grace_sec":900}'
# → 201 com cron, tz, grace_sec e next_expected_at (o instante UTC do próximo vencimento)
```

- **`cron` e `interval_sec` são mutuamente exclusivos** → 400 `cron_ou_interval`. No PATCH, mandar
  `interval_sec` num monitor com agenda dá o mesmo 400; para voltar ao modo intervalo, mande
  `{"cron": null, "interval_sec": 1800}` no mesmo corpo.
- Outros 400 da agenda: `cron_invalido` (dialeto: `*`, `a`, `a-b`, `*/n`, listas — **sem** `@daily`
  e sem `MON`), `tz_invalida`, `grace_invalido` (padrão 90, mínimo 30, máximo 86400),
  `cron_sem_ocorrencia` (não acontece dentro de 366 dias, como `0 0 30 2 *`).
- **Atrasado = `next_expected_at` + `grace_sec`.** `next_expected_at` é calculado para frente na
  criação, no patch e a cada ping; o cron de 5 minutos só compara datas. PATCH que **não** mexe na
  agenda preserva o vencimento — renomear um monitor atrasado não o deixa saudável.
- **Cobrança:** grátis, com a MESMA régua do intervalo — janela da agenda ou `grace_sec` abaixo de
  90 s é `fast_interval` ($0.05). `"cron":"* * * * *"` cobra; `"0 3 * * *"` com grace 900 não.
- **Horário de verão, limitação declarada:** hora local que não existe no dia da virada não casa (a
  rotina não é dada como atrasada); hora repetida casa uma vez.
- Com agenda, `interval_sec` vira **derivado** (a janela da agenda) e é o que alimenta o cooldown
  do alerta. Não mande, leia.

**Miss:** o cron de 5 min incrementa `miss_count` (ou consome uma falha explícita, sem inflar o contador) e, no máximo uma vez por `alert_repeat_sec` (padrão 86400, piso 3600, teto 30 dias — e o `interval_sec` é o piso de baixo), avisa por **e-mail** (`alert_to` ou e-mail da conta) e/ou **POST HTTPS** em `alert_url` (Slack Incoming, Discord webhook, n8n Webhook). Sem canal, só registra. URL só https público (sem localhost/IP). `alert_to`/`alert_url` inválidos no create/patch → 400. Snippets: `GET /api/templates`.

## Cota e preços (x402 Base)

`GET /api/pricing` (tool `pricing`) apresenta as franquias, tarifas e oferta de trial públicas.
`GET /api/billing` (tool `billing`) conserva o estado de cobrança/trial de quem chama.
Consulte antes de uma operação paga; o desafio 402 informa o valor do pedido.

- **Cobrança desligada (dono, 23/09/2026):** endpoint extra e intervalo abaixo de 90 s não cobram
  (até 500 monitores por rede); só o destino de e-mail além do 1º continua **$0.10**, porque cada
  aviso sai pela SES. Numa ação que pede os três, o 402 cobra só o e-mail. Pague com x402, crédito
  pré-pago ou crédito comprado por Pix.

- **Grátis: 10 endpoints por dono, intervalo ≥ 90s** (com agenda: janela e `grace_sec` ≥ 90s).
  Ping no ingest não é cobrado.
- **Aviso por e-mail: 1 cadastro grátis, $0.10 por cadastro a partir do 2º.**
  É a única dimensão cujo custo cresce com o USO — cada miss vira envio SES. Cobra-se o CADASTRO
  do destino, não o envio: preço no ato de registrar é previsível, e cobrar por e-mail entregue
  transformaria uma queda longa numa fatura surpresa.
  No PATCH só paga quem ENTRA no aviso por e-mail; quem já tinha não paga de novo.
- **`alert_url` (webhook) é grátis** — Slack, Discord, n8n. É um fetch nosso, custo desprezível.
  Se você tem 10 endpoints e quer avisos, o caminho barato é webhook.
- Referência, hoje sem cobrança: extra endpoint $0.10 · intervalo abaixo de 90s $0.05 · o contato é grátis
- **Trial:** conta = **90 dias** sem paywall de uso, do primeiro uso (seção Trial acima) — antes de
  pagar endpoint/intervalo, considere o loop de cadastro; é grátis e por API. Aviso por e-mail
  além do 1º destino continua $0.10 mesmo no trial.
- Números em vigor: `GET /api/billing` e o bloco `quota` de `GET /api/` (campo `trial` incluído)

## Cota estourada

Responde **402** com `accepts[]` (x402, USDC na Base). Pague e **repita a mesma chamada** com
`X-PAYMENT`. Nunca responde captcha para agente — se você levou 403 pedindo verificação, é bug.

```bash
. ~/.config/make-money-x402/make-money-x402-env.sh
~/src/mm/scripts/x402-pay.sh --url 'https://hookpulse.net/api/endpoints' \
  --header "X-Guest-Token: $TOKEN" \
  --body '{"name":"job","interval_seconds":900}' --homolog
```

## Ver o status de vários de uma vez

`GET /api/status-feed` (com o seu token) devolve uma **URL pública e só de leitura** com o
estado de todos os seus endpoints:

```bash
curl -s https://hookpulse.net/api/status-feed -H "X-Guest-Token: $TOKEN"
# { "token": "hpf_…", "json": "https://hookpulse.net/s/hpf_….json",
#                     "rss":  "https://hookpulse.net/s/hpf_….rss" }
```

- `…/s/<token>.json` traz `summary.all_ok` — o campo para um monitor externo consultar sem
  entender o resto. Ele é **falso** enquanto alguém estiver esperando o primeiro ping: endpoint
  que nunca pingou não é saudável, é endpoint que ninguém ligou.
- `…/s/<token>` (ou `.html`) é a **página**, renderizada no servidor: conteúdo no corpo, sem JS
  obrigatório, com o badge de cada monitor. **`noindex` por padrão** — a URL é a credencial.
- `…/s/<token>.rss` é RSS 2.0 para leitor de feed, bot de chat ou página de status. O `guid`
  muda quando o ESTADO muda, não a cada geração — o leitor avisa na transição, não sempre.
- **Badge SVG por monitor:** `GET /s/<feed>/<id publicado>.svg`. A URL vem PRONTA em cada linha do
  `.json` (campo `badge`) — o id publicado é um hash, ninguém deve calcular à mão. Verde `ok`,
  vermelho `late`, cinza `paused`/`waiting`/`inactive`; `Cache-Control: public, max-age=60` + ETag,
  sem fonte externa e sem escrita nenhuma. **O id de INGEST não é aceito ali de propósito**: quem o
  tem pinga o monitor e cala o alarme.
- O token do feed **não é** o token de dono: só lê status, e não carrega o token do endpoint nem
  a URL de ingest (quem lê o feed não pode forjar ping). Vazou? `DELETE /api/status-feed`.

A UI mantém rascunho na sessão e a rotina/integração selecionadas após F5. Não remove o sinal para “pausar”: silêncio provoca miss. Crontab é exemplo de heartbeat a cada 15 minutos, não prova de conclusão de backup.

## Acervos públicos de dados

`GET /api/` → `docs.data_indexes` descobre quatro acervos de leitura: endereços CNEFE,
metadados PNCP, domínios observados em CT e arquivos de programação XMLTV. As mesmas raízes
estão em `/llms.txt`, `/llms-full.txt`, `/okf/index.md` e `/developers#dados`. Abra o
`formats.json` adequado e siga a hierarquia e `links.proximo` (até 20 itens por página).
Atualização manual: confira fonte e referência. Respeite `Retry-After` em 429/503. Não
encaminhe credenciais do produto a esses hosts. Leia somente o recorte necessário à tarefa.

<!-- GERADO por scripts/monta-ui.mjs — fonte: .agents/skills/<produto>/SKILL.md. Não edite. npm run ui -->
