tttps-proof-explorer / index.html
Pittro's picture
Ollama badge: PyPI upload pending -> v0.1.0 published (verified on PyPI 2026-08-06)
e11b7f9 verified
Raw
History Blame Contribute Delete
28.6 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TTTPS Proof-of-Time Explorer</title>
<style>
:root {
--bg: #0d1117;
--panel: #161b22;
--border: #30363d;
--text: #e6edf3;
--muted: #8b949e;
--accent: #2f81f7;
--green: #1a7f37;
--yellow: #9a6700;
--red: #cf222e;
--gray: #6e7781;
--blue: #1f6feb;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #ffffff;
--panel: #f6f8fa;
--border: #d0d7de;
--text: #1f2328;
--muted: #57606a;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
line-height: 1.55;
}
.wrap { max-width: 860px; margin: 0 auto; padding: 32px 20px 64px; }
h1 { font-size: 1.55rem; margin-bottom: 4px; }
h2 { font-size: 1.15rem; margin: 0 0 10px; }
.sub { color: var(--muted); font-size: 0.95rem; margin-bottom: 24px; }
.intro { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 18px 20px; margin-bottom: 24px; font-size: 0.94rem; }
.intro code { background: rgba(127,127,127,0.15); padding: 1px 5px; border-radius: 4px; font-family: var(--mono); font-size: 0.88em; }
.intro a { color: var(--accent); }
textarea {
width: 100%; min-height: 100px; resize: vertical;
background: var(--panel); color: var(--text); border: 1px solid var(--border);
border-radius: 8px; padding: 12px; font-size: 0.95rem; font-family: inherit;
}
.row { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; align-items: center; }
button {
background: var(--accent); color: white; border: none; border-radius: 8px;
padding: 10px 18px; font-size: 0.95rem; font-weight: 600; cursor: pointer;
}
button.secondary { background: var(--panel); color: var(--text); border: 1px solid var(--border); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
button:hover:not(:disabled) { filter: brightness(1.08); }
.panel {
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
padding: 18px 20px; margin-top: 16px; font-size: 0.92rem;
}
.field-table { width: 100%; border-collapse: collapse; margin-top: 8px; }
.field-table td { padding: 6px 4px; border-bottom: 1px solid var(--border); vertical-align: top; }
.field-table td:first-child { color: var(--muted); white-space: nowrap; padding-right: 14px; width: 220px; }
.mono { font-family: var(--mono); word-break: break-all; font-size: 0.87em; }
.badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 0.82em; font-weight: 700; color: white; }
.badge.green { background: var(--green); }
.badge.yellow { background: var(--yellow); }
.badge.red { background: var(--red); }
.badge.gray { background: var(--gray); }
.badge.blue { background: var(--blue); }
.note { color: var(--muted); font-size: 0.88em; margin-top: 10px; }
.raw { margin-top: 10px; }
.raw summary { cursor: pointer; color: var(--muted); font-size: 0.85em; }
.raw pre {
background: rgba(127,127,127,0.08); border: 1px solid var(--border); border-radius: 6px;
padding: 10px; overflow-x: auto; font-size: 0.8em; margin-top: 6px;
}
.status-msg { font-size: 0.9rem; color: var(--muted); margin-top: 10px; }
.status-msg.err { color: var(--red); }
footer { margin-top: 40px; color: var(--muted); font-size: 0.82rem; border-top: 1px solid var(--border); padding-top: 16px; }
footer a { color: var(--accent); }
footer .badges { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
hr { border: none; border-top: 1px solid var(--border); margin: 28px 0; }
#verifyInput { width: 100%; font-family: var(--mono); font-size: 0.88rem; padding: 8px 10px; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 8px; }
/* --- Hero --- */
.hero { margin-bottom: 28px; }
.hero h1 { font-size: 1.9rem; margin-bottom: 8px; }
.hero .tagline { font-size: 1.02rem; color: var(--text); opacity: 0.85; max-width: 640px; margin-bottom: 18px; }
.stats-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.stat {
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
padding: 10px 16px; min-width: 150px; flex: 1;
}
.stat .num { font-size: 1.15rem; font-weight: 700; }
.stat .lbl { font-size: 0.78em; color: var(--muted); margin-top: 2px; }
/* --- Architecture flow --- */
.flow {
display: flex; align-items: center; gap: 0; flex-wrap: wrap; margin: 14px 0 6px;
}
.flow-box {
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
padding: 12px 14px; font-size: 0.85rem; text-align: center; flex: 1; min-width: 150px;
}
.flow-box .t { font-weight: 700; margin-bottom: 3px; }
.flow-box .d { color: var(--muted); font-size: 0.82em; }
.flow-arrow { color: var(--muted); font-size: 1.3rem; padding: 0 10px; flex: 0 0 auto; }
@media (max-width: 640px) {
.flow { flex-direction: column; align-items: stretch; }
.flow-arrow { text-align: center; padding: 4px 0; transform: rotate(90deg); }
}
/* --- Quickstart tabs --- */
.tabs { margin-top: 10px; }
.tab-bar { display: flex; gap: 6px; flex-wrap: wrap; border-bottom: 1px solid var(--border); padding-bottom: 0; }
.tab-btn {
background: transparent; color: var(--muted); border: none; border-bottom: 2px solid transparent;
border-radius: 0; padding: 9px 14px; font-size: 0.86rem; font-weight: 600; cursor: pointer;
}
.tab-btn.active { color: var(--text); border-bottom-color: var(--accent); }
.tab-btn:hover:not(.active) { color: var(--text); filter: none; }
.tab-panel { display: none; padding-top: 14px; }
.tab-panel.active { display: block; }
.tab-status { margin-bottom: 10px; }
pre.code {
background: rgba(127,127,127,0.08); border: 1px solid var(--border); border-radius: 8px;
padding: 14px; overflow-x: auto; font-size: 0.82rem; font-family: var(--mono); line-height: 1.5;
white-space: pre;
}
</style>
</head>
<body>
<div class="wrap">
<!-- ============ HERO ============ -->
<div class="hero">
<h1>TTTPS Proof-of-Time Explorer</h1>
<div class="tagline">
A <strong>Layer 7 AI control-plane primitive</strong>: cryptographic proof-of-time
receipts for LLM and agent outputs. Any framework's output gets hashed, sealed with a
signed timestamp, and independently verifiable after the fact &mdash; without ever
blocking or altering the original response.
</div>
<div class="stats-row">
<div class="stat">
<div class="num">8.6ms</div>
<div class="lbl">measured warm p50 for a full <code>POST /v1/anchor</code> round trip against
the live public API (p90 12.0ms, n=30). Cold p50 33.2ms / p90 45.1ms, n=30. 61/61 HTTP 200.
Client in the same region; your network path will differ.</div>
</div>
<div class="stat">
<div class="num">Fail-open, always</div>
<div class="lbl">a sealing failure never blocks or mutates your LLM response &mdash;
it degrades to a status flag. Demonstrated by blocking Roughtime UDP until the quorum
broke: <code>/v1/anchor</code> stayed 3/3 HTTP 200 and <code>time_source</code> downgraded
to <code>local</code> rather than lying.</div>
</div>
<div class="stat">
<div class="num">8 frameworks</div>
<div class="lbl">LangChain &middot; LiteLLM &middot; LlamaIndex &middot; smolagents &middot;
vLLM &middot; Ollama &middot; DeepEval &middot; Ragas. Which are installable today is
listed on the API page, not duplicated here.</div>
</div>
</div>
<div class="row" style="margin-top:14px;">
<a href="https://kpp.kenosian.com/tttps-api.html" target="_blank" rel="noopener" style="text-decoration:none;">
<button>Full API reference &amp; framework matrix &rarr;</button>
</a>
<span class="note" style="margin-top:0;">
Endpoint-by-endpoint spec, every integration's real install command, and which ones are
PyPI-published vs. locally verified &mdash; kept in sync with what's actually shipped,
not aspirational.
</span>
</div>
</div>
<!-- ============ ARCHITECTURE ============ -->
<h2>How it fits into your stack</h2>
<div class="flow">
<div class="flow-box">
<div class="t">Your app / agent</div>
<div class="d">LangChain, LiteLLM, LlamaIndex, smolagents, vLLM, or Ollama</div>
</div>
<div class="flow-arrow">&rarr;</div>
<div class="flow-box">
<div class="t">OpenTTT integration</div>
<div class="d">sha256(output) computed locally, sent async, fail-open</div>
</div>
<div class="flow-arrow">&rarr;</div>
<div class="flow-box">
<div class="t">KPP Provenance API</div>
<div class="d"><code>kpp.kenosian.com</code> &mdash; signs + timestamps the hash</div>
</div>
<div class="flow-arrow">&rarr;</div>
<div class="flow-box">
<div class="t">Verifiable receipt</div>
<div class="d"><code>receipt_id</code> + signature, independently re-checkable anytime</div>
</div>
</div>
<div class="note" style="margin-bottom:28px;">
Your original LLM/agent response is never touched or delayed &mdash; only its hash leaves
your process, asynchronously, and only a receipt comes back.
</div>
<hr>
<!-- ============ QUICKSTART ============ -->
<h2>Quickstart &mdash; pick your framework</h2>
<div class="note" style="margin-top:-6px; margin-bottom:2px;">
Every integration attaches a cryptographic audit-trail timestamp. None of them certify
legal or regulatory compliance (EU AI Act, FDA, etc.) on their own.
</div>
<div class="tabs">
<div class="tab-bar" id="tabBar">
<button class="tab-btn active" data-tab="langchain">LangChain</button>
<button class="tab-btn" data-tab="litellm">LiteLLM</button>
<button class="tab-btn" data-tab="llamaindex">LlamaIndex</button>
<button class="tab-btn" data-tab="smolagents">smolagents</button>
<button class="tab-btn" data-tab="vllm">vLLM</button>
<button class="tab-btn" data-tab="ollama">Ollama</button>
</div>
<div class="tab-panel active" id="tab-langchain">
<div class="tab-status">
<span class="badge blue">PyPI: v0.3.0 published</span>
</div>
<div class="note" style="margin-top:0;">
<code>pip install langchain-openttt</code> installs the published v0.3.0 package, which
ships both <code>TTTPSTimestampCallbackHandler</code> (internal PoT server) and
<code>TTTPSTimestampCallbackHandlerKPPLive</code> (this public self-serve API) &mdash;
verified importable from the live PyPI wheel in an isolated venv. It is also registered in
LangChain's own docs repo rather than mentioned by a third party:
<a href="https://github.com/langchain-ai/docs/pull/5257" target="_blank" rel="noopener">langchain-ai/docs&nbsp;#5257</a>
adds it to the integrations YAML and is <strong>merged</strong> into the LangChain External
Integrations Registry.
</div>
<pre class="code">pip install langchain-openttt # v0.3.0
import os
os.environ["KPP_API_KEY"] = "kpp_prov_..." # mint free: POST https://kpp.kenosian.com/v1/keys
from langchain_openttt import TTTPSTimestampCallbackHandlerKPPLive
resp = llm.invoke("hi", config={"callbacks": [TTTPSTimestampCallbackHandlerKPPLive()]})
# resp.generation_info["tttps_receipt"]
# -> {"status": "ok", "receipt_id": "...", "receipt": "...", "time": "...", ...}</pre>
</div>
<div class="tab-panel" id="tab-litellm">
<div class="tab-status">
<span class="badge blue">PyPI: v0.1.0 published</span>
</div>
<div class="note" style="margin-top:0;">
Docs recipe (inline <code>CustomLogger</code>) shipped upstream via
<a href="https://github.com/BerriAI/litellm-docs/pull/760" target="_blank" rel="noopener">BerriAI/litellm-docs&nbsp;#760</a>
(<code>docs/observability/tttps_provenance.md</code>, open).
</div>
<pre class="code">pip install litellm-openttt # v0.1.0
import litellm
from litellm_openttt import TTTPSTimestampLoggerKPPLive
litellm.callbacks = [TTTPSTimestampLoggerKPPLive()]
resp = litellm.completion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "hi"}],
)
print(resp.tttps_receipt)
# {"status": "ok", "receipt_id": "...", "receipt": "...", "time": "...", ...}</pre>
</div>
<div class="tab-panel" id="tab-llamaindex">
<div class="tab-status">
<span class="badge blue">PyPI: v0.1.0 published</span>
</div>
<div class="note" style="margin-top:0;">
No upstream PR here yet. <code>run-llama/llama_index</code>'s own CONTRIBUTING.md states
new integration packages aren't accepted into the monorepo and must already be published
to PyPI independently first. That precondition is now met, so a docs PR is the next step
rather than a blocked one.
</div>
<pre class="code">pip install llamaindex-openttt # v0.1.0
from llama_index.core import Settings
from llama_index.core.callbacks import CallbackManager
from llamaindex_openttt import TTTPSTimestampCallbackHandlerKPPLive
Settings.callback_manager = CallbackManager([TTTPSTimestampCallbackHandlerKPPLive()])
resp = llm.complete("hi")
print(resp.additional_kwargs["tttps_receipt"])
# {"status": "ok", "receipt_id": "...", "receipt": "...", "time": "...", ...}</pre>
</div>
<div class="tab-panel" id="tab-smolagents">
<div class="tab-status">
<span class="badge blue">PyPI: v0.1.0 published</span>
</div>
<div class="note" style="margin-top:0;">
Example shipped upstream via
<a href="https://github.com/huggingface/smolagents/pull/2597" target="_blank" rel="noopener">huggingface/smolagents&nbsp;#2597</a>
(<code>examples/mcp_tttps_provenance.py</code>, open). Full walkthrough in the
<a href="https://github.com/huggingface/smolagents/discussions/2600" target="_blank" rel="noopener">Show and tell discussion&nbsp;#2600</a>.
smolagents can also reach the same tools over MCP with no adapter package at all.
</div>
<pre class="code">pip install smolagents-openttt # v0.1.0
from smolagents import CodeAgent
from smolagents_openttt import ttts_pot_step_callback_kpp_live
agent = CodeAgent(
tools=[],
model=your_model,
step_callbacks=[ttts_pot_step_callback_kpp_live],
)
result = agent.run("your task")
for step in agent.memory.steps:
print(getattr(step, "tttps_receipt", None))
# {"status": "ok", "receipt_id": "...", "receipt": "...", "time": "...", ...}</pre>
</div>
<div class="tab-panel" id="tab-vllm">
<div class="tab-status">
<span class="badge blue">PyPI: v0.1.0 published</span>
</div>
<div class="note" style="margin-top:0;">
Registers via vLLM's official <code>--middleware</code> flag (confirmed live against
vLLM's own <code>build_app()</code> with a real server and a completed
<code>/v1/completions</code> round trip). The example shipped upstream via
<a href="https://github.com/vllm-project/vllm/pull/50998" target="_blank" rel="noopener">vllm-project/vllm&nbsp;#50998</a>
(<code>examples/observability/tttps/</code>, open) calls the public
<code>kpp.kenosian.com</code> API, so external readers can reproduce it.
</div>
<pre class="code">pip install vllm-openttt # v0.1.0
export KPP_API_KEY=kpp_prov_... # mint free: POST https://kpp.kenosian.com/v1/keys
vllm serve facebook/opt-125m \
--middleware vllm_openttt.TTTPSMiddleware
# curl http://localhost:8000/v1/completions ...
# -> response header: X-TTTPS-Receipt: {"status": "ok", "receipt_id": "...", "receipt": "...", ...}</pre>
</div>
<div class="tab-panel" id="tab-ollama">
<div class="tab-status">
<span class="badge blue">PyPI: v0.1.0 published</span>
</div>
<div class="note" style="margin-top:0;">
Ollama has no plugin/middleware extension point (confirmed by reading
<code>server/routes.go</code>), so this is a small reverse proxy in front of it. It
already targets the public <code>kpp.kenosian.com</code> API. Both non-streaming and
streaming (NDJSON) responses are supported &mdash; streaming gets a receipt attached to
its final <code>done:true</code> line only, with zero added per-token latency. Ollama's
README does have a real &ldquo;Community Integrations&rdquo; list open to PRs, but this
proxy isn't in its own public repo yet, so there's nothing citable to link there yet
&mdash; tracked, not forgotten.
</div>
<pre class="code">KPP_API_KEY=kpp_prov_... python ollama_proxy.py --port 11435
# point your Ollama client at localhost:11435 instead of 11434
curl localhost:11435/api/generate -d '{"model":"qwen2.5:0.5b","prompt":"hi","stream":false}'
# -> ollama's normal JSON, plus a "tttps_receipt" field and
# X-TTTPS-Receipt / X-TTTPS-Status response headers</pre>
</div>
</div>
<hr>
<!-- ============ LIVE INSPECTOR (existing, untouched logic) ============ -->
<h2>Live Inspector &mdash; try it yourself, right now</h2>
<div class="sub">Live demo of a proposal specification, not a ratified standard.</div>
<div class="intro">
This section calls the real, public <code>/v1/anchor</code> and <code>/v1/verify</code>
endpoints directly from your browser &mdash; nothing here is simulated, and there is no
backend server for this demo other than your own browser and the API itself, served from
<a href="https://kpp.kenosian.com" target="_blank" rel="noopener">kpp.kenosian.com</a>.
<br><br>
<strong>What a receipt is, and is not:</strong> a receipt cryptographically attests
<em>when</em> a content hash was sealed, its <em>order</em> relative to other seals, and
that it is <em>tamper-evident</em> after the fact. It does <strong>not</strong> attest
that the content itself is correct, true, or valid &mdash; that is a separate, orthogonal
claim TTTPS does not make.
<br><br>
The text you type below is hashed with sha256 <strong>in your own browser</strong> before
anything is sent anywhere. Only the hash ever leaves your machine.
</div>
<textarea id="contentInput" placeholder="Type or paste anything — an LLM completion, a document, a log line..."></textarea>
<div class="row">
<button id="anchorBtn">Anchor (seal this content)</button>
<span id="anchorStatus" class="status-msg"></span>
</div>
<div id="anchorPanel" class="panel" style="display:none;"></div>
<hr>
<div class="row" style="align-items:flex-start;">
<div style="flex:1; min-width:240px;">
<label for="verifyInput" style="display:block; margin-bottom:6px; color:var(--muted); font-size:0.88em;">receipt_id</label>
<input id="verifyInput" type="text" placeholder="paste or auto-filled from a seal above">
</div>
<button id="verifyBtn" class="secondary" style="margin-top:20px;">Verify this receipt_id</button>
</div>
<span id="verifyStatus" class="status-msg"></span>
<div id="verifyPanel" class="panel" style="display:none;"></div>
<footer>
Backend: <code>https://kpp.kenosian.com</code> &middot; API spec: proposal, not a ratified standard &middot;
Source endpoints: <code>POST /v1/keys</code>, <code>POST /v1/anchor</code>, <code>POST /v1/verify</code>
&middot; This is a static page; your browser talks to the API directly (CORS-enabled).
<div class="badges">
<a href="https://kpp.kenosian.com/verify.html" target="_blank" rel="noopener"><img src="https://img.shields.io/endpoint?url=https://kpp.kenosian.com/v1/badge/status.json" alt="TTTPS receipts sealed" style="vertical-align:middle;"></a>
<a href="https://pypi.org/project/langchain-openttt/" target="_blank" rel="noopener"><span class="badge blue">PyPI: langchain-openttt</span></a>
<a href="https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/" target="_blank" rel="noopener"><span class="badge gray">IETF Internet-Draft: draft-helmprotocol-tttps (proposal, not ratified)</span></a>
<a href="https://github.com/Helm-Protocol/OpenTTT" target="_blank" rel="noopener"><span class="badge gray">GitHub: Helm-Protocol/OpenTTT</span></a>
</div>
</footer>
</div>
<script>
const KPP_BASE = "https://kpp.kenosian.com";
const KEYS_URL = KPP_BASE + "/v1/keys";
const ANCHOR_URL = KPP_BASE + "/v1/anchor";
const VERIFY_URL = KPP_BASE + "/v1/verify";
// Design note (mirrors the server-side equivalent used in the Gradio prototype
// of this demo, kept for reference): mint ONE demo API key per browser session
// on first use, cache it in memory for the lifetime of this page, and reuse it
// for every anchor click in this tab. Minting a fresh key on every single click
// would burn through the 1,000-seal free quota per key far faster than
// necessary for a demo. Because this is a static page with no shared backend
// process, "per session" here naturally means "per browser tab" rather than
// "per Space process" — each visitor gets their own key, so no visitor can
// exhaust another visitor's quota.
let demoKeyPromise = null;
function getDemoKey() {
if (!demoKeyPromise) {
demoKeyPromise = fetch(KEYS_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "tttps-hf-space-demo@kenosian.com", use_case: "public HF static Space demo (tttps-proof-explorer)" })
}).then(async (r) => {
if (!r.ok) throw new Error("key mint failed: HTTP " + r.status + " " + (await r.text()));
return r.json();
});
}
return demoKeyPromise;
}
async function sha256Hex(text) {
const enc = new TextEncoder().encode(text);
const digest = await crypto.subtle.digest("SHA-256", enc);
return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, "0")).join("");
}
function badgeHtml(timeSource) {
if (timeSource === "roughtime_chain") {
return '<span class="badge green">&#10003; roughtime_chain (independently witnessed)</span>';
}
if (timeSource) {
return '<span class="badge yellow">&#9888; ' + escapeHtml(timeSource) + ' (not independently witnessed)</span>';
}
return '<span class="badge gray">? unknown</span>';
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
const contentInput = document.getElementById("contentInput");
const anchorBtn = document.getElementById("anchorBtn");
const anchorStatus = document.getElementById("anchorStatus");
const anchorPanel = document.getElementById("anchorPanel");
const verifyInput = document.getElementById("verifyInput");
const verifyBtn = document.getElementById("verifyBtn");
const verifyStatus = document.getElementById("verifyStatus");
const verifyPanel = document.getElementById("verifyPanel");
anchorBtn.addEventListener("click", async () => {
const text = contentInput.value;
anchorPanel.style.display = "none";
anchorStatus.className = "status-msg";
if (!text || !text.trim()) {
anchorStatus.textContent = "Enter some text above first.";
return;
}
anchorBtn.disabled = true;
anchorStatus.textContent = "Minting demo key / sealing…";
try {
const keyData = await getDemoKey();
const contentHash = await sha256Hex(text);
const resp = await fetch(ANCHOR_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content_hash: contentHash, key: keyData.api_key })
});
if (resp.status === 402) {
anchorStatus.className = "status-msg err";
anchorStatus.textContent = "Free seal quota exhausted (402) for this browser session's demo key. Reload the page to mint a new one.";
anchorBtn.disabled = false;
return;
}
if (!resp.ok) {
const errText = await resp.text();
anchorStatus.className = "status-msg err";
anchorStatus.textContent = "/v1/anchor returned HTTP " + resp.status + ": " + errText;
anchorBtn.disabled = false;
return;
}
const data = await resp.json();
anchorStatus.textContent = "";
verifyInput.value = data.receipt_id || "";
anchorPanel.innerHTML = `
<strong>Receipt sealed</strong>
<table class="field-table">
<tr><td>receipt_id</td><td class="mono">${escapeHtml(data.receipt_id)}</td></tr>
<tr><td>content_hash (sha256, computed in your browser)</td><td class="mono">${escapeHtml(contentHash)}</td></tr>
<tr><td>sealed time (UTC)</td><td class="mono">${escapeHtml(data.time)}</td></tr>
<tr><td>time source</td><td>${badgeHtml(data.time_source)}</td></tr>
<tr><td>receipt (HMAC signature)</td><td class="mono">${escapeHtml(data.receipt)}</td></tr>
<tr><td>backdating bound</td><td class="mono">${escapeHtml(data.backdating_p)}</td></tr>
<tr><td>demo key quota (this browser session)</td><td>${data.used} / ${data.quota} used (${data.remaining} remaining)</td></tr>
</table>
<div class="note">${escapeHtml(data.note || "A receipt proves when, order, and tamper-evidence — not correctness.")}</div>
<details class="raw"><summary>Raw /v1/anchor JSON response</summary><pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre></details>
`;
anchorPanel.style.display = "block";
} catch (err) {
anchorStatus.className = "status-msg err";
anchorStatus.textContent = "Error: " + err.message;
} finally {
anchorBtn.disabled = false;
}
});
verifyBtn.addEventListener("click", async () => {
const receiptId = verifyInput.value.trim();
verifyPanel.style.display = "none";
verifyStatus.className = "status-msg";
if (!receiptId) {
verifyStatus.textContent = "Seal something first, or paste a receipt_id above.";
return;
}
verifyBtn.disabled = true;
verifyStatus.textContent = "Verifying…";
try {
const resp = await fetch(VERIFY_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ receipt_id: receiptId })
});
if (!resp.ok) {
const errText = await resp.text();
verifyStatus.className = "status-msg err";
verifyStatus.textContent = "/v1/verify returned HTTP " + resp.status + ": " + errText;
verifyBtn.disabled = false;
return;
}
const data = await resp.json();
verifyStatus.textContent = "";
const verifiedBadge = data.verified
? '<span class="badge green">verified: true</span>'
: '<span class="badge red">verified: false</span>';
verifyPanel.innerHTML = `
<strong>Verify result</strong> &nbsp; ${verifiedBadge}
<table class="field-table">
<tr><td>receipt_id</td><td class="mono">${escapeHtml(data.receipt_id)}</td></tr>
<tr><td>content_hash</td><td class="mono">${escapeHtml(data.content_hash || "n/a")}</td></tr>
<tr><td>sealed time (UTC)</td><td class="mono">${escapeHtml(data.time || "n/a")}</td></tr>
<tr><td>time source</td><td>${badgeHtml(data.time_source)}</td></tr>
<tr><td>reason</td><td>${escapeHtml(data.reason)}</td></tr>
</table>
<details class="raw"><summary>Raw /v1/verify JSON response</summary><pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre></details>
`;
verifyPanel.style.display = "block";
} catch (err) {
verifyStatus.className = "status-msg err";
verifyStatus.textContent = "Error: " + err.message;
} finally {
verifyBtn.disabled = false;
}
});
// --- Quickstart tabs (new, additive; does not touch anything above) ---
(function () {
const bar = document.getElementById("tabBar");
if (!bar) return;
bar.addEventListener("click", (e) => {
const btn = e.target.closest(".tab-btn");
if (!btn) return;
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
document.querySelectorAll(".tab-panel").forEach(p => p.classList.remove("active"));
btn.classList.add("active");
const panel = document.getElementById("tab-" + btn.dataset.tab);
if (panel) panel.classList.add("active");
});
})();
</script>
</body>
</html>