protocolo onpostt secp256k1 HTTP + WebSocket

Blocos assinados, feed social e mensagens privadas

Referência rápida do protocolo, da API e da biblioteca oficial. Tudo o que está nesta página pode ser copiado e executado contra o relay local.

Visão geral

O onpostt é um relay para blocos JSON assinados. O cliente gera a chave, assina localmente e envia o bloco por HTTP ou WebSocket. O relay valida assinatura, id, formato, alvo e autoria antes de gravar.

Clientechave privada local
→
Relayvalida + grava
→
PostgreSQLblocks + processed_blocks
WebSocketassinaturas ao vivo
↔
Fan-outfiltro por assinatura
↔
Mensagensciphertext E2E

O que o relay garante

  • assinatura e id conferem com o conteúdo;
  • regras de modo e autoria aplicadas;
  • um id produz efeito no máximo uma vez;
  • fan-out apenas para filtros compatíveis.

O que ele não faz

  • não guarda chave privada;
  • não lê mensagens cifradas;
  • não oferece anonimato nem moderação;
  • não garante entrega ou disponibilidade.

Bloco

Um bloco é um objeto JSON com os campos abaixo. Campos extras são permitidos (até 32 no total) e entram no id e na assinatura.

CampoTipoRegra
idstringSHA-256 hex de 64 caracteres
pubkeystringsecp256k1 comprimida (66) ou não comprimida (130)
created_atinteiroUnix em segundos; sem mais de 600 s no futuro
modestringum dos modos do protocolo
queryarraypares [chave, valor]
contentstring | objeto | arrayobrigatório em post, profile, comment, message e recibos de leitura; proibido em notification; em post use { text?, image, topics? } e no profile use { name, bio, image? }
appstring1–190 caracteres; obrigatório exceto em delete
sigstringDER secp256k1 em hexadecimal
JSON
{
  "pubkey": "03a1...66-hex...",
  "created_at": 1760000000,
  "mode": "post",
  "query": [["topic", "onpostt"]],
  "content": "primeiro post",
  "app": "meu-app",
  "id": "9f2c...64-hex...",
  "sig": "3045...der..."
}
Ordem importa. A assinatura cobre JSON.stringify(bloco sem sig). Não reordene o objeto depois de assinar.

Id e assinatura

O id identifica o conteúdo. A assinatura prova a autoria. Ambos são calculados sobre a serialização exata do bloco.

JavaScript
const unsigned = { ...block };
delete unsigned.id;
delete unsigned.sig;
const id = sha256(JSON.stringify(unsigned));

const signed = { ...block };
delete signed.sig;                 // id permanece
const msgHash = sha256(JSON.stringify(signed));
const sig = secp256k1.sign(msgHash, privateKey).toDER('hex');
  • id não entra no cálculo do id; sig também não.
  • sig não entra na mensagem assinada; id entra.
  • O relay compara o id calculado com o recebido e verifica a assinatura.
  • A ordem da biblioteca é pubkey, created_at, mode, query, content, app, extras, id, sig.

Modos

ModoTransporteQueryConteúdo
postHTTPlivretexto ou { text, image }
profileHTTPlivre{ name, bio, image? }
followHTTPfollower, followingopcional
blockHTTPblocker, blockedopcional
likeHTTPblockopcional
reactionHTTPblock, reactionopcional
commentHTTPblock + parent opcionalobrigatório
repostHTTPblockopcional
notificationHTTPto, block, interaction, kindproibido
notification_readHTTP—{ read: [ids] }
deleteHTTPblockopcional
messageWebSocketfrom, toenvelope cifrado
message_readHTTP—{ read: [ids] }

like, reaction, repost, follow, block e delete podem omitir content. Nos demais modos, o conteúdo é obrigatório, exceto em notification, que não aceita content.

Toggles. like, reaction, repost, follow e block alternam: o primeiro envio cria, um novo bloco com a mesma query remove. Cada envio precisa de um novo id e de uma nova assinatura. A biblioteca adiciona um nonce aleatório a esses blocos para que duas ações no mesmo segundo não colidam.

Fotos em posts

A foto é opcional e vai como base64 dentro do content assinado. O relay não confia nas dimensões declaradas: decodifica os bytes, confere os magic bytes de JPEG/PNG, lê a largura e a altura reais do cabeçalho e rejeita imagens acima de 500px ou do limite de bytes.

content
{
  "text": "legenda opcional",
  "image": {
    "mime": "image/jpeg",
    "data": "<base64 sem prefixo data:>",
    "width": 500,
    "height": 375
  }
}
  • mime: image/jpeg ou image/png.
  • data: base64 canônico, com padding, sem data:.
  • width e height: inteiros positivos, iguais às dimensões reais e no máximo 500.
  • O JSON do conteúdo com imagem pode ter até 128 KiB; a imagem decodificada, até 72 KiB.

Redimensione sempre no cliente. onpostt.prepareImage usa canvas e FileReader, mantém o maior lado em 500px e comprime em JPEG até o alvo:

JavaScript — navegador
const image = await onpostt.prepareImage(file, {
  maxDimension: 500,
  maxBytes: 64 * 1024,
  mime: 'image/jpeg',
});

const block = await onpostt.signBlock(
  {
    mode: 'post',
    query: [],
    app: 'meu-app',
    content: {
      ...(caption ? { text: caption } : {}),
      image: {
        mime: image.mime,
        data: image.data,
        width: image.width,
        height: image.height,
      },
    },
  },
  privateKey
);

if (typeof block === 'string') throw new Error(block);
await onpostt.sendBlock(block);
Não basta olhar o nome do arquivo. O relay valida os magic bytes e as dimensões reais. Um cliente malicioso não consegue declarar 1×1 para uma imagem 4000×4000, nem enviar base64 disfarçado de JPEG.

Foto no perfil

O perfil usa o mesmo helper, mas prepara a foto para até 150px no maior lado e mostra a prévia ao importar. Quando há image, o relay aceita apenas name, bio e image. Salvar sem image remove a foto anterior. O limite de protocolo continua sendo 500px.

content do perfil
{
  "name": "Ana",
  "bio": "Uma frase sobre mim",
  "image": {
    "mime": "image/jpeg",
    "data": "<base64 sem prefixo data:>",
    "width": 150,
    "height": 150
  }
}
JavaScript — navegador
const image = await onpostt.prepareImage(file, {
  maxDimension: 150,
  maxBytes: 64 * 1024,
  mime: 'image/jpeg',
});

const block = await onpostt.signBlock(
  {
    mode: 'profile',
    query: [],
    app: 'meu-app',
    content: {
      name: 'Ana',
      bio: 'Uma frase sobre mim',
      image: {
        mime: image.mime,
        data: image.data,
        width: image.width,
        height: image.height,
      },
    },
  },
  privateKey
);

if (typeof block === 'string') throw new Error(block);
await onpostt.sendBlock(block);

Perfil público

/perfil.html?pubkey=<pubkey> abre a página pública de um usuário, com foto, nome, bio, publicações e fotos, curtidas, reações, comentários, seguidores e seguindo. A leitura usa blocos públicos de todas as aplicações e não exige autenticação.

RotaFunção
/perfil.html?pubkey=...perfil público selecionado
/perfil, /profile, /profile.htmlredirecionam para /perfil.html preservando a query string
/demo/?view=feeddemo no feed
/demo/?view=profilesdemo nos perfis
/demo/?view=notificationsdemo nas notificações
/demo/?view=chatdemo nas conversas
/demo/?view=aboutdemo na seção sobre

Sem pubkey, a página abre o perfil da identidade local quando houver uma. A demo guarda a identidade em sessionStorage e a página de perfil também aceita localStorage. Os cartões de perfil e a área do autor de cada publicação levam para o perfil selecionado.

Fotos com tamanhos diferentes. A foto do perfil é preparada para 150px; fotos de publicações continuam em 500px. As duas passam pelas mesmas validações de base64, magic bytes, dimensões reais e bytes no relay.

Reações e respostas

Curtir e reagir

A curtida é única por autor e alvo. A reação é única por autor, alvo e emoji, então a mesma pessoa pode usar vários emojis no mesmo bloco.

JavaScript
const like = await onpostt.signBlock(
  {
    mode: 'like',
    query: [['block', postId]],
    app: 'meu-app',
  },
  privateKey
);

const reaction = await onpostt.signBlock(
  {
    mode: 'reaction',
    query: [
      ['block', postId],
      ['reaction', '🔥'],
    ],
    app: 'meu-app',
  },
  privateKey
);

await onpostt.sendBlock(like);
await onpostt.sendBlock(reaction);

Comentário e resposta

O alvo continua sendo o post. A resposta acrescenta ["parent", idDoComentario]. O pai precisa ser um comentário existente do mesmo alvo e não pode estar excluído.

JavaScript
const comment = await onpostt.signBlock(
  {
    mode: 'comment',
    query: [['block', postId]],
    content: 'meu comentário',
    app: 'meu-app',
  },
  privateKey
);
await onpostt.sendBlock(comment);

const reply = await onpostt.signBlock(
  {
    mode: 'comment',
    query: [
      ['block', postId],
      ['parent', comment.id],
    ],
    content: 'minha resposta',
    app: 'meu-app',
  },
  privateKey
);
await onpostt.sendBlock(reply);

Excluir

Só o autor do alvo pode criar o tombstone. O bloco não é apagado fisicamente; as consultas passam a ocultá-lo.

JavaScript
const tombstone = await onpostt.signBlock(
  {
    mode: 'delete',
    query: [['block', postId]],
  },
  authorPrivateKey
);
await onpostt.sendBlock(tombstone);

Notificações

Uma notificação é um bloco assinado que aponta para uma interação já aceita. O ator é a pubkey do bloco; o destinatário é to; o alvo é block; a interação é interaction; e kind é like, reaction, comment, repost ou follow. O bloco não tem content.

JavaScript
const notification = await onpostt.signBlock(
  {
    mode: 'notification',
    query: [
      ['to', ownerPubkey],
      ['block', postId],
      ['interaction', like.id],
      ['kind', 'like'],
    ],
    app: 'meu-app',
  },
  actorPrivateKey
);

if (typeof notification === 'string') throw new Error(notification);
await onpostt.sendBlock(notification);
  • to precisa ser o dono do bloco alvo e diferente do autor.
  • A interação precisa existir, ter o mesmo autor e apontar para o mesmo block.
  • O modo da interação precisa ser igual a kind.
  • Alvo e interação não podem ter tombstone.
  • Só pode existir uma notificação por interação.
  • Em kind: "follow", block e interaction apontam para o próprio bloco follow; to precisa ser igual a following.
  • O relay não cria a notificação automaticamente: o cliente envia depois que a curtida, reação, comentário, repost ou follow for aceito.

O bloco é público e imutável, mas some das consultas quando a interação ou o alvo é removido. O estado de lida/limpa fica no cliente. Na demo, o contador de não lidas, o texto contextual e as ações de marcar/limpar são persistidos por destinatário em localStorage.

listar notificações
const notifications = await onpostt.list({
  mode: 'notification',
  app: 'meu-app',
  query: [['to', ownerPubkey]],
  limit: 50,
});

Bloqueio de usuário

O bloqueio é um toggle assinado pelo bloqueador. O primeiro envio cria a relação; repetir a mesma query desbloqueia. O relay recusa mensagens entre as duas pubkeys, nos dois sentidos, enquanto o bloqueio existir. A leitura também é filtrada no servidor: com um token reader assinado, o relay esconde conteúdo dos envolvidos nas duas direções; sem token, esconde autores que participam de qualquer bloqueio ativo.

JavaScript
const block = await onpostt.signBlock(
  {
    mode: 'block',
    query: [
      ['blocker', myPubkey],
      ['blocked', otherPubkey],
    ],
    app: 'meu-app',
  },
  myPrivateKey
);

if (typeof block === 'string') throw new Error(block);
const [result] = await onpostt.sendBlock(block);
// "created" bloqueia; "removed" desbloqueia

// A biblioteca assina o token de leitura automaticamente.
await onpostt.setReaderKey(myPrivateKey);
  • blocker precisa ser a pubkey do autor; não há auto-bloqueio.
  • Mensagens entre o par são recusadas com user_blocked.
  • O bloqueio e o envio de mensagem usam o mesmo lock transacional do par.
  • O token reader expira em READER_TOKEN_TTL_SECONDS (padrão: 30 dias) e só concede leitura.
  • O bloco block continua público para a interface detectar o estado.
  • Uma nova chave privada é uma nova identidade; o bloqueio não impede Sybil (ver Segurança).

Query e filtros

query é um array de pares. Chaves são únicas, têm 1–64 caracteres e valores são string, número finito ou booleano.

JSON
[
  ["topic", "onpostt"],
  ["lang", "pt-BR"]
]

Filtros de leitura usam os mesmos pares e exigem mode. Campos aceitos: mode, pubkey, query, since, until, app, id, limit e offset. O HTTP aceita ainda um campo irmão reader; no WebSocket, reader é irmão de filter. O token é assinado pelo cliente e usado para aplicar bloqueios sem confiar em um viewer enviado sem prova.

JavaScript
const posts = await onpostt.list({
  mode: 'post',
  app: 'meu-app',
  query: [['topic', 'onpostt']],
  limit: 20,
});

const reactions = await onpostt.list({
  mode: 'reaction',
  query: [
    ['block', posts[0].id],
    ['reaction', '🔥'],
  ],
  limit: 100,
});

const comments = await onpostt.list({
  mode: 'comment',
  query: [['block', posts[0].id]],
  limit: 100,
});

Em message, o filtro precisa de pubkey, dos pares from/to ou de id. Em notification, o filtro precisa do par ["to", pubkey] ou de id; to não diferencia maiúsculas de minúsculas e pubkey filtra pelo ator. Em block, use os pares blocker e/ou blocked; esse modo fica fora do filtro de visibilidade para a interface detectar o estado. O relay nunca aceita privatekey em filtros.

Com reader, o relay esconde conteúdo quando leitor e autor se bloquearam em qualquer direção. Sem token, a consulta é anônima e esconde autores que participam de qualquer bloqueio ativo. A mensagem assinada é onpostt-read-v1:<viewer>:<created_at>.

notificações do destinatário
const notifications = await onpostt.list({
  mode: 'notification',
  app: 'meu-app',
  query: [['to', myPubkey]],
  limit: 50,
});

HTTP

RotaMétodoFunção
/save_blockPOSTgrava um bloco assinado
/get_blocks_listPOSTconsulta blocos
/healthGETsaúde do relay e do banco
/api/statusGETsnapshot público
/api/modesGETregras dos modos
/perfil.htmlGETpágina pública de perfil com publicações e atividade
/perfil, /profile, /profile.htmlGETredirecionam para o perfil preservando a query string
curl
curl -s http://localhost:3000/get_blocks_list \
  -H 'Content-Type: application/json' \
  -d '{"mode":"post","app":"meu-app","limit":20}'
resposta
{
  "response": "blocks",
  "blocks": [
    {
      "id": "9f2c...",
      "pubkey": "03a1...",
      "created_at": 1760000000,
      "mode": "post",
      "query": [["topic", "onpostt"]],
      "content": "olá",
      "app": "meu-app",
      "sig": "3045..."
    }
  ],
  "requestId": "8f4c..."
}

/save_block devolve action: "created", "removed" (toggle) ou "duplicate" (id já processado). Apenas ações efetivas são transmitidas pelo WebSocket.

WebSocket

Use o mesmo host e porta. O Origin é validado no upgrade. Apenas message pode ser gravada pelo socket; os demais modos usam HTTP.

hello
{
  "response": "hello",
  "name": "onpostt",
  "relay": "onpostt relay",
  "heartbeatIntervalMs": 30000
}
assinatura
{
  "request": "get_blocks_sub",
  "subscription": "feed",
  "filter": { "mode": "post", "app": "meu-app", "limit": 20 },
  "reader": {
    "viewer": "03a1...",
    "created_at": 1760000000,
    "sig": "3045..."
  },
  "requestId": "req-1"
}

reader é opcional e usa o mesmo formato do HTTP. A página inicial e cada bloco ao vivo passam pelo filtro de bloqueio antes da entrega. Sem token, valem as regras anônimas; blocos block continuam sendo entregues para a interface detectar o estado.

blocos ao vivo
{
  "response": "blocks",
  "subscription": "feed",
  "blocks": [
    {
      "id": "9f2c...",
      "pubkey": "03a1...",
      "created_at": 1760000000,
      "mode": "post",
      "query": [],
      "content": "olá",
      "app": "meu-app",
      "sig": "3045..."
    }
  ],
  "live": true
}
gravar mensagem
{
  "request": "save_block",
  "block": {
    "id": "9f2c...",
    "pubkey": "03a1...",
    "created_at": 1760000000,
    "mode": "message",
    "query": [
      ["from", "03a1..."],
      ["to", "02b2..."]
    ],
    "content": "{\"forRecipient\":{...},\"forSender\":{...}}",
    "app": "meu-app",
    "sig": "3045..."
  },
  "requestId": "req-2"
}

Envelopes ping, unsubscribe, get_status e erros seguem o formato { request, requestId } / { response, status }. O limite padrão é 25 assinaturas e 5 pubkeys por conexão.

Mensagens cifradas

O cliente gera uma chave secp256k1 efêmera por envio, faz ECDH com a pubkey do destinatário e com a do remetente, deriva AES-256-GCM e grava dois envelopes.

content
{
  "forRecipient": {
    "ciphertext": "hex; tag incluída",
    "iv": "24 hex (12 bytes)",
    "senderPubKey": "pubkey efêmera comprimida"
  },
  "forSender": {
    "ciphertext": "hex; tag incluída",
    "iv": "24 hex (12 bytes)",
    "senderPubKey": "pubkey efêmera comprimida"
  }
}
  1. ECDH entre a chave efêmera e a pubkey do participante.
  2. Converter o segredo para hexadecimal e aplicar SHA-256 sobre o UTF-8.
  3. Cifrar com AES-256-GCM e IV aleatório de 12 bytes.
  4. Publicar ciphertext, iv e senderPubKey.

O relay não consegue ler o texto. O destinatário recebe uma cópia com id <id>_rcp na caixa de entrada; o remetente lê a mensagem original.

Biblioteca

Carregue /onpostt.min.js no navegador ou public/onpostt.js no Node.js. No Node, defina globalThis.elliptic antes de importar.

Funções principais

  • generateKeys(), sha256() e prepareImage().
  • signBlock(block, privateKey) assina e devolve o bloco, ou uma string de erro.
  • sendBlock(), connect() e sub().
  • setReaderKey(privateKey) assina o token de leitura; createReaderToken() devolve o token bruto.
  • list() devolve o array e expõe array.truncated; assinaturas recebem event.truncated.
  • encryptMessage() e decryptMessages() para mensagens diretas.
  • sanitizeFilter() remove/recusa campos reservados antes de enviar.

prepareImage é exclusivo de navegador: usa canvas, FileReader e createImageBitmap para devolver { mime, data, width, height, bytes }.

Node.js — completo
'use strict';

globalThis.elliptic = require('elliptic');
const onpostt = require('./public/onpostt.js');

const host = 'http://localhost:3000';
const app = 'meu-app';

async function main() {
  const alice = onpostt.generateKeys();
  const bob = onpostt.generateKeys();

  onpostt.connect({ host: [host] });
  await onpostt.setReaderKey(alice.privateKey);

  const post = await onpostt.signBlock(
    {
      mode: 'post',
      query: [['topic', 'onpostt']],
      content: 'olá',
      app,
    },
    alice.privateKey
  );

  if (typeof post === 'string') throw new Error(post);
  await onpostt.sendBlock(post);

  await onpostt.sendBlock(
    await onpostt.signBlock(
      {
        mode: 'reaction',
        query: [
          ['block', post.id],
          ['reaction', '🔥'],
        ],
        app,
      },
      bob.privateKey
    )
  );

  const comment = await onpostt.signBlock(
    {
      mode: 'comment',
      query: [['block', post.id]],
      content: 'primeiro comentário',
      app,
    },
    bob.privateKey
  );
  await onpostt.sendBlock(comment);

  await onpostt.sendBlock(
    await onpostt.signBlock(
      {
        mode: 'comment',
        query: [
          ['block', post.id],
          ['parent', comment.id],
        ],
        content: 'resposta',
        app,
      },
      bob.privateKey
    )
  );

  console.log(await onpostt.list({ mode: 'post', app, limit: 10 }));
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Navegador — feed ao vivo
<script src="/onpostt.min.js"></script>
<script>
  onpostt.connect({ socket: [location.origin] });

  const stop = onpostt.sub(
    { mode: 'post', app: 'meu-app', limit: 20 },
    function (event) {
      event.blocks.forEach(function (block) {
        const article = document.createElement('article');
        article.textContent = block.content;
        document.querySelector('#feed').appendChild(article);
      });
    }
  );

  // stop();
</script>
Nunca use HTML de terceiros. Trate content como texto e renderize com textContent. A CSP do relay também bloqueia scripts inline; o exemplo acima serve para uma página sua.

Segurança

  • A chave privada fica no cliente. Filtros com privatekey são rejeitados.
  • Todo bloco é validado por assinatura, id, tamanho, query e regras do modo.
  • processed_blocks impede replay de toggles.
  • delete só é aceito para o autor do alvo.
  • Notificações exigem interação real do mesmo autor e alvo; só existe uma por interação; follow notifications validam o próprio follow.
  • Mensagens entre usuários com bloqueio ativo, em qualquer sentido, são recusadas no servidor.
  • Leituras com reader verificam assinatura, timestamp e TTL; bloqueios filtram HTTP e WebSocket ao vivo nas duas direções.
  • Sem token, autores que participam de bloqueio ativo ficam ocultos; o bloco block continua público para a interface detectar o estado.
  • Uma nova chave privada é uma nova identidade: o protocolo não elimina Sybil sem um provedor externo de unicidade.
  • Respostas exigem pai do mesmo alvo e não excluído.
  • CORS e WebSocket restringem Origin.
  • Rate limit por IP e por conexão, além de limites de payload.
  • Imagens têm magic bytes, dimensões reais e tamanho verificados no relay.
  • Logs não registram chaves, senhas, corpos ou URL do banco.
HTTPS/WSS em produção. A assinatura protege a integridade dos blocos, mas não esconde metadados. Configure HOST_ALLOWED com as origens reais e mantenha ALLOW_NO_ORIGIN=false.

Limites

ItemPadrão
Bloco256 KiB
Resposta de listagem/assinatura8 MiB; acima disso, truncated: true
Conteúdo comum64 KiB
JSON de conteúdo com imagem128 KiB
Imagem decodificada72 KiB
Maior lado da imagem (protocolo)500px
Foto de perfil (cliente)150px
Campos no bloco32
Pares de query32
Emoji de reação32 code points
Blocos por consulta1000
Payload WebSocket1 MiB
Assinaturas por conexão25
Pubkeys por conexão5
Validade do token de leitura30 dias
Rate limit HTTP120 leitura / 60 escrita por minuto

Todos os valores são configuráveis por ambiente. Veja env.example. O relay aceita imagens até 500px; a demo prepara fotos de publicação em 500px e fotos de perfil em 150px.

Erros

Respostas de erro têm status: "erro", um code estável e requestId. Os códigos mais comuns:

CódigoCausa
invalid_blockformato ou regra do modo
invalid_idid não confere com o hash
invalid_signatureassinatura inválida
invalid_imagebase64, mime ou magic bytes inválidos
invalid_image_dimensionsdimensões divergentes ou acima de 500px
image_too_largeimagem decodificada acima de 72 KiB
invalid_filterfiltro inválido
invalid_readertoken reader malformado
invalid_reader_signatureassinatura do token reader inválida
reader_expiredtoken reader expirou
reader_not_yet_validreader.created_at muito no futuro
target_not_foundalvo inexistente
target_deletedalvo com tombstone
target_not_owneddelete por quem não é autor
parent_not_foundcomentário pai inexistente
parent_mismatchpai de outro alvo
interaction_not_foundnotificação aponta para interação inexistente
interaction_author_mismatchinteração de outro autor
interaction_target_mismatchinteração de outro bloco
notification_recipient_mismatchdestinatário não é dono do alvo ou não é o following
notification_kind_mismatchkind não corresponde à interação
notification_target_mismatchfollow notification não aponta para o próprio follow
notification_follow_mismatchautor do follow não é o seguidor
notification_duplicatejá existe notificação para a interação
self_notificationnotificação para a própria pubkey
user_blockedmensagem entre usuários com bloqueio ativo
private_key_forbiddenfiltro com privatekey
rate_limitedlimite de taxa
use_httpmodo não-message no WebSocket
internal_errorfalha interna
exemplo
{
  "status": "erro",
  "code": "invalid_signature",
  "message": "Assinatura inválida. Bloco rejeitado.",
  "requestId": "8f4c..."
}

Próximos passos

Executar

Suba o relay, abra a demo, publique um post e veja o perfil público, as notificações de interação e o bloqueio de usuário.

Abrir demo

Exemplos

Scripts prontos para publicar, reagir, comentar, responder e enviar mensagens.

Ver na prática

Referência completa em docs/PROTOCOL.md, docs/API.md e docs/SECURITY.md.