File size: 28,567 Bytes
6d42092
09a8425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4bf1e6b
09a8425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4bf1e6b
09a8425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4bf1e6b
09a8425
 
 
 
 
 
 
 
 
 
 
4bf1e6b
09a8425
 
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09a8425
 
 
 
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
f23a86f
 
 
 
4bf1e6b
 
 
 
f23a86f
 
 
4bf1e6b
 
f23a86f
4bf1e6b
f23a86f
 
4bf1e6b
 
525fc3e
 
 
 
 
 
 
 
 
 
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1253c7
4bf1e6b
 
c1253c7
 
 
f23a86f
 
 
 
 
4bf1e6b
c1253c7
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
 
f23a86f
4bf1e6b
bb95c84
f23a86f
bb95c84
 
 
f23a86f
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f23a86f
4bf1e6b
8735417
f23a86f
 
 
 
8735417
f23a86f
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
f23a86f
4bf1e6b
525fc3e
 
 
f23a86f
525fc3e
f23a86f
525fc3e
f23a86f
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f23a86f
4bf1e6b
 
 
f23a86f
 
bb95c84
f23a86f
 
4bf1e6b
f23a86f
 
 
4bf1e6b
f23a86f
4bf1e6b
 
f23a86f
4bf1e6b
 
 
 
e11b7f9
4bf1e6b
 
 
 
 
 
8735417
 
 
 
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
09a8425
 
 
4bf1e6b
09a8425
4bf1e6b
 
09a8425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4bf1e6b
f23a86f
4bf1e6b
 
 
 
09a8425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4bf1e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09a8425
 
6d42092
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
<!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>