Второй слой
API, формат, цепочка
Всё, из чего сделан протокол, и каждый маршрут, который его создаёт или отдаёт. Вам передали протокол, и вы хотите разобрать его без нашей помощи — страница ровно для этого, а то, что его можно разобрать без нашей помощи, и есть весь замысел.
Доступ, честно
Ключа к API нет, аккаунта нет, и нет тарифа, который открывает лимит повыше. Каждый маршрут ниже либо открыт, либо закрыт токеном, который принадлежит одной сессии или одному протоколу и выдан тому, кто её начал.
Маршруты, по которым идёт документ или деньги, отклоняют запрос, чей заголовок Origin называет не наш хост. Запрос, который не шлёт Origin вовсе — сервер, обращающийся к серверу, — не отклоняется, и мы предпочитаем сказать это здесь, а не дожидаться, пока кто-нибудь это обнаружит и решит, что от него скрывали. Это не поддерживаемая поверхность интеграции: у неё нет обещаний по версиям, нет ключа для ротации и нет настроенного под неё ограничения частоты, и она может измениться в любом выкладывании.
Если вы хотите строить на этом всерьёз, напишите и скажите. Поддерживаемую версионируемую поверхность с ключами мы построили бы для настоящего вызывающего и не стали строить для гипотетического.
The API
Таблица ниже — это и есть сам интерфейс, и она публикуется только по-английски.
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.
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
| EXISTS | 1 credit · $0.02 | Is the source there: the registries, a fresh fetch from two vantage points, a hash of what came back. |
|---|---|---|
| SAYS | 3 credits · $0.06 | Are the quoted words in it, and at which character offset. |
| HOLDS | 10 credits · $0.20 | An 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).
Маршруты
Любой ответ — JSON. Маршруты сессии отдаются с no-store и noindex; правильно составленный токен, чья сессия закончилась, отвечает 404 с expired, и это не то же самое, что токен, которого никогда не было.
Таблица ниже — это и есть сам интерфейс, и она публикуется только по-английски.
| Метод | Маршрут | Что делает | Отвечает |
|---|---|---|---|
| POST | /api/check | Start 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. — кому: same-origin | 202 {token, expires_at, arm} |
| GET | /api/check/:token | The session as it stands: every claim found, and each verdict as it lands. This is what the page polls while a check runs. — кому: the token | the session view |
| GET | /api/check/:token/status | The 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. — кому: the token | {receipt_id, sealed, checked_out, paid, closed, session} |
| GET | /api/check/:token/preview.json | The unsigned, unchained receipt-shaped preview of a check that has finished but has not been sealed. — кому: the token | a receipt-shaped object with no signature |
| GET | /api/check/:token/binding.json | The binding bundle: what turns the per-claim fingerprints in a receipt back into the claims they were computed from. — кому: the token | a downloaded file |
| POST | /api/check/:token/holds | Run the adversarial third level over one claim. Off by default, never sealed into a receipt, and it sends the sentence you wrote. — кому: the token | 202 |
| DELETE | /api/check/:token | End the session now and drop the preview. The session expires on its own either way. — кому: the token | {deleted: true} |
| POST | /api/checkout | Turn a finished check into an invoice. {token, email, tier}. — кому: same-origin | a payment URL |
| GET | /api/receipt/:id | The 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/proof | The 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.json | The full sealed receipt, cited URLs and all. This is the creator's file, and the reason the projection exists. — кому: the creator's link | the sealed bytes |
| GET | /r/:id/receipt.pdf | The printable receipt: the page somebody staples to a report and is asked about under oath. — кому: the creator's link | a PDF |
| GET | /r/:id/qr.png | The receipt's QR as a raster, for a printed page. | a PNG |
| GET | /chain/head | Where the chain is now. | {seq, self_hash, issued_at, last_root} |
| GET | /chain/root/:date | One UTC day's signed root file, exactly as it was stored and published. | the stored canonical bytes |
| GET | /chain/verify | Our 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.json | Every 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 | /healthz | Whether the service is up. | {ok} |
Маршрут протокола никогда не подтверждает идентификатор, которого у него нет, а административный маршрут без токена отвечает байт в байт так же, как путь, за которым вообще нет маршрута: тот же статус, те же заголовки, то же тело. Это сделано намеренно: разница между двумя ответами была бы картой того, что существует.
Сервер для ИИ-агентов
Это удалённый MCP-сервер по адресу https://exhibitb.autofract.com/api/mcp, на Streamable HTTP, для ИИ-агентов и любого MCP-клиента, который умеет отправлять заголовок. Он оборачивает API выше и ничего сверх того — тот же ключ, те же кредиты, те же протоколы — в пять инструментов: check_citations, check_document, get_check, get_receipt и get_usage. Создайте ключ в консоли и передайте его в заголовке Authorization; строки настройки ниже техничные и печатаются на английском. Устанавливать ничего не нужно.
Таблица ниже — это и есть сам интерфейс, и она публикуется только по-английски.
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_citations | Citations you already have, with the words quoted from each: EXISTS and SAYS, one receipt. |
|---|---|
| check_document | A whole text: finds its citations, checks them, one receipt. |
| get_check | A check by id: its status, the verdicts, the receipt once sealed. |
| get_receipt | The signed receipt: in full to the account that sealed it, the public projection to anyone else. |
| get_usage | The 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.
Подпись
Протокол — это канонический JSON. Его self_hash — это SHA-256 канонических байтов протокола, из которого убраны два члена верхнего уровня: сам self_hash и signatures. Хеш таким образом живёт вне себя, а подписи живут вне хешируемого тела, и именно это позволяет позже подписать протокол ещё раз, не сдвинув его хеш.
Подпись — это Ed25519 по 32 сырым байтам, полученным hex-декодированием self_hash. Не по 64 шестнадцатеричным символам, не по каноническому JSON и не по хешу от хеша: Ed25519 хеширует свой вход сам, поэтому подписать дайджест напрямую — это и есть вся схема.
Каждое значение base64 в формате каноническое: стандартный алфавит, выравнивание ровно по правилам, никаких пробелов, а неиспользуемые младшие биты последнего символа — нули. Поэтому у одного ключа ровно одно написание, и два ключа можно сравнивать хоть как байты, хоть как текст с одним и тем же ответом. Декодер, который терпит недостающий символ выравнивания или URL-безопасный алфавит, прочитал бы одни и те же байты из нескольких написаний, а этот формат допускает одно.
Протокол несёт ровно одну подпись с ролью issuer. Ноль означает, что он никогда не выпускался; две и больше — что он не может сказать, кем выпущен; и то и другое — провал. Подпись проверяется по ключу, опубликованному для её идентификатора ключа, и никогда по ключу, который протокол несёт сам о себе: нельзя позволять протоколу ручаться за самого себя.
Цепочка и корни
Цепочка одна. seq плотный и начинается с 1, а prev_hash на seq n — это self_hash для seq n−1. Оба лежат внутри подписанного тела, поэтому место протокола подписано вместе с ним: перенести протокол или переписать что-либо до него — значит изменить его хеш.
Сцепленные протоколы каждых суток по UTC образуют одно дерево Меркла, хешируемое ровно так, как определяет RFC 6962: листья с нулевым байтом впереди, внутренние узлы с единицей, чтобы лист нельзя было выдать за узел. Корень дня подписывается отдельным корневым ключом и публикуется в открытом репозитории, который мы не можем тихо отредактировать. Пустой день тоже получает файл, поэтому разрыв в череде был бы виден снаружи.
Доказательство включения протокола — это путь аудита в один из этих корней. У протокола, запечатанного сегодня, доказательства нет, пока ночью не срезан корень, и маршрут доказательства отвечает pending, а не выдумывает его.
Чего не несёт открытый протокол
То, что читатель видит по ссылке протокола, — это проекция: каждый процитированный адрес убран, каждый локатор сведён к {type, domain}, а собственный идентификатор записи в судебном реестре выброшен вместе с ним, потому что набор таких идентификаторов был бы перечнем источников документа. Всё остальное остаётся байт в байт, включая хеши и подписи, так что тот, у кого есть полный протокол, может связать одно с другим. Проекция подписана сама по себе, с одним дополнительным членом, которого у запечатанного протокола нет, поэтому вся её видимая поверхность — вердикты, количества, статусы, домены — покрыта подписью, а не только хешем протокола.
Верификатор
Верификатор под лицензией MIT и продублирован на открытом хостинге кода. Своих запросов он не делает: проверка — это хеширование и проверка подписей над байтами, которые у вас уже есть, поэтому счётчик рядом с ним на странице проверки может честно показывать ноль.
Он работает здесь, у вас в браузере, как скомпилированный модуль, отдаваемый с этого сайта. Откройте страницу проверки один раз, отключитесь — и он всё равно проверяет. Это та проверка, которую стоит провести, прежде чем доверять чему-либо из написанного здесь, и та же самая, которую стоит повторить, если мы когда-нибудь исчезнем.
Из терминала
Те же проверки запускаются из командной строки по протоколу у вас на диске. Пакета в открытом реестре пока нет, поэтому npx exhibitb сегодня не разрешается, и печатать это так, будто он работает, мы не станем. Сейчас существует браузерная сборка выше — это тот же верификатор — и исходный код на открытом хостинге, который собирается в обе. Когда пакет опубликуют, его назовут в истории изменений в тот же день, а не раньше.
Что отправляет надстройка
Протокол записывает, какой извлекатель произвёл текст, который он описывает, и это поле не стоит ничего, если посторонний с тем же документом на руках не может собрать ту же строку и получить тот же хеш. Вот эта спецификация. Она — опубликованный договор за меткой exhibitb-word@1, и она же объясняет, почему протокол, сделанный внутри документа, вообще можно привязать к документу.
Спецификация ниже — это сам опубликованный договор, и она печатается только по-английски, чтобы пересборка по ней везде давала байт в байт одно и то же.
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.
В каком порядке собирается строка
- 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.
- Then every footnote body, in footnote-number order.
- Then every endnote body, in endnote-number order.
- A paragraph's text is its own characters. A hyperlink contributes the words the reader sees; the address it points to is not inserted.
- The three groups are concatenated in that order — body, then footnotes, then endnotes — with nothing between them but the same separator that joins paragraphs.
Что их соединяет
Every paragraph, footnote body and endnote body is joined to the next by two line feeds (U+000A twice), which is one blank line.
Чего в ней нет никогда
- 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
Очистка — последним шагом и больше нигде
- 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
Как проверить свою пересборку
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.
Почему два извлекателя считают по-разному
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
Другая метка. Загруженный .docx разбирает библиотека, которая обходит собственных потомков документа, — это основной текст и таблицы внутри него; обычные и концевые сноски лежат в контейнере отдельно и не читаются. Всё остальное в строке то же самое, включая очистку ниже и хеш поверх неё.
Формат целиком
Каноническая форма, схема протокола член за членом, правила цепочки, построение Меркла, файл корня, доказательство включения и правило проекции описаны в одном документе, а рядом с ним лежат тестовые векторы — тот же файл, байт в байт, что едет в самом дистрибутиве верификатора. Любое число в нём воспроизводимо третьей стороной.
Тестовые векторы
- 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
Если что-то на этой странице неверно или написанная вами проверка по ней падает, а вы считаете, что не должна, по адресу поддержки в подвале отвечает человек, который сядет разбирать байты вместе с вами.