---
name: iptv
description: Use Grade to find TV and radio channels, read schedules, save channel lists and check playback results. Receive inquiries about authorized streaming projects. Use for Grade, gradetv.net or the iptv workspace.
---

# Grade — skill para agentes

**Live:** https://gradetv.net
**UI humana:** TV/rádio → buscar → abrir → favoritar/guardar → voltar. Refinamentos e detalhes
são progressivos. Agentes começam em `/developers` e usam API/OpenAPI/MCP, sem depender do HTML.

**Descoberta:** `GET /api/` · `/llms.txt` · `/openapi.json`  
**Catálogo:** `src/lib/apidocs.js`  
**MCP (remoto, recomendado):** `POST https://gradetv.net/mcp` — Streamable HTTP, JSON-RPC 2.0.
Pluga direto no cliente MCP; não precisa deste repositório. Confira com `GET https://gradetv.net/mcp`.
**MCP (stdio, local):** `node ~/src/mm/scripts/mcp/server.mjs --product iptv`

## Regra de ouro

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

**Relay continua proibido na operação atual.** Não contornar CORS/HTTP/geobloqueio baixando ou
repassando segmentos, áudio ou chaves pela Grade, c3 ou fornecedor. Licença de metadados, URL
pública, resposta 200 e pagamento x402 não concedem direitos audiovisuais. Pesquisa e contato de
produtor não ativam transmissão. Antes de alterar operação que multiplica custo, ler as
[regras duras do produto](../../../AGENTS.md) e o
[estado das proteções](../../../docs/decisoes-distribuicao-custo.md): orçamento, limites executáveis,
alerta e interrupção são requisitos; deduplicação por cache não é teto global de gasto.

## Auth

1. `POST /api/guest` → `X-Guest-Token: ipt_…` — o convidado é a identidade do agente.
2. Conta e pagamento são do SDK (21/09/2026): pessoa com sessão é a conta (cookie, só no
   navegador). A chave `iptk_…` saiu: `/api/keys*` e chave apresentada respondem 410.

## Operações

| Tool | HTTP |
|------|------|
| `api_index` | `GET /api/` |
| `list_programacao` | `GET /api/programacao?depois=` — até 48 canais com dados coletados; siga proximo |
| `get_programacao` | `GET /api/programacao/:slug?antes=` — última grade e até 20 datas; siga guia.proximo |
| `get_programacao_dia` | `GET /api/programacao/:slug/:dia` — programação histórica preservada; dia YYYY-MM-DD |
| `producer_services` | `GET /api/producers?lang=pt` — oferta e contatos para produtores; pt/en/es/fr/de/hu, sem ativação |
| `geo` | `GET /api/geo` — país sugerido (sem a borda da CF, o fallback BR) + idioma do `Accept-Language` (ISO 639-3; fallback `por`) |
| `list_countries` | `GET /api/countries?kind=tv|radio|all` — `{code,name,count,flag}` só com canal tocável (toda faceta aceita `kind`; padrão TV) |
| `origem` e `health.sources` | Cada canal e stream diz de onde veio (`origem`: `iptv-org`, `free-tv`, `kodinerds`, `radio-browser`, `tdt`) e `GET /api/health` traz `sources` por fonte (`fetched_at`, `stale`, `ausente`, `itens`). Rádio vem do Radio Browser (com votos/tags) e do TDTChannels (Espanha, sem checker). |
| stream `kind: youtube` | `streams[]` de `get_channel` pode trazer `kind: "youtube"` com `youtube {video|channel, embed, assistir}`: o browser embute o player oficial; o hop (`/api/s/:id`) só redireciona; M3U/XSPF do feed não o levam (o JSON traz `kind: "youtube"`). Lista `youtube-br` (SBT News, Record News, Jovem Pan…) + Ⓨ do Free-TV. |
| `get_channel_guide` | `GET /api/channels/:id/guia` — programação disponível: `agora`, `a_seguir`, `programas[]`. A ficha traz `guide_now`. 404 sem guia fresca. Fornecedores não são públicos; campos legados site e guide_site ficam null. |
| `list_tags` | `GET /api/tags?country=BR&limit=40` — tags das estações de rádio tocáveis (vocabulário livre do Radio Browser), `{id,name,count}` |
| `list_languages` | `GET /api/languages` — `{code,name,count}` |
| `list_networks` | `GET /api/networks` — `{name,count}` |
| `list_qualities` | `GET /api/qualities` — `{id,name,count}` |
| `list_subdivisions` | `GET /api/subdivisions?country=BR` |
| `list_cities` | `GET /api/cities?country=BR&subdivision=BR-SP` |
| `search_channels` | `GET /api/channels?q=&country=BR&language=por&playable=1` — filtros: category, network, quality, guide, subdivision, city; `kind=tv` (padrão), `kind=radio` (estações do Radio Browser) ou `kind=all`; `tag=` (tag de rádio); `sort=score` ordena pela saúde medida por terceiro (IPTV Nexus), `sort=votes` pelos votos da rádio, e `online=1` traz só quem foi visto online nas últimas 48 h. Item traz network, owners, launched, quality, feed, website, idiomas (`language_labels`), categorias em PT (`category_labels`), `kind`, `origem`, `health_ext` (`score`, `online`, `checked_at`; `null` quando ninguém mediu) e, em rádio, `radio` (`tags`, `votes`, `clicks`, `codec`, `bitrate`, `geo`). **Sem `kind`, rádio nunca aparece** |
| `get_channel` | `GET /api/channels/:id` — ficha completa + streams disponíveis |
| `legacy_stream` | `GET /api/legacy/:id` — metadados públicos, `website` oficial (anulável), `provider_url` copiável e `legacy_url`, sem buscar mídia |
| `tv_state` | `GET /api/tv/:codigo/estado` — ativação da rede da TV, sem consumir minutos |
| `tv_pass` | `POST /api/tv/:codigo/comprar` — 4,99 USDC/30 dias, x402 na Base; código identifica a rede da TV |
| `create_guest` | `POST /api/guest` |
| `get_library` | `GET /api/library` |
| `create_category` | `POST /api/categories` `{name}` → `{id, group_id, …library}` (já nasce com a sub-aba **Geral**) |
| `create_group` | `POST /api/groups` `{category_id,name}` |
| `add_item` | `POST /api/items` `{group_id,channel_id}` → `{id, group_id, category_id, …library}` |
| `get_history` | `GET /api/history?limit=&offset=` — canais que o dono assistiu, do mais recente ao mais antigo |
| `record_watch` | `POST /api/history` `{channel_id}` — reassistir soma em `plays` em vez de duplicar linha |
| `forget_watch` | `DELETE /api/history/:channel_id` |
| `clear_history` | `DELETE /api/history` |
| `report_play` | `POST /api/play-report` `{channel_id, ok, code?}` — relata se tocou |
| `channel_health` | `GET /api/channels/:id/health` — relatos por navegador, sistema e país; tempo de resposta |
| `play_reports` | `GET /api/play-reports?channel_id=&ok=0` — relatos individuais **(só operador)** |
| `list_favorites` | `GET /api/favorites` |
| `add_favorite` | `POST /api/favorites` `{channel_id}` |
| `remove_favorite` | `DELETE /api/favorites/:channel_id` |
| `list_comments` | `GET /api/channels/:id/comments` |
| `post_comment` | `POST /api/channels/:id/comments` `{body, author?}` |
| `delete_comment` | `DELETE /api/comments/:id` |
| `chat_history` | `GET /api/chat/:channel_id/mensagens` |
| `chat_send` | `POST /api/chat/:channel_id/mensagens` `{body, author?}` — com a cobrança desligada, sem passe e sem 402 (referência $0.10 / 30 dias) |
| `chat_pass` | `POST /api/chat/pass` — dispensável com a cobrança desligada; quando ela voltar, $0.10 / 30 dias (x402 ou crédito), do convidado que fala no chat |
| `me` | `GET /api/me` (cookie da conta MM) — e-mail e tamanho da biblioteca da conta |
| `billing` | `GET /api/billing` — `prices.chat_month_usd`, `prices.abuso_24h_usd`, `chat.active/until` |
| `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` |

## Projetos de produtores sob consulta

`GET /api/producers` é público e informativo: `status: sob_consulta`, `activation_available: false`.
Retorna a mesma oferta de `/produtores` (e traduções): distribuição autorizada, player e acompanhamento
como escopo a avaliar. Use `contact.form_url` para o formulário humano, `contact.email`
(`contato@gradetv.net`) para e-mail ou `contact.api_url` (`POST /api/contact`) para enviar a proposta.
`contact.message_template` orienta canal/evento, direitos, audiência, duração e data. Não envie segredos.
O contato é grátis, sem captcha nem pagamento (uma mensagem a cada 10 s por rede), e não contrata transmissão.
O canal precisa de autorização dos titulares para sinal e obras da programação, território e prazo.
Nenhuma rota desta oferta inicia relay: `/api/m/` continua 410.

## Catálogo comunitário

Falha da lista da emissora responde 422 com código e dados da origem, inclusive na retomada de TV.
Serviço ocupado responde 503 com `Retry-After: 5`; respeite a espera antes de repetir.

Cada canal do `search_channels`/`get_channel` traz `social`:
`{plays, fails, favorites, comments, health, last_fail_code}`. `health` é o percentual de sucesso e
vem **`null`** até haver 3 relatos. As contagens são de relatos, não de pessoas únicas.

**Informe o resultado no seu player** (`report_play`). Os relatos mostram onde a reprodução
funcionou ou falhou. Eles não confirmam a causa da falha. Vocabulário de `code` (só quando `ok:false`):
`cors` · `geo` · `sumiu` · `codec` · `playlist` · `sem_resposta` · `protocolo` · `sem_stream` · `embed` · `outro`.
O que vier fora da lista entra como `outro` — o vocabulário é fechado para que as falhas agrupem.

Conta **uma vez por dono, por canal, por dia e por resultado**.
O primeiro relato desse resultado no dia responde 201. Repetir o POST responde 200 com
`counted:false` e `reason:"ja_relatado_hoje"`. Não repita em laço.
Canal ausente responde 404. JSON inválido, canal não informado ou `ok` diferente de booleano respondem 400.

Navegador e sistema saem do `User-Agent`; o país vinha do `CF-IPCountry` da borda da CF, que saiu em 23/09/2026. Mandar `browser`/`country` no
corpo não muda nada.

**Identificador da rede:** `postRelato` aplica `hashRede` ao IP antes de gravar o relato.
Nos relatos atuais, o campo `ip` guarda esse identificador.
O cálculo combina a rede com um valor secreto, chamado sal.
Sem o sal configurado, o campo fica `null`. A consulta pública
`channel_health` não o devolve. Ler o relato exige `METRICS_TOKEN` (`play_reports`).
O prazo de retenção dos relatos individuais é de 90 dias. A manutenção diária remove os relatos
que passaram desse prazo. A estatística agrupada permanece.

`channel_health` compara resultados por navegador, sistema e país.
`your_environment` mostra o ambiente de quem consulta. `environments[]` mostra os ambientes relatados.

**Resultados e tempo de resposta por país:** `regions[]` traz plays/fails/health por país.
`geo.tipo` indica `"geo"` quando há falhas concentradas e sucesso em outro país, `"down"` quando
os países medidos só têm falhas e `"ok"` quando todos os países medidos têm algum sucesso.
Com menos de 3 relatos em cada país, indica `"unknown"`.
Esse padrão não confirma bloqueio regional nem queda da transmissão.
Em `GET /api/s/:id`, a Grade mede o tempo para buscar os dados da transmissão. Não mede o início do vídeo.
`latency[]` traz avg_ms e nota `otima|boa|lenta|ruim`. `your_country.latency_ms` traz a média
no país de quem consulta. `pra_voce` é um objeto. Seu campo `nivel` resume os resultados:
`geo_bloqueado` · `lenta` · `instavel` · `boa` · `sem_dado`. O nome `geo_bloqueado` não confirma um bloqueio.
No catálogo, cada card traz `social.your_geo_ok` (false = todas as tentativas do seu país falharam),
`social.your_country_ok/fail` e `social.your_latency_ms/grade` — mesma informação, sem segunda chamada.

## Chat da sala (WebSocket)

`GET /api/chat/:channel_id/ws` com `Upgrade: websocket`. O socket **entra mudo** — sem token na URL
de propósito. Protocolo:

1. `{"t":"hello","token":"ipt_…","autor":"Nome"}` → `{"t":"pronto","autor":"…","items":[…],"watching":N}`
2. `{"t":"msg","body":"…"}` → todos recebem `{"t":"msg","id","autor","body","at"}`
3. Quem entra ou sai da sala: `{"t":"presenca","watching":N}` — gente com o player aberto, não o histórico de plays.
4. Erros: `{"t":"erro","code":"auth|vazio|rajada|json|tipo|formato|grande|pago"}` — `pago` = precisa do passe mensal (`chat_pass`).

Teto de 6 mensagens por 10s por conexão e 500 caracteres por mensagem. Só as últimas 50 ficam.
Sem WebSocket, use `chat_history` / `chat_send`.

**Passe mensal — hoje dispensado:** com a cobrança desligada (dono, 23/09/2026) enviar não exige
passe. Quando ela voltar, enviar (HTTP ou `{t:"msg"}`) exige `POST /api/chat/pass` — **$0.10 / 30
dias**, x402 ou crédito; sem pagamento → **402**. Ler histórico e `hello` são grátis. O passe é de quem fala
no chat: o convidado do `X-Guest-Token` (o mesmo do `hello`). `GET /api/billing` com o convidado
devolve `chat.active` / `chat.until`.

## Conta MM (reivindica o convidado)

Conta e pagamento são do SDK (21/09/2026). Sem conta, o convidado `ipt_…` é o dono. Com a sessão
da conta (a pessoa entra pela modal da conta ou em `/conta/global`: código ou link por e-mail, senha
ou passkey), a pessoa **é a conta**: pastas, favoritos, histórico, comentários e feed passam a ser do
id da conta, e escrever com a sessão exige a mesma origem e o `X-CSRF-Token` de
`/api/auth/bootstrap`. Cookie da conta com a sessão vencida → **401 `session_ended`** (nunca vira
convidado). A modal, logo depois de entrar, chama `POST /api/auth/claim {guest_token}`, que **move**
para a conta o que o convidado daquele aparelho guardou — `claimed.product.movidos` diz quanto, por
tabela, e `claimed.product.direitos` quantas compras passaram; o que colide com o que a conta já tem
fica no convidado. De outro aparelho a biblioteca já vem da conta. Token de antes da assinatura que
passou tudo para a conta deixa de valer (401 com ele): peça outro em `POST /api/guest`.
`POST /api/auth/start` e `/verify` respondem 410; agente não tem bearer de conta nem chave de API —
usa o convidado.

Feeds públicos (sem header): `GET /f/{iptf_…}/library.m3u` e `/c/{slug}.m3u` · `.json` · `.xspf`.  
A árvore em `GET /api/library` já traz essas URLs em cada nó. Cada item do M3U aponta para
`https://m3m8.gradetv.net/api/s/:streamId.m3u8`. Links antigos em `gradetv.net/api/s/` ainda
302 para o mesmo path (`curl -L`). Path, formato, IDs e tickets são preservados.
Playlists principais e internas passam ao o2; respostas por rede usam no-store.
Copie as URLs internas completas com `p` assinado (até 6000 caracteres, mesmo stream e
origem); não construa tickets. Links públicos duram enquanto a origem/bloqueio permitirem.
Segmentos e chaves vêm direto da origem; `/api/m/` responde 410.
Cada busca tem teto de 512 KiB/8 s/3 redirects, até 128 referências internas e 4 níveis. Falha
no o2 retorna 422 JSON (`playlist_origem`/`playlist_acesso`/`playlist_limite`) para tentar outro
espelho, sem redirecionar a playlist para HTTP. Sem chave, 503 `playlist_config`; ticket inválido/expirado, 404. Refresh interno não conta outro play.
Após a migração, filhas geradas apontam diretamente ao o2. HLS ao vivo tem franquia e passe por rede.
Ao esgotar, o vídeo próprio da Grade mostra QR e seis dígitos. O sinal do emissor permanece direto.

IDs de stream são opacos para o cliente e não mudam pela ordem da recarga. Hops posicionais antigos
ausentes e itens salvos recuperam o mesmo canal, feed e fonte na leitura; não recrie pastas ou itens.
Sem stream correspondente, a resposta continua 404. Os bloqueios continuam valendo.

**Toda escrita devolve a árvore inteira** (`categories[]` com sub-abas, itens e feeds). Depois de
`POST/DELETE`, use a resposta — um `GET /api/library` em seguida é ida perdida ao servidor.

A UI persiste busca/aba em `localStorage` (`ipt_ui`) e as pastas no guest. Não peça para “limpar
estado” no boot e não recrie `POST /api/guest` se já houver `ipt_…` — isso some com a galeria.

O histórico é do dono (a conta da sessão ou o convidado `ipt_…`), teto de 200 canais, e a listagem pagina no
servidor. Histórico de outro dono responde lista vazia / 404 — nunca o dado alheio.

## Páginas HTML (sem JS)

| URL | O que é |
|-----|---------|
| `/brasil` | TV pública do Brasil. Cards abrem o player (`/?q=`). `/pais/br` redireciona pra cá |
| `/como-usar` | Player, pastas, M3U no VLC |
| `/sobre` | Uso, preço, dados guardados, créditos e pedido de remoção |
| `/en` `/es` `/fr` `/de` `/hu` | Mesma superfície nos outros 5 idiomas (hreflang). PT sem prefixo é o canônico. UI (chat, paywall, conta) segue o idioma do path |
| `/en/brazil` `/en/how-to` `/en/about` | Traduções; slugs nativos (es/fr/de/hu no mesmo padrão) |
| `/parceria` `/en/partners` `/es/publicidad` `/fr/publicite` `/de/werbung` `/hu/hirdetes` | Anunciar, patrocinar ou propor parceria, na língua do endereço: os espaços com preço sugerido (rodapé, abertura, listas, faixa do player acima do chat) e a proposta, que chega ao dono. `?espaco=<id>` já marca o espaço |
| `/numeros` `/en/stats` `/es/estadisticas` `/fr/statistiques` `/de/zahlen` `/hu/szamok` | Páginas vistas, uso e confiabilidade. Dados públicos de `/api/vitrine`; valores financeiros privados |
| `/developers` e `/<idioma>/developers` | API, OpenAPI, MCP e dados públicos. Menu, rodapé e seletor mantêm o idioma da página |
| `/programacao[/:slug[/YYYY-MM-DD]]` | Coleção com logos e busca `?q=`; canal com identidade, player ao lado da agenda, busca `?programa=` e datas sem interromper o ao vivo; nos seis idiomas |
| `/sitemap.xml` | Índice da base institucional e dos shards de programação por dia, sem catálogo vazio |

`list_programacao` e `GET /api/programacao` aceitam `q` (até 80 caracteres) e `depois`.
A busca cobre todo o acervo, além da página atual; cada canal traz logo pública e categorias.
O detalhe traz também rede, idiomas e site oficial quando cadastrados.
Histórico coletado é permanente; falha não sobrescreve o último dado válido. Só datas disponíveis
respondem 200. O ao vivo é o atual mesmo ao abrir uma grade antiga. Passe de TV: 4,99 USDC/30 dias
em `/pay`; anunciantes em `/parceria`. OKF: `/okf/programacao/:slug.md` e
`/okf/programacao/:slug--YYYY-MM-DD.md`, usando a mesma API. A coleção fica em
`/okf/programacao/index.md`; `/api/` anuncia os formatos, cursor e atualização diária do acervo.
Grade de blocos curtos: até 512 programas e 128 KiB de JSON por canal, sem cortar a projeção histórica.

A guia do dia é produto (`get_channel_guide`). Ficha de canal com programação + saúde + play usa o dado; despejar o catálogo iptv-org em dezenas de milhares de URLs é scaling. Catálogo = `GET /api/channels?country=BR&playable=1`.
`GET /api/channels/:id` traz `_links.self` (API) e `_links.app` (player).
Path inexistente responde **404 de verdade**. URL de stream só na API.

Tocar no browser = `https://m3m8.gradetv.net/api/s/:id`, a mesma URL do VLC. As playlists passam pela Grade; a mídia
direta ainda precisa ser acessível pelo browser. Um 401/403 da origem vem como 422
`playlist_acesso` no o2, com `X-Grade-Upstream-Status`, e oferece **Abrir site do canal** (`website`,
também em `X-Grade-Website`) e **Copiar playlist** (`provider_url`, também em `X-Grade-Source`,
URL original para colar em outro player). Sem site cadastrado, apenas a cópia. Clipboard bloqueado
oferece campo selecionado para cópia manual; o botão de site nunca abre o M3U8.
`X-Grade-Media-HTTP: 1` identifica mídia/chave HTTP no manifesto, inclusive interno: o player
HTTPS interrompe e oferece **Abrir no player legado**. `legacy_url` abre
`http://legacy.gradetv.net:8080/legacy?stream=ID`, sem login ou ticket; playlists
seguem no hop HTTPS e mídia direto na origem. HTTP não remove CORS nem acesso negado.

`logo_url` no JSON: se o espelho de logos (os arquivos do app) já tem o arquivo, é `https://gradetv.net/logos/{id}` (variante do card, não o original); senão continua a URL de origem (imgur/wikimedia/etc). `GET /logos/:id` sem objeto = 404, sem baixar na hora; canal que saiu do catálogo = 410; canal de conteúdo adulto = 404.

## Cota (leia antes de gastar chamada)

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

- **Grátis, sem cota:** catálogo, busca, facets, ficha de canal, saúde, ler chat e comentários,
  galeria pessoal, pastas e feeds M3U/JSON/XSPF por convidado.
- **Cobrança desligada (dono, 23/09/2026):** escrever no chat não cobra; **$0.10 / 30 dias** (x402,
  USDC na Base) fica como referência. O contato é grátis.
- **TV:** HLS ao vivo tem 120 minutos por rede/dia UTC, compartilhados entre aparelhos e canais.
  Passe de **4,99 USDC/30 dias**, sem renovação automática, em `/pay?codigo=123456`.
  O código vence em dez minutos; pagar em outro IP libera a rede da TV. WebSocket
  `/api/tv/:codigo/ws` acompanha a compra, com gêmea HTTP `tv_state`.
  A home mostra preço e condições nos seis idiomas; o botão pede o código da TV.
  URLs diretas, YouTube, áudio contínuo e VOD sem refresh ficam fora dessa medição. NAT compartilha
  franquia e passe; mudar IP muda a identidade. O passe não remove recusa do provedor.
- Bloco `quota` em `GET /api/`, preços em `GET /api/billing`.

## Cota estourada

**402** com `accepts[]`. Pague e **repita a mesma chamada** com `X-PAYMENT`.

## 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 -->
