-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathinfrastructure-audit.html
More file actions
370 lines (360 loc) · 34.5 KB
/
infrastructure-audit.html
File metadata and controls
370 lines (360 loc) · 34.5 KB
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Infrastructure — Architecture Audit</title>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,300;1,400;1,600&family=Urbanist:wght@300;400;500;600&family=JetBrains+Mono:wght@300;400&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0D0C0B; --surface: #161412; --surface-2: #1E1B18;
--border: rgba(244,239,233,0.07); --border-hover: rgba(244,239,233,0.14);
--cream: #F4EFE9; --cream-50: rgba(244,239,233,0.5); --cream-25: rgba(244,239,233,0.25); --cream-10: rgba(244,239,233,0.08);
--blue: #A8C4E0; --pink: #E8B4C8; --peach: #F0C8A8; --lavender: #C8B8E0;
--gold: #C4A46B; --green: #9AC4A8; --red: #E8A8A8; --amber: #E8D4A8;
--font-display: 'Cormorant Garamond', Georgia, serif;
--font-body: 'Urbanist', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--bg); color: var(--cream); font-family: var(--font-body); font-weight: 300; line-height: 1.6; min-height: 100vh; }
.container { max-width: 1280px; margin: 0 auto; padding: 0 56px; }
h1 { font-family: var(--font-display); font-style: italic; font-weight: 300; font-size: clamp(48px,5vw,72px); line-height: 1; letter-spacing: -0.02em; }
h2 { font-family: var(--font-display); font-style: italic; font-weight: 300; font-size: 38px; line-height: 1; }
h3 { font-family: var(--font-body); font-weight: 500; font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--cream-50); }
.label { font-family: var(--font-body); font-weight: 500; font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--cream-25); }
code { font-family: var(--font-mono); font-size: 0.82em; color: var(--blue); }
header { padding: 80px 0 64px; border-bottom: 1px solid var(--border); position: relative; overflow: hidden; }
header::before { content:''; position:absolute; top:-120px; right:-80px; width:600px; height:600px; background:radial-gradient(circle,rgba(168,196,224,0.04) 0%,transparent 65%); pointer-events:none; }
header::after { content:''; position:absolute; bottom:-1px; left:0; width:100px; height:1px; background:var(--gold); }
.header-stats { display:flex; gap:2px; margin-top:40px; flex-wrap:wrap; }
.stat-pill { display:flex; align-items:baseline; gap:8px; padding:10px 20px; border:1px solid var(--border); background:var(--surface); }
.stat-num { font-family:var(--font-display); font-size:28px; font-weight:300; line-height:1; }
.stat-label { font-size:10px; letter-spacing:0.1em; text-transform:uppercase; color:var(--cream-50); }
section { padding:64px 0; border-bottom:1px solid var(--border); }
.section-header { display:flex; align-items:baseline; gap:20px; margin-bottom:40px; }
.section-num { font-family:var(--font-display); font-size:13px; font-style:italic; color:var(--cream-25); min-width:24px; }
.tag { display:inline-block; padding:2px 8px; border-radius:2px; font-family:var(--font-body); font-size:9px; letter-spacing:0.08em; text-transform:uppercase; font-weight:600; vertical-align:middle; }
.tag-ok { background:rgba(154,196,168,0.12); color:var(--green); border:1px solid rgba(154,196,168,0.2); }
.tag-legacy { background:rgba(244,239,233,0.04); color:var(--cream-25); border:1px solid var(--border); }
.tag-broken { background:rgba(232,168,168,0.12); color:var(--red); border:1px solid rgba(232,168,168,0.2); }
.tag-warn { background:rgba(232,212,168,0.12); color:var(--amber); border:1px solid rgba(232,212,168,0.2); }
.tag-info { background:rgba(168,196,224,0.12); color:var(--blue); border:1px solid rgba(168,196,224,0.2); }
.block-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:1px; background:var(--border); border:1px solid var(--border); }
.block-card { background:var(--surface); padding:22px 20px 18px; display:flex; flex-direction:column; gap:0; transition:background 0.15s; position:relative; }
.block-card:hover { background:var(--surface-2); }
.block-card.warn { background:rgba(232,212,168,0.03); }
.type-badge { display:inline-flex; align-items:center; gap:6px; padding:3px 8px 3px 0; font-family:var(--font-mono); font-size:11px; margin-bottom:10px; color:var(--cream-50); }
.block-name { font-family:var(--font-display); font-style:italic; font-weight:300; font-size:20px; line-height:1; margin-bottom:10px; }
.block-desc { font-size:11.5px; color:var(--cream-50); line-height:1.55; flex:1; margin-bottom:14px; }
.block-renderer { font-family:var(--font-mono); font-size:10px; color:var(--cream-25); border-top:1px solid var(--border); padding-top:10px; margin-top:auto; }
.table-wrap { overflow-x:auto; border:1px solid var(--border); }
table { width:100%; border-collapse:collapse; font-size:12px; }
thead th { background:var(--surface-2); padding:10px 14px; text-align:left; font-family:var(--font-mono); font-size:9.5px; letter-spacing:0.08em; text-transform:uppercase; color:var(--cream-50); font-weight:400; border-bottom:1px solid var(--border); white-space:nowrap; }
tbody tr { border-bottom:1px solid var(--border); transition:background 0.1s; }
tbody tr:last-child { border-bottom:none; }
tbody tr:hover { background:var(--surface); }
tbody td { padding:10px 14px; color:var(--cream-50); vertical-align:middle; white-space:nowrap; }
tbody td:first-child { font-family:var(--font-mono); font-size:11px; color:var(--cream); font-weight:400; }
.cell-yes { text-align:center; color:var(--green); }
.cell-no { text-align:center; color:var(--red); opacity:0.6; }
.cell-partial { text-align:center; color:var(--amber); }
.cell-na { text-align:center; color:rgba(244,239,233,0.12); }
.cell-center { text-align:center; }
.wrap-cell { white-space:normal; }
.flow { display:flex; align-items:flex-start; gap:0; overflow-x:auto; padding-bottom:4px; }
.flow-node { flex-shrink:0; display:flex; flex-direction:column; align-items:center; gap:6px; }
.flow-box { padding:12px 18px; border:1px solid var(--border); background:var(--surface); text-align:center; min-width:120px; }
.flow-box .fb-label { font-family:var(--font-mono); font-size:9px; letter-spacing:0.08em; text-transform:uppercase; color:var(--cream-25); display:block; margin-bottom:4px; }
.flow-box .fb-name { font-size:12px; color:var(--cream); font-weight:500; line-height:1.3; }
.flow-arrow { display:flex; align-items:center; padding:0 6px; margin-top:18px; color:var(--cream-25); font-size:16px; flex-shrink:0; font-family:var(--font-mono); }
.issue-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.issue-card { padding:24px 24px 20px; border:1px solid var(--border); background:var(--surface); position:relative; overflow:hidden; }
.issue-card::before { content:''; position:absolute; top:0; left:0; width:3px; height:100%; }
.issue-card.critical::before { background:var(--red); }
.issue-card.warning::before { background:var(--amber); }
.issue-card.info::before { background:var(--blue); }
.issue-num { font-family:var(--font-display); font-size:42px; font-style:italic; color:var(--cream-10); position:absolute; top:12px; right:18px; line-height:1; }
.issue-title { font-weight:500; font-size:14px; margin-bottom:8px; padding-right:48px; }
.issue-desc { font-size:12px; color:var(--cream-50); line-height:1.65; margin-bottom:12px; }
.issue-fix { font-size:10.5px; color:var(--blue); font-family:var(--font-mono); line-height:1.6; }
.kv-grid { display:grid; grid-template-columns:auto 1fr; gap:1px; background:var(--border); border:1px solid var(--border); font-size:12px; overflow:hidden; }
.kv-grid .k { background:var(--surface-2); padding:8px 12px; font-family:var(--font-mono); font-size:10px; color:var(--cream-25); white-space:nowrap; }
.kv-grid .v { background:var(--surface); padding:8px 12px; color:var(--cream-50); }
.callout { background:rgba(196,164,107,0.05); border:1px solid rgba(196,164,107,0.14); padding:16px 20px; font-size:13px; color:var(--cream-50); line-height:1.7; margin-top:24px; }
.callout strong { color:var(--gold); font-weight:500; }
.callout.blue { background:rgba(168,196,224,0.05); border-color:rgba(168,196,224,0.14); }
.callout.blue strong { color:var(--blue); }
.callout.green { background:rgba(154,196,168,0.05); border-color:rgba(154,196,168,0.18); }
.callout.green strong { color:var(--green); }
.legend { display:flex; gap:20px; flex-wrap:wrap; padding:12px 16px; border:1px solid var(--border); background:var(--surface); margin-bottom:20px; }
.legend-item { display:flex; align-items:center; gap:7px; font-size:11px; color:var(--cream-50); }
.legend-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
footer { padding:48px 0; }
.mt-16{margin-top:16px} .mt-24{margin-top:24px} .mt-32{margin-top:32px} .mb-16{margin-bottom:16px} .mb-24{margin-bottom:24px}
.text-gold{color:var(--gold)} .text-blue{color:var(--blue)} .text-green{color:var(--green)} .text-red{color:var(--red)} .text-amber{color:var(--amber)} .text-muted{color:var(--cream-25)} .text-dim{color:var(--cream-50)}
.mono{font-family:var(--font-mono)}
</style>
</head>
<body>
<header>
<div class="container">
<p class="label" style="color:var(--gold);margin-bottom:20px;letter-spacing:0.18em;">Secondlayer — Infrastructure Architecture Audit</p>
<h1>Streams · Index<br>Subgraphs · Subscriptions</h1>
<p style="margin-top:18px;font-size:14px;color:var(--cream-50);max-width:620px;line-height:1.75;">
The four product surfaces after the subgraph source re-point. The Kourier "tooling builds on the public microservices" model is now realized in production: subgraphs read the Streams clock + Index data over HTTP instead of tapping the indexer Postgres directly.
</p>
<div class="header-stats">
<div class="stat-pill"><span class="stat-num" style="color:var(--cream);">4</span><span class="stat-label">Product Surfaces</span></div>
<div class="stat-pill"><span class="stat-num text-blue">20</span><span class="stat-label">HTTP Endpoints</span></div>
<div class="stat-pill"><span class="stat-num text-green">0</span><span class="stat-label">Golden-diff / 1534</span></div>
<div class="stat-pill"><span class="stat-num text-green">LIVE</span><span class="stat-label">streams-index in prod</span></div>
<div class="stat-pill"><span class="stat-num text-amber">4</span><span class="stat-label">Tracked Gaps</span></div>
</div>
</div>
</header>
<!-- §1 -->
<section>
<div class="container">
<div class="section-header"><span class="section-num">§1</span><h2>The Four Surfaces</h2></div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> Live / GA</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--blue)"></div> Microservice / data plane</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--lavender)"></div> Tooling layer</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--amber)"></div> Has deferred surface</div>
</div>
<div class="block-grid">
<div class="block-card">
<div class="type-badge"><span style="color:var(--blue)">ms-1 · streams</span> <span class="tag tag-ok">live</span></div>
<div class="block-name">Streams</div>
<div class="block-desc">Canonical raw event firehose. 11 Stacks event types, <code><height>:<index></code> cursors, reorg envelope, finality markers. Bearer-only, tier-gated retention. The canonical <strong style="color:var(--cream)">clock</strong> for tooling.</div>
<div class="block-renderer">→ <span style="color:var(--blue)">packages/api/src/routes/streams.ts</span></div>
</div>
<div class="block-card">
<div class="type-badge"><span style="color:var(--blue)">ms-2 · index</span> <span class="tag tag-ok">live</span></div>
<div class="block-name">Index</div>
<div class="block-desc">Decoded data layer. events / contract-calls / transactions / blocks / canonical / stacking / mempool. Anon reads (free hard-blocked), full-history (no retention gate). The <strong style="color:var(--cream)">data plane</strong> for tooling.</div>
<div class="block-renderer">→ <span style="color:var(--blue)">packages/api/src/routes/index.ts</span></div>
</div>
<div class="block-card">
<div class="type-badge"><span style="color:var(--lavender)">L3 · subgraphs</span> <span class="tag tag-ok">re-pointed</span></div>
<div class="block-name">Subgraphs</div>
<div class="block-desc">App-specific indexers (The-Graph-style, REST + typed ORM). <strong style="color:var(--cream)">Now consume Streams + Index</strong> via the <code>BlockSource</code> seam instead of the indexer DB. Auto-served REST, Prisma/Drizzle codegen, BYO DB.</div>
<div class="block-renderer">→ <span style="color:var(--lavender)">packages/subgraphs/src/runtime</span></div>
</div>
<div class="block-card warn">
<div class="type-badge"><span style="color:var(--lavender)">L3 · delivery</span> <span class="tag tag-warn">partial</span></div>
<div class="block-name">Subscriptions</div>
<div class="block-desc">Webhook delivery ("lambdas for Stacks"). Outbox → emitter → 6 formats, HMAC, retries/circuit/SSRF. Sits on subgraph rows. Direct chain-level subs (<code>#8</code>) <strong style="color:var(--amber)">deferred</strong>.</div>
<div class="block-renderer">→ <span style="color:var(--lavender)">runtime/emitter.ts · outbox-emit.ts</span></div>
</div>
</div>
<div class="callout green mt-24">
<strong>What changed this session:</strong> Subgraphs were re-pointed (#7) off the indexer Postgres tap onto the public Streams clock + Index data — the layered Kourier vision realized. <code>SUBGRAPH_SOURCE=streams-index</code> is live in prod across all 3 deployed benches; output proven byte-identical (golden-diff 0 diffs / 1,534 payloads / 101 real blocks). Default remains <code>db</code> per subgraph; the path is gated by eligibility + instant-rollback.
</div>
</div>
</section>
<!-- §2 -->
<section>
<div class="container">
<div class="section-header"><span class="section-num">§2</span><h2>The Layered Architecture</h2></div>
<p style="font-size:13px;color:var(--cream-50);max-width:680px;margin-bottom:28px;line-height:1.7;">Each layer consumes only the layer below it over HTTP. Reorg authority flows down from Streams; decoding happens once at Index; tooling never re-taps the chain DB.</p>
<div class="flow">
<div class="flow-node"><div class="flow-box" style="border-color:var(--cream-25)"><span class="fb-label">L0</span><span class="fb-name">Stacks node</span></div><span style="font-size:10px;color:var(--cream-25);font-family:var(--font-mono);margin-top:4px;">observer</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box"><span class="fb-label">indexer</span><span class="fb-name">persistBlock</span></div><span style="font-size:10px;color:var(--cream-25);font-family:var(--font-mono);margin-top:4px;">replace-per-height</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box" style="border-color:var(--blue)"><span class="fb-label">ms-1</span><span class="fb-name">Streams /events</span></div><span style="font-size:10px;color:var(--blue);font-family:var(--font-mono);margin-top:4px;">raw · clock</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box" style="border-color:var(--blue)"><span class="fb-label">ms-2</span><span class="fb-name">Index</span></div><span style="font-size:10px;color:var(--blue);font-family:var(--font-mono);margin-top:4px;">L2 decoder → decoded_events</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box" style="border-color:var(--lavender)"><span class="fb-label">L3 tooling</span><span class="fb-name">Subgraphs</span></div><span style="font-size:10px;color:var(--lavender);font-family:var(--font-mono);margin-top:4px;">clock+data → BlockData</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box" style="border-color:var(--lavender)"><span class="fb-label">L3 delivery</span><span class="fb-name">Subscriptions</span></div><span style="font-size:10px;color:var(--lavender);font-family:var(--font-mono);margin-top:4px;">outbox → webhook</span></div>
</div>
<div class="callout blue mt-32">
<strong>Reorg authority:</strong> Streams owns reorg detection (<code>/reorgs</code>, <code>fork_point_height</code>). Index <code>/contract-calls</code> + <code>/transactions</code> intentionally return <code>reorgs: []</code> — consumers get reorg signal from Streams. The subgraph runtime polls <code>streams.reorgs.list</code> in streams-index mode and rewinds via the existing idempotent <code>handleSubgraphReorg</code>.
</div>
</div>
</section>
<!-- §3 -->
<section>
<div class="container">
<div class="section-header"><span class="section-num">§3</span><h2>The Subgraph Re-point — BlockSource Seam</h2></div>
<div class="two-col" style="display:grid;grid-template-columns:1fr 1fr;gap:40px;align-items:start;">
<div>
<h3 style="margin-bottom:14px;">Before — indexer Postgres tap</h3>
<div class="flow" style="flex-direction:column;align-items:flex-start;gap:8px;">
<div class="flow-box" style="min-width:200px;border-color:var(--cream-25)"><span class="fb-label">getSourceDb()</span><span class="fb-name">blocks · transactions · events</span></div>
<div class="flow-arrow" style="margin:0 0 0 30px;transform:rotate(90deg)">→</div>
<div class="flow-box" style="min-width:200px"><span class="fb-label">runtime</span><span class="fb-name">matchSources → handlers → flush</span></div>
</div>
<p style="font-size:11.5px;color:var(--cream-25);margin-top:12px;font-family:var(--font-mono);">privileged DB read · LISTEN new_block/reorg</p>
</div>
<div>
<h3 style="margin-bottom:14px;color:var(--green);">After — public microservices</h3>
<div class="flow" style="flex-direction:column;align-items:flex-start;gap:8px;">
<div class="flow-box" style="min-width:200px;border-color:var(--blue)"><span class="fb-label">PublicApiBlockSource</span><span class="fb-name">Streams tip+reorgs · Index blocks/events/txs</span></div>
<div class="flow-arrow" style="margin:0 0 0 30px;transform:rotate(90deg)">→</div>
<div class="flow-box" style="min-width:200px;border-color:var(--lavender)"><span class="fb-label">reconstruct.ts</span><span class="fb-name">flat Index rows → raw BlockData</span></div>
<div class="flow-arrow" style="margin:0 0 0 30px;transform:rotate(90deg)">→</div>
<div class="flow-box" style="min-width:200px"><span class="fb-label">runtime (unchanged)</span><span class="fb-name">matchSources → handlers → flush</span></div>
</div>
<p style="font-size:11.5px;color:var(--green);margin-top:12px;font-family:var(--font-mono);">HTTP · internal enterprise key · golden-diff verified</p>
</div>
</div>
<div class="kv-grid mt-32">
<div class="k">seam</div><div class="v"><code>BlockSource</code> — <code>getTip()</code> · <code>loadBlockRange()</code> · resolved per-subgraph by <code>resolveBlockSource()</code> on <code>SUBGRAPH_SOURCE</code> + eligibility</div>
<div class="k">unchanged</div><div class="v"><code>matchSources</code> · handler runner · <code>flush</code> · subscription outbox · trait resolution — only the loader/tip/reorg-signal swap</div>
<div class="k">eligibility</div><div class="v">all sources event-type / contract_call / contract_deploy / trait · Record-style (no array/<code>*</code>-handler). Ineligible → transparent DB tap.</div>
<div class="k">transport</div><div class="v"><code>@secondlayer/shared/index-http</code> — leaf package (SDK depends on subgraphs, so no cycle). One home for the wire format.</div>
<div class="k">tip / alignment</div><div class="v"><code>getTip()</code> returns the <strong style="color:var(--cream)">Index</strong> tip (data-availability bound) — catch-up never processes past what Index can serve.</div>
<div class="k">reorg</div><div class="v">Streams <code>/reorgs</code> poll → <code>handleSubgraphReorg(fork_point_height)</code> (idempotent, runs alongside the PG LISTEN).</div>
<div class="k">auth</div><div class="v">internal enterprise seed key, <strong style="color:var(--cream)">no account_id</strong> → unmetered. Defaults (<code>sk-sl_index_internal</code>, <code>api:3800</code>) match deployed seeds.</div>
</div>
<div class="callout mt-24">
<strong>The crux discovery:</strong> raw events carry the stacks-node's verbose serde-tagged Clarity JSON (<code>{UInt:223}</code>, <code>{Tuple:{Sequence:{String:{ASCII…}}}}</code>) that Index only stores as <strong style="color:var(--cream)">hex</strong>. The golden-diff caught every leak — nft <code>tokenId</code>, print <code>value</code>, the contract_call event spread, even empty <code>memo</code>. All now decode from the canonical hex (<code>raw_value</code> / <code>function_args_hex</code>), making the two sources byte-identical <em>and</em> cleaner for handler authors.
</div>
</div>
</section>
<!-- §4 -->
<section>
<div class="container">
<div class="section-header"><span class="section-num">§4</span><h2>Endpoint Inventory</h2></div>
<h3 style="margin-bottom:14px;color:var(--blue);">Streams — ms-1 (bearer required, tier-gated)</h3>
<div class="table-wrap mb-24">
<table>
<thead><tr><th>Endpoint</th><th>Purpose</th><th>Cursor</th><th>Reorgs</th><th>Cache</th></tr></thead>
<tbody>
<tr><td>GET /events</td><td class="wrap-cell">Raw event firehose, 11 types, server-side filters</td><td>height:index</td><td class="cell-yes">✓</td><td class="cell-partial" data-tip="Immutable finalized pages: 1yr; ETag/304">finalized</td></tr>
<tr><td>GET /events/:tx_id</td><td class="wrap-cell">All events for one tx</td><td class="cell-na">·</td><td class="cell-yes">✓</td><td class="cell-partial">finalized</td></tr>
<tr><td>GET /blocks/:ref/events</td><td class="wrap-cell">Events for one block (height or hash)</td><td class="cell-na">·</td><td class="cell-yes">✓</td><td class="cell-partial">finalized</td></tr>
<tr><td>GET /canonical/:height</td><td class="wrap-cell">Single canonical block-hash (self-verify)</td><td class="cell-na">·</td><td class="cell-na">·</td><td class="cell-partial">finalized</td></tr>
<tr><td>GET /reorgs</td><td class="wrap-cell">Reorg history (<code>since</code> token)</td><td>since</td><td class="cell-yes">✓</td><td class="cell-no">no</td></tr>
<tr><td>GET /tip</td><td class="wrap-cell">Canonical tip + finalized_height + lag</td><td class="cell-na">·</td><td class="cell-na">·</td><td class="cell-partial" data-tip="private, 1s">1s</td></tr>
<tr><td>GET /</td><td class="wrap-cell">Discovery doc (no auth)</td><td class="cell-na">·</td><td class="cell-na">·</td><td class="cell-no">no</td></tr>
</tbody>
</table>
</div>
<h3 style="margin-bottom:14px;color:var(--blue);">Index — ms-2 (anon reads, free hard-blocked, internal key unmetered)</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Endpoint</th><th>Purpose</th><th>Cursor</th><th>Reorgs</th><th>Metered</th></tr></thead>
<tbody>
<tr><td>GET /events</td><td class="wrap-cell">Decoded events, 11 types (event_type required)</td><td>height:event_idx</td><td class="cell-yes">✓</td><td class="cell-yes">✓</td></tr>
<tr><td>GET /ft-transfers · /nft-transfers</td><td class="wrap-cell">Typed aliases over /events</td><td>height:event_idx</td><td class="cell-yes">✓</td><td class="cell-yes">✓</td></tr>
<tr><td>GET /contract-calls</td><td class="wrap-cell">Decoded calls + <code>function_args_hex</code> <span class="tag tag-info">new</span></td><td>height:tx_idx</td><td class="cell-no" data-tip="Streams owns reorg">stub []</td><td class="cell-yes">✓</td></tr>
<tr><td>GET /transactions · /:tx_id</td><td class="wrap-cell">Full tx docs (fee, nonce, post-conditions, hex args)</td><td>height:tx_idx</td><td class="cell-no" data-tip="Streams owns reorg">stub []</td><td class="cell-yes">✓</td></tr>
<tr><td>GET /blocks · /:ref</td><td class="wrap-cell">Canonical block metadata (subgraph enumeration)</td><td>height:0</td><td class="cell-na">·</td><td class="cell-partial" data-tip="reference data">ref</td></tr>
<tr><td>GET /canonical</td><td class="wrap-cell">Block-hash map over a range</td><td>height:0</td><td class="cell-na">·</td><td class="cell-no">no</td></tr>
<tr><td>GET /stacking</td><td class="wrap-cell">Decoded PoX-4 actions (gated decoder)</td><td>height:tx_idx</td><td class="cell-na">·</td><td class="cell-yes">✓</td></tr>
<tr><td>GET /mempool · /:tx_id</td><td class="wrap-cell">Pending txs (single-node, never cached)</td><td>seq int</td><td class="cell-na">·</td><td class="cell-no">no</td></tr>
</tbody>
</table>
</div>
<p style="font-size:11px;color:var(--cream-25);margin-top:12px;font-family:var(--font-mono);">Subgraphs auto-serve REST per table (apps/web/.../api/subgraphs/[name]/[table]) · Subscriptions expose ~12 CRUD/ops routes.</p>
</div>
</section>
<!-- §5 -->
<section>
<div class="container">
<div class="section-header"><span class="section-num">§5</span><h2>Auth, Consumers & Delivery Matrix</h2></div>
<div class="table-wrap mb-24">
<table>
<thead><tr><th>Surface</th><th>Anon read</th><th>Internal key</th><th>Retention gate</th><th>Consumed by</th></tr></thead>
<tbody>
<tr><td>Streams</td><td class="cell-no">✗ bearer</td><td class="cell-yes" data-tip="sk-sl_streams_l2_internal, enterprise">✓</td><td class="cell-yes" data-tip="7/30/90d by tier; enterprise unlimited">tier</td><td class="wrap-cell">L2 decoder · subgraph clock (tip+reorgs)</td></tr>
<tr><td>Index</td><td class="cell-yes" data-tip="open beta; free tier hard-blocked">✓</td><td class="cell-yes" data-tip="sk-sl_index_internal, no account_id = unmetered">✓</td><td class="cell-no" data-tip="full history queryable">none</td><td class="wrap-cell">subgraph data plane · public API</td></tr>
<tr><td>Subgraphs</td><td class="cell-yes" data-tip="open beta read auth">✓</td><td class="cell-na">·</td><td class="cell-no">none</td><td class="wrap-cell">apps · subscriptions (row source)</td></tr>
<tr><td>Subscriptions</td><td class="cell-no" data-tip="account-scoped writes">✗ keyed</td><td class="cell-na">·</td><td class="cell-na">·</td><td class="wrap-cell">Discord/Slack/Trigger.dev/Inngest/CF/own HTTP</td></tr>
</tbody>
</table>
</div>
<h3 style="margin-bottom:14px;">Subscription delivery pipeline</h3>
<div class="flow">
<div class="flow-node"><div class="flow-box"><span class="fb-label">producer</span><span class="fb-name">emitSubscriptionOutbox</span></div><span style="font-size:10px;color:var(--cream-25);font-family:var(--font-mono);margin-top:4px;">in processBlock flush tx</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box"><span class="fb-label">ledger</span><span class="fb-name">subscription_outbox</span></div><span style="font-size:10px;color:var(--cream-25);font-family:var(--font-mono);margin-top:4px;">SHA-256 dedup · NOTIFY</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box"><span class="fb-label">emitter</span><span class="fb-name">claim & drain</span></div><span style="font-size:10px;color:var(--cream-25);font-family:var(--font-mono);margin-top:4px;">SKIP LOCKED · concurrency 4</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box"><span class="fb-label">6 formats</span><span class="fb-name">HMAC dispatch</span></div><span style="font-size:10px;color:var(--cream-25);font-family:var(--font-mono);margin-top:4px;">SSRF guard · backoff 30s→72h</span></div>
<div class="flow-arrow">→</div>
<div class="flow-node"><div class="flow-box" style="border-color:var(--green)"><span class="fb-label">webhook</span><span class="fb-name">your endpoint</span></div><span style="font-size:10px;color:var(--green);font-family:var(--font-mono);margin-top:4px;">≥20 fail → paused</span></div>
</div>
<div class="callout mt-24">
<strong>The coupling:</strong> the <em>only</em> outbox producer is <code>emitSubscriptionOutbox</code>, called from inside a subgraph's <code>processBlock</code> flush. So a subscription targets a <code>{subgraphName, tableName}</code> — <strong style="color:var(--cream)">you must deploy a subgraph first.</strong> The delivery stack (emitter, retries, circuit breaker, SSRF, 6 formats, dedup) is otherwise subgraph-agnostic.
</div>
</div>
</section>
<!-- §6 -->
<section>
<div class="container">
<div class="section-header"><span class="section-num">§6</span><h2>Risk Map & Tracked Gaps</h2></div>
<div class="issue-grid">
<div class="issue-card warning">
<span class="issue-num">1</span>
<div class="issue-title">SDK duplicates the Index/Streams wire types</div>
<div class="issue-desc">The public SDK hand-maintains <code>IndexEvent</code>/<code>IndexTransaction</code> shapes that now also live in <code>@secondlayer/shared/index-http</code>. Two copies can drift.</div>
<div class="issue-fix">→ FT.1 (tracked): converge SDK onto the shared transport types; deferred (touches SDK public types) until the shapes drift.</div>
</div>
<div class="issue-card warning">
<span class="issue-num">2</span>
<div class="issue-title">Streams rate-limit is process-local</div>
<div class="issue-desc"><code>streams/rate-limit.ts</code> uses an in-process sliding window — correct only for a single API instance. TODO notes Redis before scaling out.</div>
<div class="issue-fix">→ move to Redis-backed limiting before running multiple API replicas.</div>
</div>
<div class="issue-card info">
<span class="issue-num">3</span>
<div class="issue-title">Index reorgs stubbed on tx-level endpoints</div>
<div class="issue-desc"><code>/contract-calls</code> + <code>/transactions</code> return <code>reorgs: []</code> by design — Streams is the reorg authority. Not a bug; documented in-code.</div>
<div class="issue-fix">→ by design: consumers (incl. the subgraph runtime) take reorg signal from Streams.</div>
</div>
<div class="issue-card info">
<span class="issue-num">4</span>
<div class="issue-title">Direct chain-level subscriptions (#8) deferred</div>
<div class="issue-desc">The <code>triggers/</code> stub (<code>EventTrigger{filter}</code>, 13 typed <code>on.*</code> constructors) is inert — not wired to the emitter or API. Subscriptions still require a subgraph.</div>
<div class="issue-fix">→ deferred in favor of #7 (the re-point). Delivery stack is already subgraph-agnostic; a second outbox producer would unlock it.</div>
</div>
<div class="issue-card info">
<span class="issue-num">5</span>
<div class="issue-title">Value normalization is a behavior change</div>
<div class="issue-desc">nft <code>tokenId</code> / print <code>value</code> / contract_call spread now decode from hex (clean) instead of the node serde form. Changes handler output for those types.</div>
<div class="issue-fix">→ ~0 blast radius (only 3 internal benches deployed, none nft/print external); reindex affected subgraphs. Changeset documents it.</div>
</div>
<div class="issue-card info">
<span class="issue-num">6</span>
<div class="issue-title">Index mempool is a single-node, go-forward view</div>
<div class="issue-desc">Built from one node's observer; no pre-observer backlog replay. Fine for tx-lifecycle/optimistic-UX, not global mempool/MEV aggregation.</div>
<div class="issue-fix">→ by design (documented). No standard bulk-mempool RPC exists to aggregate.</div>
</div>
</div>
<div class="callout blue mt-24">
<strong>No critical issues.</strong> Every gap is either tracked tech-debt (1–2) or an intentional, documented design choice (3–6). The re-point shipped with full golden-diff parity and a live, instantly-reversible prod canary.
</div>
</div>
</section>
<!-- §7 -->
<section>
<div class="container">
<div class="section-header"><span class="section-num">§7</span><h2>Decision Log</h2></div>
<div class="kv-grid">
<div class="k">tooling on services</div><div class="v">Subgraphs consume Streams (clock) + Index (data) over HTTP — <span class="tag tag-ok">live</span> the Kourier "build on the public microservices" model, realized.</div>
<div class="k">Streams = reorg authority</div><div class="v">Reorg detection lives at Streams; Index tx-level endpoints stub <code>reorgs:[]</code>; subgraph runtime polls <code>/reorgs</code>.</div>
<div class="k">Index = data, registry = platform</div><div class="v">Trait resolution reads the contract registry on <code>targetDb</code> (platform DB the processor always holds) → source-independent, no HTTP trait endpoint needed.</div>
<div class="k">decode from canonical hex</div><div class="v">nft/print/contract_call values decode from <code>raw_value</code>/<code>function_args_hex</code>, not the node's serde-tagged JSON (unreproducible from Index).</div>
<div class="k">transport in shared leaf</div><div class="v"><code>@secondlayer/shared/index-http</code> — both SDK and subgraphs depend on shared; avoids the sdk→subgraphs cycle.</div>
<div class="k">self-host scope</div><div class="v">Fully-self-hosted only (own api + indexer + registry + processor). No public-API-consumer self-host.</div>
<div class="k">GraphQL</div><div class="v">Deliberate NO — REST + typed ORM codegen is the answer across all surfaces.</div>
<div class="k">#8 direct subscriptions</div><div class="v">Deferred (separate effort). Subscriptions sit on subgraph rows (L3) / decoded data — not raw L1 (Chainhooks' lane).</div>
<div class="k">rollout posture</div><div class="v">Per-subgraph eligibility gate; default <code>db</code>; <code>SUBGRAPH_SOURCE=streams-index</code> committed to prod compose (survives <code>git reset --hard</code> deploys). Unset = instant rollback.</div>
</div>
</div>
</section>
<footer>
<div class="container">
<p class="label">Generated June 3, 2026 · Secondlayer · Infrastructure architecture (Streams · Index · Subgraphs · Subscriptions)</p>
</div>
</footer>
</body>
</html>