Segunda camada

A API, o formato, a cadeia

Tudo de que um comprovante é feito, e cada rota que cria ou serve um. Alguém te entregou um comprovante e você quer desmontá-lo sem a nossa ajuda: é para isso que esta página existe, e desmontá-lo sem a nossa ajuda é o projeto inteiro.

O acesso, com franqueza

Não há chave de API, nem cadastro, nem plano que libere um limite maior. Toda rota abaixo é pública ou protegida por um token que pertence a uma sessão ou a um comprovante e é entregue a quem a iniciou.

As rotas que carregam um documento ou o dinheiro recusam uma requisição cujo cabeçalho Origin nomeie um host que não é o nosso. Uma requisição que não manda cabeçalho Origin nenhum —um servidor chamando outro servidor— não é recusada, e preferimos dizer isso aqui a que alguém descubra e suponha que estava escondido. Não é uma superfície de integração suportada: não tem promessa de versionamento, nem chave para girar, nem limite de requisições pensado para ela, e pode mudar em qualquer implantação.

Se você quiser construir em cima disto a sério, escreva e diga. Uma superfície suportada e versionada, com chaves, é algo que construiríamos para alguém real e que não construímos para alguém hipotético.

The API

A tabela abaixo é o próprio protocolo e é publicada só em inglês.

Sign in to the console with your email, make a key, and send the citations you already have, or a whole document. Every completed check seals a receipt onto the same public chain as everything else here.

Open the console

curl -X POST "https://exhibitb.autofract.com/api/v1/checks?wait=30" \
  -H "Authorization: Bearer xb_live_<id>_<secret>" \
  -H "Content-Type: application/json" \
  -d '{"level": "says", "claims": [
        {"id": "c1", "citation": "https://www.rfc-editor.org/rfc/rfc6962",
         "quote": "publicly logging the existence of Transport Layer Security (TLS) certificates"}]}'

Prices, per claim

EXISTS1 credit · $0.02Is the source there: the registries, a fresh fetch from two vantage points, a hash of what came back.
SAYS3 credits · $0.06Are the quoted words in it, and at which character offset.
HOLDS10 credits · $0.20An adversarial pass over one claim. Returned to you while the check's session lives, never sealed, never stored. Not on Free.

Free is 350 credits a month (200 EXISTS and 50 SAYS, in one balance), resetting on the 1st, UTC. Each claim is charged at the level its receipt records, so a SAYS claim with no quote to read costs what EXISTS costs. A check that fails costs nothing, and a document longer than your balance is read as far as it covers, with the rest counted in the receipt as not checked.

Court citations are checked against a public case registry whose daily allowance this API shares with the site and the nightly benchmark. Past it, the engine falls back to the registry's open search, and then to no verdict rather than a guess.

Webhooks

One endpoint per account, https and public. Events: check.completed, check.failed. Each carries exhibitb-signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t.body)>; reject anything older than five minutes. Nothing a webhook carries is text from your document.

The whole contract is /api/v1/openapi.json (OpenAPI 3.1).

Rotas

Toda resposta é JSON. As rotas de sessão são no-store e noindex; um token bem formado cuja sessão terminou responde 404 com expired, que é coisa diferente de um token que nunca existiu.

A tabela abaixo é o próprio protocolo e é publicada só em inglês.

MétodoRotaO que fazResponde
POST/api/checkStart a check. Multipart with a file, or JSON {text, ref}. A ref of word-win, word-mac or word-web over a posted string records that the caller assembled the string out of an open document, and the receipt then names the add-in's extractor rather than the file reader's. — quem: same-origin202 {token, expires_at, arm}
GET/api/check/:tokenThe session as it stands: every claim found, and each verdict as it lands. This is what the page polls while a check runs. — quem: the tokenthe session view
GET/api/check/:token/statusThe completion signal, read off the database row rather than out of memory — so a deploy in the middle of a payment does not tell somebody who has paid that their session is gone. — quem: the token{receipt_id, sealed, checked_out, paid, closed, session}
GET/api/check/:token/preview.jsonThe unsigned, unchained receipt-shaped preview of a check that has finished but has not been sealed. — quem: the tokena receipt-shaped object with no signature
GET/api/check/:token/binding.jsonThe binding bundle: what turns the per-claim fingerprints in a receipt back into the claims they were computed from. — quem: the tokena downloaded file
POST/api/check/:token/holdsRun the adversarial third level over one claim. Off by default, never sealed into a receipt, and it sends the sentence you wrote. — quem: the token202
DELETE/api/check/:tokenEnd the session now and drop the preview. The session expires on its own either way. — quem: the token{deleted: true}
POST/api/checkoutTurn a finished check into an invoice. {token, email, tier}. — quem: same-origina payment URL
GET/api/receipt/:idThe public projection of a sealed receipt: every cited URL removed, every verdict, count and hash kept, signed in its own right.the stored canonical bytes
GET/api/receipt/:id/proofThe inclusion proof pointing at the day's Merkle root. 202 while that day has not been rooted yet.an audit path
GET/r/:id/receipt.jsonThe full sealed receipt, cited URLs and all. This is the creator's file, and the reason the projection exists. — quem: the creator's linkthe sealed bytes
GET/r/:id/receipt.pdfThe printable receipt: the page somebody staples to a report and is asked about under oath. — quem: the creator's linka PDF
GET/r/:id/qr.pngThe receipt's QR as a raster, for a printed page.a PNG
GET/chain/headWhere the chain is now.{seq, self_hash, issued_at, last_root}
GET/chain/root/:dateOne UTC day's signed root file, exactly as it was stored and published.the stored canonical bytes
GET/chain/verifyOur own replay of the chain, and the first break in it if there is one. Rooted days are re-derived on every call; the answer is memoised for sixty seconds.{ok, checked, head_seq, first_broken, …}
GET/keys.jsonEvery public key this install has ever signed with, so a verifier can pin them rather than trust the key a receipt carries about itself.the key set
GET/healthzWhether the service is up.{ok}

Uma rota de comprovante nunca confirma um identificador que ela não tem, e uma rota de administração sem o token dela responde byte a byte igual a um caminho que não tem rota nenhuma: mesmo status, mesmos cabeçalhos, mesmo corpo. É deliberado: a diferença entre as duas respostas seria, do contrário, um mapa do que existe.

Um servidor para agentes de IA

É um servidor MCP remoto em https://exhibitb.autofract.com/api/mcp, sobre Streamable HTTP, para agentes de IA e qualquer cliente MCP que consiga enviar um cabeçalho. Ele envolve a API acima e nada além disso — a mesma chave, os mesmos créditos, os mesmos comprovantes — como cinco ferramentas: check_citations, check_document, get_check, get_receipt e get_usage. Crie uma chave no console e envie-a no cabeçalho Authorization; as linhas de configuração abaixo são técnicas e saem em inglês. Não há nada para instalar.

A tabela abaixo é o próprio protocolo e é publicada só em inglês.

What every client asks for

URL     https://exhibitb.autofract.com/api/mcp
Header  Authorization: Bearer xb_live_<id>_<secret>

An mcpServers entry, for a client that reads one

{
  "mcpServers": {
    "exhibitb": {
      "type": "http",
      "url": "https://exhibitb.autofract.com/api/mcp",
      "headers": {
        "Authorization": "Bearer xb_live_<id>_<secret>"
      }
    }
  }
}

Tools

check_citationsCitations you already have, with the words quoted from each: EXISTS and SAYS, one receipt.
check_documentA whole text: finds its citations, checks them, one receipt.
get_checkA check by id: its status, the verdicts, the receipt once sealed.
get_receiptThe signed receipt: in full to the account that sealed it, the public projection to anyone else.
get_usageThe plan, the allowance and the credits left this month.

Protocol revisions 2026-07-28 and 2025-03-26 through 2025-11-25, answers in JSON. A check tool waits up to 45 seconds and otherwise answers with the id to read later. A call without a good key comes back as a tool error that says where to get one. A client that connects only through OAuth cannot use this server yet.

A assinatura

Um comprovante é JSON canônico. O self_hash dele é o SHA-256 dos bytes canônicos do comprovante com dois membros de primeiro nível removidos, self_hash e signatures, de modo que o hash vive fora de si mesmo e as assinaturas vivem fora do corpo sobre o qual o hash é calculado. É isso que permite contra-assinar um comprovante mais tarde sem mover o hash dele.

A assinatura é Ed25519 sobre os 32 bytes brutos obtidos ao decodificar self_hash de hexadecimal. Não sobre os 64 caracteres hexadecimais, nem sobre o JSON canônico, nem sobre um hash do hash: o Ed25519 aplica o próprio hash à entrada, então assinar o resumo diretamente é o esquema inteiro.

Todo valor base64 no formato é canônico: alfabeto padrão, preenchimento completo, sem espaços e com os bits baixos não usados do último símbolo em zero, de modo que uma chave tem uma única escrita e duas chaves podem ser comparadas como bytes ou como texto com a mesma resposta. Um decodificador que tolerasse um preenchimento ausente ou o alfabeto seguro para URL leria os mesmos bytes de várias escritas, e este formato admite uma.

Um comprovante carrega uma única assinatura cujo papel é issuer. Zero diz que ele nunca foi emitido; duas ou mais dizem que ele não consegue declarar por quem; as duas coisas são falhas. A assinatura é verificada contra a chave publicada para o identificador de chave dela, nunca contra a chave que o comprovante diz de si mesmo: um comprovante não pode responder por si.

A cadeia e as raízes

Há uma cadeia só. seq é densa e começa em 1, e prev_hash em seq n é o self_hash de seq n−1. Os dois ficam dentro do corpo assinado, então a posição de um comprovante é assinada junto com ele: mover um comprovante, ou reescrever qualquer coisa antes dele, muda o hash dele.

Os comprovantes encadeados de cada dia UTC formam uma árvore de Merkle, com o hash calculado tal como o RFC 6962 define: as folhas com um byte zero na frente e os nós interiores com um, de modo que uma folha nunca possa passar por nó. A raiz do dia é assinada com uma chave de raiz separada e publicada em um repositório público que não dá para editarmos em silêncio. Um dia vazio também ganha seu arquivo, então uma falha na série ficaria visível de fora.

A prova de inclusão de um comprovante é um caminho de auditoria até uma dessas raízes. Um comprovante selado hoje não tem prova até a raiz desta noite ser cortada, e a rota da prova diz pending em vez de inventar uma.

O que um comprovante público não carrega

O que quem abre o link de um comprovante vê é uma projeção: cada URL citada removida, cada localizador reduzido a {type, domain} e o identificador próprio do registro de decisões descartado junto, porque o conjunto desses identificadores seria a tabela de autoridades do documento. Todo o resto fica byte a byte, hashes e assinaturas inclusos, então quem tiver o comprovante completo consegue amarrar os dois. Uma projeção é assinada por si, com um membro a mais que o comprovante selado nunca tem, de modo que toda a superfície visível dela —veredictos, contagens, estados, domínios— fica coberta por uma assinatura, e não só pelo hash do comprovante.

O verificador

O verificador é licenciado sob MIT e está espelhado em um serviço público de código. Ele não faz requisição própria nenhuma: verificar é calcular hashes e conferir assinaturas sobre bytes que você já tem, que é por isso que o contador ao lado dele na página de verificação pode marcar zero com honestidade.

Ele roda aqui, no seu navegador, como um módulo compilado servido a partir deste site. Carregue a página de verificação uma vez, desconecte, e ela continua verificando: essa é a conferência a fazer antes de confiar em qualquer coisa disto, e é a conferência a refazer se um dia sumirmos.

A partir de um terminal

As mesmas conferências rodam a partir da linha de comando contra um comprovante no seu disco. O pacote ainda não está no registro público, então npx exhibitb hoje não resolve e não vamos imprimi-lo como se resolvesse. O que existe agora é a build para navegador acima, que é o mesmo verificador, e o código no serviço público, que compila para os dois. Quando o pacote for publicado, ele será nomeado no registro de mudanças no dia em que isso acontecer, não antes.

Rode sobre um comprovante que te enviaram

O que o suplemento envia

Um comprovante registra qual extrator produziu o texto que ele descreve, e esse campo não vale nada se um desconhecido com o mesmo documento não conseguir reconstruir a mesma cadeia de texto e obter o mesmo hash. Esta é essa especificação. É o contrato publicado por trás do rótulo exhibitb-word@1, e é por isso que um comprovante feito dentro de um documento pode ser amarrado ao documento.

A especificação abaixo é o próprio contrato publicado e é impressa só em inglês, para que uma reconstrução contra ela seja idêntica byte a byte em todo lugar.

exhibitb-word@1

exhibitb-word@1 — the string a receipt from the add-in describes

A receipt names the extractor that produced the text it was made from. When the check ran from inside Word, the text was assembled in the open document rather than parsed out of an uploaded file, and this is how. Rebuild the string from your own copy of the document, hash it, and it matches the text_sha256 in the receipt — or it does not, and you have learned something.

A ordem em que a cadeia de texto é construída

  1. Take the paragraphs of the document's main story, in the order Word lays them out. Paragraphs inside a table are part of that order: a table is read row by row, and each row cell by cell in reading order, at the point where the table sits.
  2. Then every footnote body, in footnote-number order.
  3. Then every endnote body, in endnote-number order.
  4. A paragraph's text is its own characters. A hyperlink contributes the words the reader sees; the address it points to is not inserted.
  5. The three groups are concatenated in that order — body, then footnotes, then endnotes — with nothing between them but the same separator that joins paragraphs.

O que as junta

Every paragraph, footnote body and endnote body is joined to the next by two line feeds (U+000A twice), which is one blank line.

O que nunca está dentro

  • the .docx file itself — no part of the container is uploaded
  • headers and footers
  • comments, and the identity of whoever wrote them
  • tracked changes: deleted-but-unaccepted text is not sent, insertions are sent as the paragraph reads with them applied, and no revision author is sent
  • styles, fonts, numbering, colour and every other piece of formatting
  • images, charts, embedded objects and their captions' pictures (a caption's own words are a paragraph like any other)
  • the file name, the folder it sits in, and every piece of document metadata
  • fields, in favour of the result they display

A limpeza, aplicada por último e a mais nada

  • carriage returns become line feeds: and a lone both become
  • control characters are removed (U+0000–U+0008, U+000B, U+000C, U+000E–U+001F, U+007F)
  • soft hyphens (U+00AD) are removed
  • spaces and tabs immediately before a line feed are removed
  • three or more consecutive line feeds collapse to two
  • leading and trailing whitespace is removed from the whole string

Conferindo a sua própria reconstrução

The cleaning is the same eight lines the uploaded-file path runs, and it is idempotent: running it twice changes nothing, so a rebuild that applies it once is comparable byte for byte. SHA-256 of the resulting UTF-8 bytes is the receipt's document.text_sha256; document.chars is that string's length in code units.

Por que os dois extratores contam diferente

The two paths do not read the same amount of a document, and the label is how a reader tells them apart. exhibitb-word@1 reads footnotes and endnotes. exhibitb-text@1 over an uploaded .docx reads the main story only, so a footnote-heavy report checked as a file will show fewer citations than the same report checked from inside Word. Neither number is wrong; they are counts of different strings, and the receipt says which one it made.

exhibitb-text@1

O outro rótulo. Um .docx enviado é lido por uma biblioteca que percorre os filhos do próprio documento, que são o corpo principal e as tabelas dentro dele; as notas de rodapé e as notas de fim ficam em outro lugar do contêiner e não são lidas. Todo o resto sobre a cadeia de texto é igual, inclusive a limpeza abaixo e o hash sobre ela.

O formato por inteiro

A forma canônica, o esquema do comprovante membro a membro, as regras da cadeia, a construção de Merkle, o arquivo da raiz, a prova de inclusão e a regra de projeção estão especificados em um documento só, com vetores de teste ao lado: o mesmo arquivo, byte a byte, que vai junto no repositório do próprio verificador. Todo número nele é um número que um terceiro consegue reproduzir.

Vetores de teste

  • the canonical form of an object, with the worked examples that pin the whitespace and the number formatting
  • self-hash vectors, including an object carrying a wrong self-hash and a nonsense signature array hashing to what it hashes to without them
  • a signed receipt, its key set and its verification result
  • Merkle roots for trees of 0, 1, 2, 3 and 7 leaves, with every audit path

Se algo nesta página estiver errado, ou se uma conferência que você escreveu contra ela falhar e você achar que não deveria, o endereço de suporte no rodapé chega a uma pessoa que vai olhar os bytes com você.