Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
e0971cc
P0.1 — app skeleton & seeded config
Jun 6, 2026
56ffe75
Merge pull request #1 from markosiks/feat/p0.1-skeleton
loficoded Jun 6, 2026
10439ec
P0.2 — Neon data model & migrations (§7)
loficoded Jun 6, 2026
b03626a
Merge pull request #3 from markosiks/feat/p0.2-data-model
loficoded Jun 6, 2026
692ca5d
P0.3 — Intent contract: schema, signing, ordered validation (§8)
loficoded Jun 6, 2026
11ad647
fix(intent): reject exponent-driven decimal expansion (pre-auth DoS)
loficoded Jun 6, 2026
ed9c016
P0.3 — harden Intent boundary: deterministic ttl, float-free bounds, …
loficoded Jun 6, 2026
dbd2efd
P0.2 — close real data-integrity & migration-safety gaps from the sec…
loficoded Jun 6, 2026
ec01875
P0.1 — bound the health probe so a slow DB can't exhaust the pool
loficoded Jun 6, 2026
df2439e
Merge feat/p0.2-data-model: security audit fixes for data integrity &…
loficoded Jun 6, 2026
946d162
Merge feat/p0.3-intent-contract: Intent contract with schema, signing…
loficoded Jun 6, 2026
626cb69
Merge fix/p0.1-health-probe: bound health probe to prevent pool exhau…
loficoded Jun 6, 2026
3044ed0
P1.1 — Referee / Firewall: ordered bounded-execution gate
loficoded Jun 6, 2026
c75ab77
fix(test): make /api/health route test hermetic against pool singleton
loficoded Jun 6, 2026
6508edc
referee: fix clip-ordering firewall bypass (terminal decisions must d…
loficoded Jun 6, 2026
ea06ca5
Merge fix/health-probe-test-isolation: hermetic /api/health route test
loficoded Jun 6, 2026
71b4876
Merge feat/p1.1-referee: Referee / Firewall bounded-execution gate + …
loficoded Jun 6, 2026
ca793d3
harden P1.x: durable nonce anti-replay, db pool/probe fixes, intent s…
loficoded Jun 6, 2026
cbb7cd3
Merge feat/p1.x-hardening: durable nonce anti-replay + db pool/probe …
loficoded Jun 6, 2026
bde4d2d
feat(scoring): P1.2 scoring engine (AgentScore in [0,100])
loficoded Jun 6, 2026
e1e7479
fix(scoring): P1.2 security audit hardening
loficoded Jun 6, 2026
83b0f28
docs(scoring): clarify s_min boundary is already consistent (gate < c…
loficoded Jun 6, 2026
6ab3134
Merge pull request #8 from markosiks/feat/p1.2-scoring
loficoded Jun 6, 2026
1ffdaae
feat(router): P1.3 reputation-weighted capital router
loficoded Jun 7, 2026
28f903f
Merge feat/p1.3-capital-router: reputation-weighted capital router (§…
loficoded Jun 7, 2026
3a61fbc
fix(router): durable one-row-per-(agent,round) for capital_allocations
loficoded Jun 7, 2026
e8a81a1
Merge fix/p1.3-router-persistence: UNIQUE(agent_id,round_id) + idempo…
loficoded Jun 7, 2026
ceb54a5
fix(router): fail closed on malformed fixed-point / prev-state input
loficoded Jun 7, 2026
b136777
Merge fix/p1.3-router-input-validation: fail-closed parseUnits/toUnit…
loficoded Jun 7, 2026
0de7b93
feat(replay): P1.4 deterministic demo spine
loficoded Jun 7, 2026
7e95ac0
Merge feat/p1.4-demo-spine: P1.4 deterministic demo spine (replay)
loficoded Jun 7, 2026
cf45a7a
fix(replay): enforce single-connection settle contract in runArc
loficoded Jun 7, 2026
421c19f
Merge fix/p1.4-replay-client-guard: enforce single-connection settle …
loficoded Jun 7, 2026
3c26993
fix(db): gate full-teardown scripts behind VECTOR_ALLOW_DESTRUCTIVE
loficoded Jun 7, 2026
54aad6a
Merge fix/p1.4-destructive-op-guard: opt-in gate for full-teardown db…
loficoded Jun 7, 2026
cf01b09
fix(db): bound the shared pool connect queue with connectionTimeoutMi…
loficoded Jun 7, 2026
059709e
Merge fix/p1.4-health-connect-timeout: bound /api/health connect queue
loficoded Jun 7, 2026
01a9929
feat(api): P1.5 read API (SWR-pollable leaderboard, agent detail, feeds)
loficoded Jun 7, 2026
3f59945
test: make single-process `bun test` green by removing the global dri…
loficoded Jun 7, 2026
0244b09
perf(db): index P1.5 read-API feed queries (0005)
loficoded Jun 7, 2026
61a7b1b
Merge feat/p1.5-read-api-core: SWR-pollable read API (leaderboard, ag…
loficoded Jun 7, 2026
4e13895
Merge test/p1.5-single-process-bun-test: DI pool seam, drop process-w…
loficoded Jun 7, 2026
4daf4d3
Merge perf/p1.5-read-api-feed-indexes: index agent/attestation feed q…
loficoded Jun 7, 2026
ce6d5e4
feat(arena): P1.6 arena/leaderboard screen
loficoded Jun 7, 2026
8b1f675
test(arena): real live e2e — pipeline + Postgres, gated arc start
loficoded Jun 7, 2026
95810f1
Merge feat/p1.6-arena-leaderboard: P1.6 Arena/Leaderboard screen
loficoded Jun 7, 2026
d9bda30
Merge test/p1.6-arena-live-e2e: real live e2e for /arena
loficoded Jun 7, 2026
48ef78d
feat(chain): P1.7 ERC-8004 Reputation Registry on Mantle Sepolia
loficoded Jun 8, 2026
bb86efa
docs(chain): note giveFeedback self-feedback ban + two-key (owner/att…
loficoded Jun 8, 2026
bd31185
fix(chain): resolve P1.7 ERC-8004 blockers — registration-backed agen…
loficoded Jun 8, 2026
6d0222c
fix(api): mint keyset cursor at microsecond precision to stop silent …
loficoded Jun 8, 2026
dbe36de
fix(intent): reject unstorable numeric magnitude at the gate instead …
loficoded Jun 8, 2026
2d0d292
fix(config): validate cross-field invariants crash_cap < s_min and sc…
loficoded Jun 8, 2026
fc710fe
Merge pull request #15 from markosiks/feat/p1.7-erc8004-registry
loficoded Jun 8, 2026
0680ffc
Merge pull request #16 from markosiks/fix/p1.7-blockers
loficoded Jun 8, 2026
754bbf2
Merge pull request #17 from markosiks/fix/api-cursor-precision
loficoded Jun 8, 2026
c309b79
Merge pull request #18 from markosiks/fix/intent-size-magnitude-bound
loficoded Jun 8, 2026
618a92c
Merge pull request #19 from markosiks/fix/config-invariant-validation
loficoded Jun 8, 2026
2ed5a00
fix(chain): fail closed on untrusted registry RPC returns
loficoded Jun 8, 2026
0a82f33
fix(repo): untrack accidentally-committed node_modules symlink
loficoded Jun 8, 2026
e9468bb
Merge pull request #20 from markosiks/fix/chain-registry-failclosed
loficoded Jun 8, 2026
d80accd
Merge pull request #21 from markosiks/fix/untrack-node-modules
loficoded Jun 8, 2026
3137119
fix(replay): wire operator kill switch into the live pipeline
loficoded Jun 8, 2026
dc2bc20
test(replay): regression-guard the kill-switch wiring (integration)
loficoded Jun 8, 2026
adf6255
Merge pull request #22 from markosiks/fix/wire-kill-switch
loficoded Jun 8, 2026
06d8dfa
feat(attestation): P1.8 ERC-8004 attestation pipeline
loficoded Jun 8, 2026
bfdf729
harden(attestation): pin feedbackURI response with nosniff + cover th…
loficoded Jun 8, 2026
c0d3d27
fix(env): restrict PUBLIC_BASE_URL to http(s), matching its contract
loficoded Jun 8, 2026
56c7536
Merge: P1.8 ERC-8004 attestation pipeline (core)
loficoded Jun 8, 2026
4efebb6
Merge: harden the feedbackURI endpoint (security-audit follow-up)
loficoded Jun 8, 2026
ec39ab7
Merge: restrict PUBLIC_BASE_URL to http(s) (subagent re-audit follow-up)
loficoded Jun 8, 2026
a4b9af1
feat(rail): Byreal Perps CLI rail adapter (P2.1)
loficoded Jun 8, 2026
238eff3
harden(byreal): fix SIGKILL escalation, fee constraint, status honest…
loficoded Jun 8, 2026
2b6a750
test(integration,e2e): fix 3 stale tests surfaced by running against …
loficoded Jun 8, 2026
72c016d
docs(byreal): note the determinism boundary is now verified against r…
loficoded Jun 8, 2026
ec6eab2
test(attestation): live ERC-8004 write-path e2e against Mantle Sepolia
loficoded Jun 8, 2026
6373da0
feat(rail): Byreal Perps CLI rail adapter (P2.1)
loficoded Jun 8, 2026
b0ad9e2
harden(byreal): fix SIGKILL escalation, fee constraint, status honest…
loficoded Jun 8, 2026
ee9a6b4
test(integration): align byreal determinism-boundary test with the sc…
loficoded Jun 8, 2026
47ab174
docs(byreal): note the determinism boundary is now verified against r…
loficoded Jun 8, 2026
d84874a
test(attestation): live ERC-8004 write-path e2e against Mantle Sepolia
loficoded Jun 8, 2026
f6265fa
test(e2e): fix two stale tests surfaced by running against a real DB
loficoded Jun 8, 2026
a83e497
harden(attestation): int128 bound + wire the dead self-feedback guard…
loficoded Jun 8, 2026
bdeebe7
Merge: Byreal Perps CLI rail adapter (P2.1)
loficoded Jun 8, 2026
cca0b71
Merge: fix two stale e2e tests surfaced by a real DB
loficoded Jun 8, 2026
5e4b729
Merge: ERC-8004 attestation on-chain write path (P1.8) — live e2e + h…
loficoded Jun 8, 2026
5da8629
Merge pull request #24 from markosiks/feat/p2.1-byreal-rail
loficoded Jun 8, 2026
7f56c8a
feat(signals): P2.2 Nansen smart-money signal (read-only, fail-open)
loficoded Jun 8, 2026
840f9fd
test(signals): compare Nansen arc end-state allocations order-indepen…
loficoded Jun 8, 2026
5d82591
harden(signals): fix real findings from P2.2 Nansen security audit
loficoded Jun 8, 2026
6301e95
Merge pull request #25 from markosiks/feat/p2.2-nansen-signal
loficoded Jun 8, 2026
e352048
Merge pull request #26 from markosiks/harden/p2.2-nansen-audit
loficoded Jun 8, 2026
4faea2f
style: apply prettier to byreal rail + tests (#27)
loficoded Jun 8, 2026
16986ab
fix(db): impose a total order on per-round reads
loficoded Jun 8, 2026
1ad9789
Merge pull request #28 from markosiks/fix/db-total-order-queries
loficoded Jun 8, 2026
99c1e03
docs: note that bun run build requires DATABASE_URL
loficoded Jun 8, 2026
fd32533
Merge pull request #29 from markosiks/docs/build-env-requirement
loficoded Jun 8, 2026
b004f37
docs: refresh README to reflect built system + Mantle on-chain
loficoded Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Vector — environment variables (P0.1)
# Copy to `.env.local` and fill in real values. Never commit `.env*` (see .gitignore).
# Validation runs at startup (lib/config/env.ts); a missing/malformed REQUIRED
# value crashes the server with a redacted message. Secrets are server-only and
# never enter the client bundle.

# --- Required ---------------------------------------------------------------
# Neon (Postgres) connection string. Must be postgres:// or postgresql://.
DATABASE_URL=

# --- Optional (validated only if set; needed in later stages) ----------------
# Mantle testnet RPC URL (http(s) or ws(s)). On-chain stages (P1.7+).
# Public default works for reads: https://rpc.sepolia.mantle.xyz
MANTLE_TESTNET_RPC_URL=
# Nansen smart-money API key (P2.2). Secret.
NANSEN_API_KEY=
# Elfa social-signal API key (P3.1). Secret.
ELFA_API_KEY=
# Owner key: registers seed agents in the ERC-8004 Identity Registry. Secret.
OPERATOR_PRIVATE_KEY=
# Attestor key: authors ERC-8004 feedback writes (P1.8). Secret.
# MUST be a different address than OPERATOR_PRIVATE_KEY — the registry rejects
# self-feedback from an agent's owner/operator.
ATTESTOR_PRIVATE_KEY=
# Deployed commit SHA surfaced by /api/health. Non-secret.
GIT_COMMIT=
# Absolute base URL the off-chain attestation detail is served from, e.g.
# https://vector.app — used to build the ERC-8004 feedbackURI (P1.8). Non-secret.
PUBLIC_BASE_URL=

# --- Byreal perps rail (P2.1) — optional; DEFAULT-OFF, fails closed -----------
# The rail enables only when BOTH the agent key and wallet address are set;
# absent => deterministic seed settlement (byte-identical scores).
# Byreal/Hyperliquid agent API key. Secret; scoped to the CLI child env only.
BYREAL_PERPS_AGENT_KEY=
# Byreal/Hyperliquid wallet address (0x…40 hex) the rail trades from.
BYREAL_PERPS_WALLET_ADDRESS=
# Network the rail may trade on: testnet | mainnet. Defaults to testnet. Safety
# boundary — the adapter refuses to construct with mainnet creds unless this is
# explicitly set to mainnet.
BYREAL_PERPS_NETWORK=
# Explicit path to the @byreal-io/byreal-perps-cli entry; else package-resolved.
BYREAL_PERPS_CLI_PATH=
6 changes: 4 additions & 2 deletions gitignore → .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
# =============================================================================

# ----- Dependencies ----------------------------------------------------------
# Match both a node_modules directory and a stray node_modules symlink/file
# (a trailing-slash pattern ignores only directories).
node_modules
node_modules/
.pnp
.pnp.*
Expand Down Expand Up @@ -146,8 +149,7 @@ temp/
# docs will be ignored. Prefer ignoring only GENERATED docs instead, e.g.:
# docs/_generated/
# docs/api/
# Uncomment the broad rule only if docs/ is fully auto-generated:
docs/
# Ignore only GENERATED docs; handwritten docs/*.md are committed deliverables.
docs/_generated/
docs/.vitepress/cache/
docs/.vitepress/dist/
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.next/
node_modules/
coverage/
bun.lock
*.md
# Generated by `bun run api:openapi`; that script owns its formatting (and keeps
# it byte-deterministic for CI diffing), so Prettier must not reflow it.
docs/openapi.json
7 changes: 7 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}
121 changes: 121 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Vector

**The merit layer for autonomous capital on Mantle.**

A bounded-execution **referee** (firewall) + reputation **scoring** (AgentScore
0–100) + a reputation-weighted **capital router**, anchored on-chain by an
ERC-8004 Reputation Registry on Mantle testnet. Demo rail: Byreal Perps CLI.
The product is a deterministic 90-second arc: merit → blocked theft →
reputation collapse → capital reroute.

Built for **Mantle: The Turing Test Hackathon 2026** — track *Agentic Wallets &
Economy*.

## The pipeline

Vector is not three demos — it is one real, deterministic pipeline. The same
referee, scoring and router used in production drive a frozen 90-second arc, so
the demo is honest (no mocked verdicts) and reproducible (same seed ⇒
byte-identical run).

```
signal → decide → intent → referee → execution → outcome
score (AgentScore 0–100)
on-chain anchor (ERC-8004 on Mantle)
capital re-route (pool conserved)
```

- **Referee (firewall).** A pure, deterministic gate over *typed, signed Intents*
(never prompts). A fixed, ordered rule set reduces each Intent to
`HALT/REJECT/CLIP/ALLOW`; blocking rules dominate soft clips. Rule #3
`fresh_wallet_transfer_block` rejects a drain to a non-whitelisted wallet
`hard` — the load-bearing security property. See [docs/referee.md](./docs/referee.md).
- **AgentScore ∈ [0,100].** A pure scoring function whose *only* exposure input is
capital-at-risk (`car_r`), not trade count or volume — the structural root of
its anti-wash / anti-Sybil property. A confirmed drain floor-crashes the score.
See [docs/scoring.md](./docs/scoring.md).
- **Reputation-weighted capital router.** Moves a fixed, *exactly conserved*
capital pool toward the highest scores in bounded, stable steps; a blocked
theft drains the offender and reroutes to the honest leaders. See
[docs/capital-router.md](./docs/capital-router.md).

## On Mantle (on-chain)

Per-round agent feedback is anchored on the **canonical ERC-8004 Reputation
Registry** already deployed on **Mantle Sepolia** (`chainId 5003`). Vector reads/
writes the shared singletons — it does not deploy its own.

| Contract | Address |
| ------------------ | -------------------------------------------- |
| ReputationRegistry | `0x8004B663056A597Dffe9eCcC1965A193B7388713` |
| IdentityRegistry | `0x8004A818BFB912233c491871b3d84c89A494BD9e` |

`giveFeedback(...)` is authorized by `msg.sender` (no off-chain signature); the
feedback author must differ from the agent's owner/operator. See
[docs/erc8004-registry.md](./docs/erc8004-registry.md).

**Real venue, not a sim:** allowed Intents can settle on the real Byreal Perps
testnet venue via the official `@byreal-io/byreal-perps-cli` — a real on-venue
order id + PnL next to the demo. Byreal fills and the read-only Nansen
smart-money signal are optional side-channels that **never** feed the
deterministic scoring arc (default-off ⇒ byte-identical run). See
[docs/byreal-rail.md](./docs/byreal-rail.md) and [docs/nansen-signal.md](./docs/nansen-signal.md).

## Demo — the 90-second arc

Two seed agents (`seed-leader`, `seed-2`). On the penultimate round an operator
injects a fund-draining `transfer` from the leader → the referee blocks it →
scoring crashes the leader → the router reroutes its capital to the honest
runner-up, pool conserved to the last unit. Entry point `runArc(db, DEMO_ARC)`;
determinism is pinned by golden + e2e + fuzz + integration tests. See
[docs/demo-spine.md](./docs/demo-spine.md).

## Stack

Next.js (App Router) · TypeScript (strict) · Neon/Postgres · SWR polling (no
sockets) · zod · **Bun** (package manager, runtime, test runner).

## Quickstart

Requires [Bun](https://bun.sh) ≥ 1.3.

```bash
bun install
cp .env.example .env.local # set DATABASE_URL (Neon postgres:// string)
bun run dev # http://localhost:3000
```

Health: `GET /api/health` runs a real `SELECT 1` and returns
`{ ok, db, config_loaded, commit }` (200 up / 503 down).

## Scripts

```bash
bun run dev | build | start
bun run typecheck # tsc --noEmit
bun run lint # eslint .
bun run test # unit + fuzz + e2e (integration auto-skips w/o DB)
bun run test:integration # needs DATABASE_URL; run in its own process
```

> ⚠️ `bun run build` requires a valid `DATABASE_URL`. The API route modules
> read `ENV` at import, which is validated eagerly when Next collects page data
> at build time, so a build without the variable fails fast. This is expected
> for the Vercel deploy (where `DATABASE_URL` is set); set it locally to build.

## Docs

- [docs/demo-spine.md](./docs/demo-spine.md) — the deterministic 90-second arc.
- [docs/referee.md](./docs/referee.md) · [docs/scoring.md](./docs/scoring.md) ·
[docs/capital-router.md](./docs/capital-router.md) — the three core engines.
- [docs/erc8004-registry.md](./docs/erc8004-registry.md) — on-chain ERC-8004
integration on Mantle Sepolia.
- [docs/byreal-rail.md](./docs/byreal-rail.md) · [docs/nansen-signal.md](./docs/nansen-signal.md)
— the credibility rail and smart-money signal (optional side-channels).
- [docs/config.md](./docs/config.md) — every constant, default and §ARCH ref.
- [docs/env.md](./docs/env.md) — env variables, formats, secret handling.
- [docs/adr/0001-…](./docs/adr/0001-seeded-config-and-swr-polling.md) — why one
seeded config + SWR polling (not sockets).
62 changes: 62 additions & 0 deletions app/api/agents/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { NextRequest } from 'next/server';

import {
type AgentDetailDto,
toAgentDto,
toIntentDto,
toOutcomeDto,
toPolicyEventDto,
toScoreDto,
} from '@/lib/api/dto';
import { NotFoundError } from '@/lib/api/errors';
import { parseLimit, parseUuid } from '@/lib/api/query';
import { ok, route } from '@/lib/api/respond';
import { getPool } from '@/lib/db/client';
import { getAgent } from '@/lib/db/repos/agents';
import { listIntentsByAgent } from '@/lib/db/repos/intents';
import { listRecentOutcomesByAgent } from '@/lib/db/repos/outcomes';
import { listRecentPolicyEventsByAgent } from '@/lib/db/repos/policy-events';
import { listScoreHistoryByAgent } from '@/lib/db/repos/scores';

/**
* `GET /api/agents/[id]` — one agent's detail: its EWMA score history (oldest
* round first), recent intents, the referee decisions on them, and recent
* outcomes. The UI correlates an intent with its decision by `intent_id`, so the
* lists are returned side by side rather than as a fragile nested join.
*
* A malformed `id` is `400 invalid_id`; a well-formed id matching no agent is
* `404 agent_not_found` — the two are kept distinct so an id probe never reads
* as a real "not found". `?limit=` bounds the recent intents/events/outcomes;
* the score history is bounded by the number of rounds.
*/
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';

export function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }): Promise<Response> {
return route(async () => {
const id = parseUuid((await ctx.params).id);
const limit = parseLimit(new URL(req.url).searchParams.get('limit'));
const db = getPool();

const agent = await getAgent(db, id);
if (agent === null) {
throw new NotFoundError('agent not found', 'agent_not_found');
}

const [scores, intents, policyEvents, outcomes] = await Promise.all([
listScoreHistoryByAgent(db, id),
listIntentsByAgent(db, id, limit),
listRecentPolicyEventsByAgent(db, id, limit),
listRecentOutcomesByAgent(db, id, limit),
]);

const payload: AgentDetailDto = {
agent: toAgentDto(agent),
scores: scores.map(toScoreDto),
intents: intents.map(toIntentDto),
policy_events: policyEvents.map(toPolicyEventDto),
outcomes: outcomes.map(toOutcomeDto),
};
return ok(payload);
});
}
48 changes: 48 additions & 0 deletions app/api/attestations/[id]/feedback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextResponse, type NextRequest } from 'next/server';

import { NotFoundError } from '@/lib/api/errors';
import { parseUuid } from '@/lib/api/query';
import { route } from '@/lib/api/respond';
import { getPool } from '@/lib/db/client';
import { getAttestationById } from '@/lib/db/repos/attestations';

/**
* `GET /api/attestations/[id]/feedback` — the off-chain ERC-8004 feedback
* **detail** for one attestation, served from Neon at the on-chain `feedbackURI`.
*
* The body is the **exact stored bytes** (`attestations.feedback_detail`), not a
* re-serialization, so `KECCAK-256(body)` always equals the on-chain
* `feedback_hash` — a client can fetch this, hash it, and prove integrity. The
* hash is echoed in `X-Feedback-Hash` / `ETag` for convenience; it is *not* the
* source of truth (the body is). A malformed `id` is `400`; a well-formed id
* with no attestation, or one whose detail has not been built yet, is `404`.
*/
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';

export function GET(
_req: NextRequest,
ctx: { params: Promise<{ id: string }> },
): Promise<Response> {
return route(async () => {
const id = parseUuid((await ctx.params).id);
const row = await getAttestationById(getPool(), id);
if (row === null || row.feedback_detail === null) {
throw new NotFoundError('attestation detail not found', 'attestation_not_found');
}
return new NextResponse(row.feedback_detail, {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
// The body is served as the *exact* stored bytes for hash verification,
// so pin the declared type: never let a browser MIME-sniff the payload
// into another (e.g. executable) content type.
'X-Content-Type-Options': 'nosniff',
'Cache-Control': 'no-store',
...(row.feedback_hash !== null
? { 'X-Feedback-Hash': row.feedback_hash, ETag: `"${row.feedback_hash}"` }
: {}),
},
});
});
}
32 changes: 32 additions & 0 deletions app/api/attestations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { NextRequest } from 'next/server';

import { toAttestationDto } from '@/lib/api/dto';
import { parseChainState, parseCursor, parseLimit } from '@/lib/api/query';
import { ok, paginate, route } from '@/lib/api/respond';
import { getPool } from '@/lib/db/client';
import { listAttestationsPage } from '@/lib/db/repos/attestations';

/**
* `GET /api/attestations` — ERC-8004 attestation records mirrored in Neon,
* newest first, with their `chain_state` (`optimistic`/`confirmed`/`failed`),
* `tx_hash`, and `block_number`. Optional `?chain_state=` filter and keyset
* `?cursor=` pagination; `?limit=` bounds the page.
*/
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';

export function GET(req: NextRequest): Promise<Response> {
return route(async () => {
const params = new URL(req.url).searchParams;
const limit = parseLimit(params.get('limit'));
const chainState = parseChainState(params.get('chain_state'));
const cursor = parseCursor(params.get('cursor'));

const rows = await listAttestationsPage(getPool(), {
limit,
...(chainState !== undefined ? { chainState } : {}),
...(cursor !== null ? { before: cursor } : {}),
});
return ok(paginate(rows, toAttestationDto, limit));
});
}
18 changes: 18 additions & 0 deletions app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from 'next/server';

import { checkDb } from '@/lib/db/client';
import { buildHealthPayload, healthStatusCode } from '@/lib/health';

/**
* Health endpoint. Runs a real `SELECT 1` against Neon and reports liveness,
* config-loaded status and the deployed commit. Always dynamic (never cached)
* and on the Node.js runtime because it opens a database connection.
*/
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';

export async function GET(): Promise<NextResponse> {
const db = await checkDb();
const payload = buildHealthPayload({ db, commit: process.env.GIT_COMMIT });
return NextResponse.json(payload, { status: healthStatusCode(db) });
}
Loading