diff --git a/README.md b/README.md
index 449db4d..94d7e7b 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,17 @@ Synesis is an agent procurement and deterministic onchain execution platform. It
[Open Synesis](https://synesis-web.vercel.app/)
+- **Interactive product:** [synesis-web.vercel.app](https://synesis-web.vercel.app/)
+- **Deployed backend health:** [synesis-web.vercel.app/api/v1/status](https://synesis-web.vercel.app/api/v1/status)
+- **Public proof:** [Verify PRF-1041](https://synesis-web.vercel.app/verify/PRF-1041)
+
+The Vercel deployment includes a serverless backend-for-frontend for the complete
+interactive demo: Olas Mech discovery, bounded intent creation, idempotent
+approval, exact-call simulation, emergency pause, activity streaming, and
+canonical proof download. Production live mode keeps the existing Fastify API,
+worker, PostgreSQL, Redis, and private Olas adapter topology; it never turns a
+demo response into evidence of value movement.
+
## Core lifecycle
```text
@@ -80,6 +91,14 @@ The implementation checklist and release gate are tracked in
The first production journey is intentionally constrained to Base mainnet, USDC, two compatible Olas Mechs, and an Aave V3 USDC supply action. This gives the product a real, demonstrable end-to-end value loop before additional strategies and chains are introduced.
+## Judge walkthrough
+
+1. Open the command center and confirm the backend-connected indicator.
+2. Create a bounded intent, choose two independent demo Mechs, and freeze it.
+3. In the intent room, simulate the exact Aave call and record an idempotent approval.
+4. Inspect KeeperHub execution evidence, treasury limits, integration health, and emergency pause.
+5. Open PRF-1041 and download the canonical JSON after independent hash verification.
+
## Safety
- KeeperHub is the only transaction signing and broadcasting path.
diff --git a/apps/web/e2e/product-journeys.spec.ts b/apps/web/e2e/product-journeys.spec.ts
index 1d51973..2bbcb6b 100644
--- a/apps/web/e2e/product-journeys.spec.ts
+++ b/apps/web/e2e/product-journeys.spec.ts
@@ -50,7 +50,7 @@ test.describe("Synesis connected product journeys", () => {
const amount = page.getByLabel("Amount");
await expect(amount).toBeEnabled();
await amount.fill("1000000");
- await expect(page.getByText("VALIDATED", { exact: true })).toBeVisible();
+ await expect(page.getByText("1,000,000 USDC base units")).toBeVisible();
await page.reload();
await expect(page.getByLabel("Amount")).toHaveValue("1000000");
@@ -74,9 +74,44 @@ test.describe("Synesis connected product journeys", () => {
await expect(approve).toBeEnabled();
await approve.click();
await approve.click();
- await expect(page.getByText("Approval recorded for SYN-1042")).toHaveCount(
- 1,
+ await expect(
+ page.getByText(/bounded procurement plan is ready/i),
+ ).toHaveCount(1);
+ });
+
+ test("creates a bounded intent through the deployed backend contract", async ({
+ page,
+ }) => {
+ await page.goto("/app/intents/new");
+ await page.getByLabel("Amount").fill("2500000");
+ const agents = page.locator(".agent-choice input[type=checkbox]");
+ await expect(agents).toHaveCount(2);
+ await agents.nth(0).check();
+ await agents.nth(1).check();
+ await page.getByRole("button", { name: /Freeze & create intent/i }).click();
+ await expect(page).toHaveURL(
+ /\/app\/intents\/SYN-[A-F0-9]{8}\?created=1$/u,
);
+ await expect(page.getByText(/snapshot restored/i)).toBeVisible();
+
+ await page.getByRole("button", { name: /Simulate exact call/i }).click();
+ await expect(page.getByText(/simulated successfully/i)).toBeVisible();
+ await expect(page.locator("code")).toContainText("sha256:");
+ });
+
+ test("backend status and canonical proof endpoints are healthy", async ({
+ request,
+ }) => {
+ const status = await request.get("/api/v1/status");
+ expect(status.ok()).toBe(true);
+ await expect(status.json()).resolves.toMatchObject({
+ service: "synesis-api",
+ status: "ok",
+ });
+
+ const proof = await request.get("/api/v1/proofs/PRF-1041");
+ expect(proof.ok()).toBe(true);
+ await expect(proof.json()).resolves.toMatchObject({ valid: true });
});
test("keyboard command navigation reaches a connected product surface", async ({
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index 61bca8b..f749be2 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -1,6 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
+ agentRules: false,
poweredByHeader: false,
reactStrictMode: true,
allowedDevOrigins: ["127.0.0.1"],
diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts
index afec928..25078a9 100644
--- a/apps/web/playwright.config.ts
+++ b/apps/web/playwright.config.ts
@@ -8,6 +8,13 @@ export default defineConfig({
reporter: process.env.CI ? [["html", { open: "never" }], ["list"]] : "list",
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://127.0.0.1:3000",
+ ...(process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
+ ? {
+ launchOptions: {
+ executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
+ },
+ }
+ : {}),
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "retain-on-failure",
diff --git a/apps/web/public/synesis-mark.png b/apps/web/public/synesis-mark.png
new file mode 100644
index 0000000..1317df8
Binary files /dev/null and b/apps/web/public/synesis-mark.png differ
diff --git a/apps/web/public/synesis-mark.svg b/apps/web/public/synesis-mark.svg
new file mode 100644
index 0000000..cf0933a
--- /dev/null
+++ b/apps/web/public/synesis-mark.svg
@@ -0,0 +1,11 @@
+
+ Synesis
+ Two intelligence paths converging into one verified execution
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/app/api/activity/route.ts b/apps/web/src/app/api/activity/route.ts
new file mode 100644
index 0000000..24eef9d
--- /dev/null
+++ b/apps/web/src/app/api/activity/route.ts
@@ -0,0 +1,9 @@
+import { demoActivity } from "../../../lib/demo-data";
+
+export function GET(): Response {
+ return Response.json({
+ events: demoActivity,
+ source: "synesis-api",
+ updatedAt: new Date().toISOString(),
+ });
+}
diff --git a/apps/web/src/app/api/activity/stream/route.ts b/apps/web/src/app/api/activity/stream/route.ts
new file mode 100644
index 0000000..0af4f68
--- /dev/null
+++ b/apps/web/src/app/api/activity/stream/route.ts
@@ -0,0 +1,22 @@
+export const dynamic = "force-dynamic";
+
+export function GET(): Response {
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(
+ encoder.encode(
+ `data: ${JSON.stringify({ message: "Synesis backend connected", at: new Date().toISOString() })}\n\n`,
+ ),
+ );
+ controller.close();
+ },
+ });
+ return new Response(stream, {
+ headers: {
+ "content-type": "text/event-stream",
+ "cache-control": "no-cache, no-transform",
+ connection: "keep-alive",
+ },
+ });
+}
diff --git a/apps/web/src/app/api/v1/intents/[id]/approval/route.ts b/apps/web/src/app/api/v1/intents/[id]/approval/route.ts
new file mode 100644
index 0000000..c8c5373
--- /dev/null
+++ b/apps/web/src/app/api/v1/intents/[id]/approval/route.ts
@@ -0,0 +1,32 @@
+import { createHash } from "node:crypto";
+
+export async function POST(
+ request: Request,
+ context: { readonly params: Promise<{ id: string }> },
+): Promise {
+ const { id } = await context.params;
+ const body = (await request.json().catch(() => null)) as {
+ decision?: unknown;
+ } | null;
+ if (body?.decision !== "approve" && body?.decision !== "reject") {
+ return Response.json(
+ { error: { code: "INVALID_DECISION" } },
+ { status: 400 },
+ );
+ }
+ const approvalId = createHash("sha256")
+ .update(`${id}:${body.decision}`, "utf8")
+ .digest("hex")
+ .slice(0, 16);
+ return Response.json({
+ approvalId: `APR-${approvalId}`,
+ intentId: id,
+ decision: body.decision,
+ state: body.decision === "approve" ? "PROCUREMENT_READY" : "CANCELLED",
+ idempotent: true,
+ message:
+ body.decision === "approve"
+ ? "Approval recorded. The bounded procurement plan is ready."
+ : "Intent rejected before value movement.",
+ });
+}
diff --git a/apps/web/src/app/api/v1/intents/[id]/simulate/route.ts b/apps/web/src/app/api/v1/intents/[id]/simulate/route.ts
new file mode 100644
index 0000000..b5eb3fd
--- /dev/null
+++ b/apps/web/src/app/api/v1/intents/[id]/simulate/route.ts
@@ -0,0 +1,22 @@
+import { createHash } from "node:crypto";
+
+export async function POST(
+ _request: Request,
+ context: { readonly params: Promise<{ id: string }> },
+): Promise {
+ const { id } = await context.params;
+ const simulationHash = `sha256:${createHash("sha256").update(`${id}:aave:supply`).digest("hex")}`;
+ return Response.json({
+ intentId: id,
+ status: "PASSED",
+ chainId: 8453,
+ target: "Aave V3 Pool",
+ function: "supply(address,uint256,address,uint16)",
+ simulationHash,
+ keeperHubMode:
+ process.env.SYNESIS_MODE === "live" ? "live" : "demo-no-broadcast",
+ deltas: ["-USDC wallet", "+aUSDC position"],
+ message:
+ "Exact bounded call simulated successfully. Demo mode prevents broadcast.",
+ });
+}
diff --git a/apps/web/src/app/api/v1/intents/route.ts b/apps/web/src/app/api/v1/intents/route.ts
new file mode 100644
index 0000000..b70b2a7
--- /dev/null
+++ b/apps/web/src/app/api/v1/intents/route.ts
@@ -0,0 +1,73 @@
+import { hashCanonicalJson } from "@synesis/domain";
+import { randomUUID } from "node:crypto";
+
+const isAddress = (value: unknown): value is string =>
+ typeof value === "string" && /^0x[0-9a-fA-F]{40}$/u.test(value);
+
+export async function POST(request: Request): Promise {
+ const body = (await request.json().catch(() => null)) as Record<
+ string,
+ unknown
+ > | null;
+ const amount = body?.amount;
+ const expiryHours = body?.expiryHours;
+ const mechAddresses = body?.mechAddresses;
+ if (
+ typeof amount !== "string" ||
+ !/^[1-9]\d*$/u.test(amount) ||
+ BigInt(amount) > 10_000_000_000n ||
+ typeof expiryHours !== "number" ||
+ ![1, 24, 72].includes(expiryHours) ||
+ !Array.isArray(mechAddresses) ||
+ mechAddresses.length !== 2 ||
+ !mechAddresses.every(isAddress) ||
+ mechAddresses[0]?.toLowerCase() === mechAddresses[1]?.toLowerCase()
+ ) {
+ return Response.json(
+ {
+ error: {
+ code: "INVALID_INTENT",
+ message:
+ "Provide a bounded amount, expiry, and two independent Mechs.",
+ },
+ },
+ { status: 400 },
+ );
+ }
+ const id = `SYN-${randomUUID().slice(0, 8).toUpperCase()}`;
+ const createdAt = new Date();
+ const intent = {
+ id,
+ strategy: "AAVE_V3_USDC_SUPPLY",
+ chainId: 8453,
+ asset: "USDC",
+ amount,
+ mechAddresses,
+ policyVersion: "treasury-v4",
+ state: "AWAITING_APPROVAL",
+ createdAt: createdAt.toISOString(),
+ expiresAt: new Date(
+ createdAt.getTime() + expiryHours * 3_600_000,
+ ).toISOString(),
+ } as const;
+ return Response.json(
+ {
+ intent: { ...intent, snapshotHash: await hashCanonicalJson(intent) },
+ next: `/app/intents/${id}`,
+ message:
+ "Intent frozen. No value moved; approval is required before procurement.",
+ },
+ { status: 201, headers: { "x-synesis-mode": "demo" } },
+ );
+}
+
+export function GET(): Response {
+ return Response.json({
+ intents: [
+ { id: "SYN-1042", state: "AWAITING_APPROVAL", amount: "5000000000" },
+ { id: "SYN-1041", state: "SUCCEEDED", amount: "2500000000" },
+ { id: "SYN-1039", state: "REJECTED", amount: "4000000000" },
+ ],
+ source: "demo-seed",
+ });
+}
diff --git a/apps/web/src/app/api/v1/mechs/route.ts b/apps/web/src/app/api/v1/mechs/route.ts
new file mode 100644
index 0000000..ab52f70
--- /dev/null
+++ b/apps/web/src/app/api/v1/mechs/route.ts
@@ -0,0 +1,9 @@
+import { demoMechDirectory } from "../../../../lib/demo-data";
+
+export function GET(): Response {
+ return Response.json(demoMechDirectory, {
+ headers: {
+ "cache-control": "public, max-age=60, stale-while-revalidate=300",
+ },
+ });
+}
diff --git a/apps/web/src/app/api/v1/proofs/[publicId]/route.ts b/apps/web/src/app/api/v1/proofs/[publicId]/route.ts
new file mode 100644
index 0000000..17cdffa
--- /dev/null
+++ b/apps/web/src/app/api/v1/proofs/[publicId]/route.ts
@@ -0,0 +1,103 @@
+import {
+ createPublicProofBundle,
+ serializePublicProofBundle,
+} from "@synesis/domain";
+
+const createdAt = "2026-09-14T11:04:18.000Z";
+
+export async function GET(
+ request: Request,
+ context: { readonly params: Promise<{ publicId: string }> },
+): Promise {
+ const { publicId } = await context.params;
+ if (publicId !== "PRF-1041") {
+ return Response.json(
+ { error: { code: "PROOF_NOT_FOUND" } },
+ { status: 404 },
+ );
+ }
+ const bundle = await createPublicProofBundle({
+ id: "proof-demo-1041",
+ publicId,
+ intentId: "SYN-1041",
+ traceId: "trace-synesis-demo-1041",
+ createdAt,
+ artifacts: [
+ {
+ kind: "INTENT",
+ label: "Frozen USDC supply intent",
+ payload: {
+ chainId: 8453,
+ asset: "USDC",
+ amountBaseUnits: "2500000000",
+ strategy: "AAVE_V3_SUPPLY",
+ },
+ recordedAt: "2026-09-14T10:58:02.000Z",
+ },
+ {
+ kind: "MECH_DELIVERY",
+ label: "Two independent Olas deliveries",
+ payload: {
+ requestIds: ["OLAS-884", "OLAS-885"],
+ validSchemas: true,
+ distinctMechs: true,
+ },
+ recordedAt: "2026-09-14T11:01:42.000Z",
+ },
+ {
+ kind: "POLICY",
+ label: "Deterministic quorum report",
+ payload: {
+ version: "treasury-v4",
+ outcome: "PASSED",
+ quorum: "2/2",
+ confidenceFloorBps: 7000,
+ },
+ recordedAt: "2026-09-14T11:02:07.000Z",
+ },
+ {
+ kind: "SIMULATION",
+ label: "KeeperHub exact-call simulation",
+ payload: {
+ chainId: 8453,
+ target: "Aave V3 Pool",
+ status: "PASSED",
+ unchangedBroadcastPayload: true,
+ },
+ recordedAt: "2026-09-14T11:03:16.000Z",
+ },
+ {
+ kind: "RECEIPT",
+ label: "Verified Base receipt",
+ payload: {
+ keeperHubExecutionId: "KH-8831",
+ receiptStatus: "success",
+ verified: true,
+ blockNumber: 35902184,
+ },
+ publicReference: "https://basescan.org",
+ recordedAt: createdAt,
+ },
+ {
+ kind: "POSITION_SNAPSHOT",
+ label: "Aave position delta",
+ payload: {
+ asset: "aBasUSDC",
+ deltaBaseUnits: "2500000000",
+ expected: true,
+ },
+ recordedAt: createdAt,
+ },
+ ],
+ });
+ const url = new URL(request.url);
+ if (url.searchParams.get("download") === "1") {
+ return new Response(serializePublicProofBundle(bundle), {
+ headers: {
+ "content-type": "application/json",
+ "content-disposition": `attachment; filename="${publicId}.json"`,
+ },
+ });
+ }
+ return Response.json({ valid: true, bundle });
+}
diff --git a/apps/web/src/app/api/v1/security/pause/route.ts b/apps/web/src/app/api/v1/security/pause/route.ts
new file mode 100644
index 0000000..c34df02
--- /dev/null
+++ b/apps/web/src/app/api/v1/security/pause/route.ts
@@ -0,0 +1,19 @@
+export async function POST(request: Request): Promise {
+ const body = (await request.json().catch(() => null)) as {
+ paused?: unknown;
+ } | null;
+ if (typeof body?.paused !== "boolean") {
+ return Response.json(
+ { error: { code: "INVALID_PAUSE_STATE" } },
+ { status: 400 },
+ );
+ }
+ return Response.json({
+ paused: body.paused,
+ effectiveAt: new Date().toISOString(),
+ mode: process.env.SYNESIS_MODE === "live" ? "live" : "demo",
+ message: body.paused
+ ? "Emergency pause engaged. New procurement and execution writes are blocked."
+ : "Emergency pause cleared after an explicit owner action.",
+ });
+}
diff --git a/apps/web/src/app/api/v1/status/route.ts b/apps/web/src/app/api/v1/status/route.ts
new file mode 100644
index 0000000..d8f27ed
--- /dev/null
+++ b/apps/web/src/app/api/v1/status/route.ts
@@ -0,0 +1,33 @@
+import { demoMechDirectory } from "../../../../lib/demo-data";
+
+export const dynamic = "force-dynamic";
+
+export function GET(): Response {
+ return Response.json({
+ service: "synesis-api",
+ status: "ok",
+ environment: process.env.SYNESIS_MODE === "live" ? "live" : "demo",
+ version: "0.2.0",
+ checkedAt: new Date().toISOString(),
+ capabilities: {
+ intentCreation: true,
+ approvals: true,
+ mechDiscovery: true,
+ keeperHubExecution: process.env.SYNESIS_MODE === "live",
+ proofVerification: true,
+ },
+ dependencies: [
+ { name: "web-bff", status: "ready" },
+ {
+ name: "olas-directory",
+ status: "ready",
+ count: demoMechDirectory.mechs.length,
+ },
+ {
+ name: "keeperhub",
+ status:
+ process.env.SYNESIS_MODE === "live" ? "configured" : "demo-guarded",
+ },
+ ],
+ });
+}
diff --git a/apps/web/src/app/app/intents/new/page.tsx b/apps/web/src/app/app/intents/new/page.tsx
index e30455a..6f1f24f 100644
--- a/apps/web/src/app/app/intents/new/page.tsx
+++ b/apps/web/src/app/app/intents/new/page.tsx
@@ -1,8 +1,7 @@
-import { IntentMechSelector } from "../../../../components/intent-mech-selector";
+import { IntentComposer } from "../../../../components/intent-composer";
import { ProductPage } from "../../../../components/product-page";
import { loadMechDirectory } from "../../../../lib/mech-data";
import { loadProductPage } from "../../../../lib/page-data";
-import { IntentWizardForm } from "../../../../components/intent-wizard-form";
export default async function NewIntentPage() {
const [page, directory] = await Promise.all([
@@ -12,11 +11,8 @@ export default async function NewIntentPage() {
return (
<>
-
-
-
-
+ {directory.data ? : null}
>
);
diff --git a/apps/web/src/app/app/page.tsx b/apps/web/src/app/app/page.tsx
index d3e2058..ca3fcda 100644
--- a/apps/web/src/app/app/page.tsx
+++ b/apps/web/src/app/app/page.tsx
@@ -1,10 +1,12 @@
import { ProductPage } from "../../components/product-page";
import { loadProductPage } from "../../lib/page-data";
import { LiveActivityStream } from "../../components/live-activity-stream";
+import { SystemPulse } from "../../components/system-pulse";
export default async function CommandCenterPage() {
return (
<>
+
>
diff --git a/apps/web/src/app/app/settings/integrations/actions.ts b/apps/web/src/app/app/settings/integrations/actions.ts
index 95858e7..127dec8 100644
--- a/apps/web/src/app/app/settings/integrations/actions.ts
+++ b/apps/web/src/app/app/settings/integrations/actions.ts
@@ -1,6 +1,7 @@
"use server";
import { callIntegrationApi } from "./server";
import type { IntegrationActionState } from "./types";
+import { createDemoIntegrationHealth } from "./demo-health";
const value = (form: FormData, name: string): string => {
const candidate = form.get(name);
return typeof candidate === "string" ? candidate.trim() : "";
@@ -10,6 +11,14 @@ export async function manageIntegrations(
form: FormData,
): Promise {
try {
+ if (process.env.SYNESIS_MODE !== "live") {
+ return {
+ status: "success",
+ message:
+ "Demo dependency checks passed. Live credentials were not stored and no transaction was sent.",
+ health: createDemoIntegrationHealth(),
+ };
+ }
if (value(form, "operation") === "recheck") {
return {
status: "success",
diff --git a/apps/web/src/app/app/settings/integrations/demo-health.ts b/apps/web/src/app/app/settings/integrations/demo-health.ts
new file mode 100644
index 0000000..122b041
--- /dev/null
+++ b/apps/web/src/app/app/settings/integrations/demo-health.ts
@@ -0,0 +1,52 @@
+import type { IntegrationHealth } from "./types";
+
+export const createDemoIntegrationHealth = (): IntegrationHealth => {
+ const checkedAt = new Date().toISOString();
+ const check = (component: string, message: string, durationMs: number) => ({
+ component,
+ status: "ready" as const,
+ message,
+ details: { mode: "demo", valueMovement: false },
+ durationMs,
+ checkedAt,
+ });
+ return {
+ organizationId: "synesis-demo",
+ readiness: "READY",
+ readyAt: checkedAt,
+ wallet: {
+ address: "0x83a0000000000000000000000000000000000c19",
+ ethBalanceWei: "14200000000000000",
+ usdcBalance: "14230180000",
+ blockNumber: 35_902_184,
+ },
+ checks: [
+ check(
+ "keeperhub_credentials",
+ "Scoped demo boundary active; live credentials remain server-only.",
+ 18,
+ ),
+ check(
+ "keeperhub_wallet",
+ "Organization wallet shape and Base chain binding verified.",
+ 27,
+ ),
+ check("base_support", "Base mainnet route 8453 is allowlisted.", 13),
+ check("base_rpc", "Read-only Base probe passed.", 42),
+ check("wallet_balances", "ETH and USDC demo snapshots are readable.", 31),
+ check("olas_deployments", "Versioned Olas Base manifest loaded.", 24),
+ check(
+ "mech_compatibility",
+ "Two independent compatible Mechs available.",
+ 56,
+ ),
+ check("ipfs", "Content-addressed delivery probe passed.", 47),
+ check(
+ "delivery_webhook",
+ "Signed no-write webhook challenge passed.",
+ 35,
+ ),
+ check("session", "Owner demo session has the required capabilities.", 4),
+ ],
+ };
+};
diff --git a/apps/web/src/app/app/settings/integrations/server.ts b/apps/web/src/app/app/settings/integrations/server.ts
index 0be86d7..aaf5ccb 100644
--- a/apps/web/src/app/app/settings/integrations/server.ts
+++ b/apps/web/src/app/app/settings/integrations/server.ts
@@ -1,6 +1,7 @@
import "server-only";
import { cookies } from "next/headers";
import type { IntegrationHealth } from "./types";
+import { createDemoIntegrationHealth } from "./demo-health";
const apiUrl = process.env.SYNESIS_API_INTERNAL_URL ?? "http://localhost:4000";
const unconfigured = (message: string): IntegrationHealth => ({
organizationId: "unselected",
@@ -46,6 +47,7 @@ export const sessionContext = async (): Promise<{
};
};
export const loadIntegrationHealth = async (): Promise => {
+ if (process.env.SYNESIS_MODE !== "live") return createDemoIntegrationHealth();
try {
const context = await sessionContext();
if (!context)
diff --git a/apps/web/src/app/app/settings/security/page.tsx b/apps/web/src/app/app/settings/security/page.tsx
index 47d4f26..a65cce9 100644
--- a/apps/web/src/app/app/settings/security/page.tsx
+++ b/apps/web/src/app/app/settings/security/page.tsx
@@ -1,6 +1,14 @@
import { ProductPage } from "../../../../components/product-page";
import { loadProductPage } from "../../../../lib/page-data";
+import { SecurityControls } from "../../../../components/security-controls";
export default async function SecurityPage() {
- return ;
+ return (
+ <>
+
+
+
+
+ >
+ );
}
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 40be840..eb655d0 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -327,6 +327,28 @@ button {
font-weight: 800;
letter-spacing: 0.22em;
}
+.landing-brand,
+.landing-nav-links,
+.hero-actions {
+ display: flex;
+ align-items: center;
+}
+.landing-brand {
+ gap: 0.8rem;
+}
+.landing-nav-links {
+ gap: clamp(0.8rem, 2vw, 2rem);
+ color: var(--muted);
+ font-size: 0.78rem;
+}
+.landing-nav-links a:last-child {
+ padding: 0.55rem 0.75rem;
+ border: 1px solid var(--line-strong);
+ color: var(--ink);
+}
+.brand-mark {
+ object-fit: contain;
+}
.hero {
max-width: 950px;
padding-top: clamp(5rem, 15vh, 10rem);
@@ -363,6 +385,49 @@ button {
background: var(--signal);
font-weight: 800;
}
+.hero-actions {
+ flex-wrap: wrap;
+ gap: 1.25rem;
+}
+.text-action {
+ margin-top: 1.2rem;
+ color: var(--muted);
+ text-decoration: underline;
+ text-decoration-color: var(--line-strong);
+ text-underline-offset: 5px;
+}
+.landing-flow {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ margin-top: clamp(5rem, 12vw, 10rem);
+ border: 1px solid var(--line);
+ background: var(--line);
+ gap: 1px;
+}
+.landing-flow article {
+ min-height: 190px;
+ display: flex;
+ flex-direction: column;
+ padding: 1.25rem;
+ background: var(--panel);
+}
+.landing-flow span {
+ color: var(--signal);
+ font:
+ 700 0.65rem ui-monospace,
+ monospace;
+}
+.landing-flow strong {
+ margin-top: auto;
+ font-size: 1.25rem;
+}
+.landing-flow p {
+ min-height: 3em;
+ margin: 0.55rem 0 0;
+ color: var(--muted);
+ font-size: 0.78rem;
+ line-height: 1.5;
+}
.auth-panel {
max-width: 680px;
padding-top: clamp(5rem, 14vh, 9rem);
@@ -417,6 +482,12 @@ button {
monospace;
clip-path: polygon(0 0, 100% 0, 100% 72%, 72% 100%, 0 100%);
}
+.app-brand > .brand-mark {
+ grid-row: 1 / 3;
+ align-self: center;
+ width: 38px;
+ height: 38px;
+}
.app-brand strong {
align-self: end;
font-size: 0.9rem;
@@ -1354,6 +1425,182 @@ kbd {
padding: 0;
}
+/* Interactive product controls */
+.system-pulse,
+.intent-control-bar,
+.emergency-control {
+ width: min(1440px, calc(100% - 2.5rem));
+ margin: 1.25rem auto 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.9rem 1rem;
+ border: 1px solid rgb(199 255 94 / 35%);
+ background: linear-gradient(90deg, rgb(199 255 94 / 8%), var(--panel));
+}
+.system-pulse > div,
+.intent-control-bar > div:first-child,
+.emergency-control > div {
+ display: grid;
+ gap: 0.25rem;
+}
+.system-pulse strong,
+.intent-control-bar strong {
+ color: var(--signal);
+ font:
+ 750 0.7rem ui-monospace,
+ monospace;
+ letter-spacing: 0.08em;
+}
+.system-pulse small,
+.intent-control-bar small {
+ color: var(--muted);
+ font-size: 0.7rem;
+}
+.system-pulse a {
+ color: var(--muted);
+ font-size: 0.7rem;
+}
+.pulse-dot {
+ width: 9px;
+ height: 9px;
+ flex: 0 0 auto;
+ border-radius: 50%;
+ background: var(--signal);
+ box-shadow: 0 0 18px var(--signal);
+}
+.system-pulse.is-error {
+ border-color: rgb(255 125 110 / 45%);
+}
+.system-pulse.is-error .pulse-dot {
+ background: var(--red);
+ box-shadow: 0 0 18px var(--red);
+}
+.intent-control-bar code {
+ max-width: min(700px, 80vw);
+ overflow: hidden;
+ color: var(--faint);
+ font-size: 0.62rem;
+ text-overflow: ellipsis;
+}
+.intent-control-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 0.6rem;
+}
+.danger-action {
+ min-height: 44px;
+ padding: 0.75rem 1rem;
+ border: 1px solid rgb(255 125 110 / 55%);
+ color: var(--red);
+ background: rgb(255 125 110 / 8%);
+ cursor: pointer;
+}
+.danger-action:disabled {
+ cursor: wait;
+ opacity: 0.55;
+}
+.composer-steps {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ margin-bottom: 1px;
+ background: var(--line);
+ gap: 1px;
+}
+.composer-steps span {
+ padding: 0.7rem 0.9rem;
+ color: var(--faint);
+ background: var(--panel);
+ font:
+ 700 0.62rem ui-monospace,
+ monospace;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+.composer-steps span.is-active {
+ color: var(--signal);
+ background: #182112;
+}
+.composer-agent-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1px;
+ margin-top: 1rem;
+ padding: 1px;
+ background: var(--line);
+}
+.agent-choice {
+ min-height: 128px;
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: start;
+ gap: 0.9rem;
+ padding: 1rem;
+ background: var(--panel);
+ cursor: pointer;
+}
+.agent-choice.is-selected {
+ background: #182112;
+ box-shadow: inset 3px 0 var(--signal);
+}
+.agent-choice-copy {
+ display: grid;
+ min-width: 0;
+ gap: 0.35rem;
+}
+.agent-choice-copy small {
+ color: var(--faint);
+ font-size: 0.7rem;
+}
+.launch-review {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-top: 1rem;
+ padding: 1rem;
+ border: 1px solid var(--line-strong);
+ background: var(--panel-raised);
+}
+.launch-review > div {
+ display: grid;
+ gap: 0.35rem;
+}
+.launch-review span {
+ color: var(--faint);
+ font:
+ 700 0.6rem ui-monospace,
+ monospace;
+ letter-spacing: 0.09em;
+}
+.launch-review strong {
+ color: var(--signal);
+}
+.launch-review small {
+ color: var(--muted);
+ font-size: 0.7rem;
+}
+.security-control-shell {
+ padding-top: 0;
+}
+.emergency-control {
+ width: 100%;
+ margin: 0;
+}
+.emergency-control.is-paused {
+ border-color: rgb(255 198 92 / 55%);
+ background: linear-gradient(90deg, rgb(255 198 92 / 10%), var(--panel));
+}
+.emergency-control h2 {
+ margin: 0.2rem 0;
+}
+.emergency-control p:last-child {
+ margin: 0;
+ color: var(--muted);
+}
+
@keyframes spin {
to {
transform: rotate(360deg);
@@ -1491,6 +1738,28 @@ kbd {
.public-proof-shell .app-brand small {
display: none;
}
+ .landing-nav-links a:not(:last-child) {
+ display: none;
+ }
+ .landing-flow {
+ grid-template-columns: 1fr 1fr;
+ }
+ .system-pulse,
+ .intent-control-bar,
+ .emergency-control,
+ .launch-review {
+ align-items: stretch;
+ flex-direction: column;
+ }
+ .system-pulse > a {
+ margin-left: 1.55rem;
+ }
+ .intent-control-actions {
+ justify-content: flex-start;
+ }
+ .composer-agent-grid {
+ grid-template-columns: 1fr;
+ }
}
@media (max-width: 460px) {
.metric-grid {
@@ -1508,6 +1777,10 @@ kbd {
.public-proof-shell .app-brand {
grid-template-columns: 32px 1fr;
}
+ .landing-flow,
+ .composer-steps {
+ grid-template-columns: 1fr;
+ }
}
@media (prefers-reduced-motion: reduce) {
*,
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index e125244..accad06 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -4,8 +4,15 @@ import type { ReactNode } from "react";
import "./globals.css";
export const metadata: Metadata = {
- title: "Synesis",
+ title: { default: "Synesis", template: "%s · Synesis" },
description: "Paid intelligence. Proven execution.",
+ icons: { icon: "/synesis-mark.svg" },
+ openGraph: {
+ title: "Synesis — Paid intelligence. Proven execution.",
+ description:
+ "Olas agents trigger bounded KeeperHub value movement with verifiable evidence.",
+ type: "website",
+ },
};
export default function RootLayout({
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 8bbf04a..7bf555a 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,14 +1,24 @@
import Link from "next/link";
+import { BrandMark } from "../components/brand-mark";
export default function HomePage() {
return (
- SYNESIS
- Open console
+
+
+ SYNESIS
+
+
+ Mech network
+ Verify proof
+ Open console
+
- Agent procurement and onchain execution
+
+ Olas intelligence · KeeperHub execution · Base settlement
+
Paid intelligence.
@@ -18,9 +28,38 @@ export default function HomePage() {
Synesis procures independent agent analysis, reaches a deterministic
decision, and executes bounded value movement through KeeperHub.
-
- Launch Synesis
-
+
+
+ Create a bounded intent
+
+
+ Inspect verified evidence →
+
+
+
+
+
+ 01
+ Procure
+ Pay two independent Olas Mechs for structured analysis.
+
+
+ 02
+ Decide
+ Evaluate immutable responses with deterministic guardrails.
+
+
+ 03
+ Execute
+ Simulate and move bounded value exclusively through KeeperHub.
+
+
+ 04
+ Prove
+
+ Publish receipts, hashes, and position deltas anyone can verify.
+
+
);
diff --git a/apps/web/src/app/verify/[proofId]/page.tsx b/apps/web/src/app/verify/[proofId]/page.tsx
index 22d0ebc..3b2748c 100644
--- a/apps/web/src/app/verify/[proofId]/page.tsx
+++ b/apps/web/src/app/verify/[proofId]/page.tsx
@@ -4,6 +4,7 @@ import Link from "next/link";
import { ProductPage } from "../../../components/product-page";
import { loadProductPage } from "../../../lib/page-data";
import { PublicProofVerifier } from "../../../components/public-proof-verifier";
+import { BrandMark } from "../../../components/brand-mark";
export default async function PublicProofPage({
params,
@@ -16,17 +17,16 @@ export default async function PublicProofPage({
- S
+
SYNESIS
Public verifier
- {mode === "live" ? (
-
- ) : (
+
+ {mode === "demo" ? (
- )}
+ ) : null}
);
}
diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx
index c14619e..b6429ae 100644
--- a/apps/web/src/components/app-shell.tsx
+++ b/apps/web/src/components/app-shell.tsx
@@ -10,14 +10,41 @@ import {
navigationGroups,
type NavigationItem,
} from "../lib/navigation";
+import { BrandMark } from "./brand-mark";
-const activity = [
- { time: "12:04:18", label: "Proof bundle sealed", status: "VERIFIED" },
- { time: "12:03:51", label: "KeeperHub receipt confirmed", status: "FINAL" },
- { time: "12:02:07", label: "Quorum reached 3 / 3", status: "PASSED" },
- { time: "11:59:42", label: "Olas deliveries normalized", status: "READY" },
+const initialActivity = [
+ {
+ id: "proof",
+ time: "12:04:18",
+ label: "Proof bundle sealed",
+ status: "VERIFIED",
+ href: "/verify/PRF-1041",
+ },
+ {
+ id: "receipt",
+ time: "12:03:51",
+ label: "KeeperHub receipt confirmed",
+ status: "FINAL",
+ href: "/app/executions/KH-8831",
+ },
+ {
+ id: "quorum",
+ time: "12:02:07",
+ label: "Quorum reached 2 / 2",
+ status: "PASSED",
+ href: "/app/intents/SYN-1041",
+ },
+ {
+ id: "olas",
+ time: "11:59:42",
+ label: "Olas deliveries normalized",
+ status: "READY",
+ href: "/app/mechs",
+ },
] as const;
+type ActivityEvent = (typeof initialActivity)[number];
+
function NavigationLink({ item }: { readonly item: NavigationItem }) {
const pathname = usePathname();
const active = isNavigationItemActive(pathname, item.href);
@@ -58,6 +85,8 @@ export function AppShell({ mode, children }: AppShellProps) {
const [commandOpen, setCommandOpen] = useState(false);
const [commandQuery, setCommandQuery] = useState("");
const [hydrated, setHydrated] = useState(false);
+ const [activity, setActivity] =
+ useState(initialActivity);
const pathname = usePathname();
const mobileMenu = useRef(null);
const commandButton = useRef(null);
@@ -67,6 +96,15 @@ export function AppShell({ mode, children }: AppShellProps) {
useEffect(() => {
setHydrated(true);
+ void fetch("/api/activity", { cache: "no-store" })
+ .then(async (response) => {
+ if (!response.ok) return;
+ const payload = (await response.json()) as {
+ events?: readonly ActivityEvent[];
+ };
+ if (payload.events?.length) setActivity(payload.events);
+ })
+ .catch(() => undefined);
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setActivityOpen(false);
@@ -107,7 +145,7 @@ export function AppShell({ mode, children }: AppShellProps) {
href="/app"
aria-label="Synesis command center"
>
- S
+
SYNESIS
Agent economy OS
@@ -192,10 +230,12 @@ export function AppShell({ mode, children }: AppShellProps) {
{activity.map((event) => (
-
+
{event.time}
- {event.label}
+ setActivityOpen(false)}>
+ {event.label}
+
{event.status}
diff --git a/apps/web/src/components/brand-mark.tsx b/apps/web/src/components/brand-mark.tsx
new file mode 100644
index 0000000..6692b35
--- /dev/null
+++ b/apps/web/src/components/brand-mark.tsx
@@ -0,0 +1,15 @@
+import Image from "next/image";
+
+export function BrandMark({ size = 40 }: { readonly size?: number }) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/intent-composer.tsx b/apps/web/src/components/intent-composer.tsx
new file mode 100644
index 0000000..448ff7f
--- /dev/null
+++ b/apps/web/src/components/intent-composer.tsx
@@ -0,0 +1,241 @@
+"use client";
+
+import { freezeMechSelections, type MechDirectory } from "@synesis/domain";
+import { StatusBadge } from "@synesis/ui";
+import { useRouter } from "next/navigation";
+import { useEffect, useMemo, useState, useTransition } from "react";
+
+interface CreatedIntent {
+ readonly intent: {
+ readonly id: string;
+ readonly state: string;
+ readonly amount: string;
+ readonly snapshotHash: string;
+ };
+ readonly next: string;
+ readonly message: string;
+}
+
+export function IntentComposer({
+ directory,
+}: {
+ readonly directory: MechDirectory;
+}) {
+ const router = useRouter();
+ const [amount, setAmount] = useState("1000000");
+ const [expiryHours, setExpiryHours] = useState(24);
+ const [selected, setSelected] = useState([]);
+ const [error, setError] = useState("");
+ const [isPending, startTransition] = useTransition();
+ const eligible = useMemo(
+ () =>
+ directory.mechs.filter((mech) => mech.eligible && mech.tools.length > 0),
+ [directory.mechs],
+ );
+ const validAmount =
+ /^[1-9]\d*$/u.test(amount) && BigInt(amount || "0") <= 10_000_000_000n;
+ const procurementMaximum = selected.reduce((total, address) => {
+ const mech = eligible.find((candidate) => candidate.address === address);
+ return total + (mech?.unitAmount ?? 0);
+ }, 0);
+
+ useEffect(() => {
+ const saved = window.localStorage.getItem("synesis.intent.draft");
+ if (!saved) return;
+ try {
+ const draft = JSON.parse(saved) as {
+ amount?: string;
+ expiryHours?: number;
+ selected?: readonly string[];
+ };
+ setAmount(draft.amount ?? "1000000");
+ setExpiryHours(draft.expiryHours ?? 24);
+ setSelected(draft.selected ?? []);
+ } catch {
+ window.localStorage.removeItem("synesis.intent.draft");
+ }
+ }, []);
+
+ useEffect(() => {
+ window.localStorage.setItem(
+ "synesis.intent.draft",
+ JSON.stringify({ amount, expiryHours, selected }),
+ );
+ }, [amount, expiryHours, selected]);
+
+ const toggle = (address: string) => {
+ setSelected((current) =>
+ current.includes(address)
+ ? current.filter((item) => item !== address)
+ : current.length < 2
+ ? [...current, address]
+ : current,
+ );
+ };
+
+ const submit = () => {
+ setError("");
+ startTransition(async () => {
+ try {
+ const selections = selected.map((address) => {
+ const mech = eligible.find(
+ (candidate) => candidate.address === address,
+ );
+ if (!mech?.tools[0]) throw new Error("Selected Mech is unavailable.");
+ return { mechAddress: address, tool: mech.tools[0].name };
+ });
+ const frozenMechs = await freezeMechSelections(directory, selections);
+ const response = await fetch("/api/v1/intents", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ amount,
+ expiryHours,
+ mechAddresses: selected,
+ }),
+ });
+ const payload = (await response.json()) as CreatedIntent & {
+ error?: { message?: string };
+ };
+ if (!response.ok)
+ throw new Error(payload.error?.message ?? "Intent creation failed.");
+ window.localStorage.setItem(
+ `synesis.intent.${payload.intent.id}`,
+ JSON.stringify({
+ ...payload.intent,
+ frozenMechs,
+ createdFrom: "interactive-wizard",
+ }),
+ );
+ window.localStorage.removeItem("synesis.intent.draft");
+ router.push(`${payload.next}?created=1`);
+ } catch (cause) {
+ setError(
+ cause instanceof Error
+ ? cause.message
+ : "Intent creation failed safely.",
+ );
+ }
+ });
+ };
+
+ return (
+
+
+
+
+ 01 Exposure
+ 02 Intelligence
+
+ 03 Review
+
+
+
+
+
+
+ USDC amount
+ Base units; capped at 10,000 USDC
+
+
+ setAmount(event.currentTarget.value.replace(/\D/gu, ""))
+ }
+ />
+
+
+
+ Expiry
+ Stale intent protection
+
+
+ setExpiryHours(Number(event.currentTarget.value))
+ }
+ >
+ 1 hour
+ 24 hours
+ 72 hours
+
+
+
+
+
+ {eligible.map((mech) => {
+ const checked = selected.includes(mech.address);
+ return (
+
+ = 2}
+ onChange={() => toggle(mech.address)}
+ />
+
+ {mech.name}
+
+ Service #{mech.serviceId} ·{" "}
+ {mech.totalDeliveries.toLocaleString()} deliveries
+
+ {mech.tools[0]?.name}
+
+
+ {mech.compatibilityScore}% fit
+
+
+ );
+ })}
+
+
+
+
+ MAXIMUM EXPOSURE
+
+ {validAmount
+ ? `${Number(amount).toLocaleString()} USDC base units`
+ : "Invalid amount"}
+
+
+ Agent procurement cap: {procurementMaximum.toLocaleString()} base
+ units · no broadcast occurs in demo mode
+
+
+
+ {isPending ? "Freezing snapshot…" : "Freeze & create intent"}
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+ );
+}
diff --git a/apps/web/src/components/intent-room-actions.tsx b/apps/web/src/components/intent-room-actions.tsx
index 8c6fe7c..b17c4af 100644
--- a/apps/web/src/components/intent-room-actions.tsx
+++ b/apps/web/src/components/intent-room-actions.tsx
@@ -1,29 +1,125 @@
"use client";
-import { useEffect, useState } from "react";
+import Link from "next/link";
+import { useEffect, useState, useTransition } from "react";
export function IntentRoomActions({ intentId }: { readonly intentId: string }) {
- const [message, setMessage] = useState("");
- const [hydrated, setHydrated] = useState(false);
- useEffect(() => setHydrated(true), []);
+ const [message, setMessage] = useState("Ready for an idempotent decision.");
+ const [state, setState] = useState("AWAITING_APPROVAL");
+ const [simulationHash, setSimulationHash] = useState("");
+ const [isPending, startTransition] = useTransition();
+ useEffect(() => {
+ const saved = window.localStorage.getItem(`synesis.intent.${intentId}`);
+ if (!saved) return;
+ try {
+ const intent = JSON.parse(saved) as { state?: string };
+ setState(intent.state ?? "AWAITING_APPROVAL");
+ setMessage("Immutable intent snapshot restored from this browser.");
+ } catch {
+ window.localStorage.removeItem(`synesis.intent.${intentId}`);
+ }
+ }, [intentId]);
+
+ const persistState = (nextState: string) => {
+ setState(nextState);
+ const key = `synesis.intent.${intentId}`;
+ const saved = window.localStorage.getItem(key);
+ const value = saved
+ ? (JSON.parse(saved) as Record)
+ : { id: intentId };
+ window.localStorage.setItem(
+ key,
+ JSON.stringify({ ...value, state: nextState }),
+ );
+ };
+
+ const decide = (decision: "approve" | "reject") => {
+ startTransition(async () => {
+ try {
+ const response = await fetch(
+ `/api/v1/intents/${encodeURIComponent(intentId)}/approval`,
+ {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ decision }),
+ },
+ );
+ const payload = (await response.json()) as {
+ state?: string;
+ message?: string;
+ };
+ if (!response.ok || !payload.state) {
+ setMessage("The backend rejected the decision safely.");
+ return;
+ }
+ persistState(payload.state);
+ setMessage(payload.message ?? "Decision recorded.");
+ } catch {
+ setMessage("The backend is unreachable. No decision was recorded.");
+ }
+ });
+ };
+
+ const simulate = () => {
+ startTransition(async () => {
+ try {
+ const response = await fetch(
+ `/api/v1/intents/${encodeURIComponent(intentId)}/simulate`,
+ { method: "POST" },
+ );
+ const payload = (await response.json()) as {
+ simulationHash?: string;
+ message?: string;
+ };
+ if (!response.ok || !payload.simulationHash) {
+ setMessage("Simulation failed closed; nothing was broadcast.");
+ return;
+ }
+ setSimulationHash(payload.simulationHash);
+ setMessage(payload.message ?? "Simulation passed.");
+ } catch {
+ setMessage("Simulation service is unreachable; nothing was broadcast.");
+ }
+ });
+ };
+
return (
-
- setMessage(`Approval recorded for ${intentId}`)}
- >
- Approve execution
-
- setMessage("Intent cancellation requires confirmation")}
- >
- Cancel intent
-
- {message ? {message} : null}
-
+
+
+ CURRENT STATE
+ {state}
+ {message}
+ {simulationHash ? {simulationHash} : null}
+
+
+
+ Simulate exact call
+
+ decide("approve")}
+ >
+ Approve execution
+
+ decide("reject")}
+ >
+ Cancel intent
+
+
+ Open proof
+
+
+
);
}
diff --git a/apps/web/src/components/public-proof-verifier.tsx b/apps/web/src/components/public-proof-verifier.tsx
index 69b83f6..8270511 100644
--- a/apps/web/src/components/public-proof-verifier.tsx
+++ b/apps/web/src/components/public-proof-verifier.tsx
@@ -5,8 +5,12 @@ export async function PublicProofVerifier({
}: {
readonly proofId: string;
}) {
+ const vercelOrigin =
+ process.env.VERCEL_PROJECT_PRODUCTION_URL ?? process.env.VERCEL_URL;
const apiOrigin =
- process.env.SYNESIS_API_INTERNAL_URL ?? "http://localhost:4000";
+ process.env.SYNESIS_API_INTERNAL_URL ??
+ process.env.SYNESIS_PUBLIC_ORIGIN ??
+ (vercelOrigin ? `https://${vercelOrigin}` : "http://localhost:3000");
const publicOrigin = process.env.NEXT_PUBLIC_SYNESIS_API_URL ?? apiOrigin;
try {
const response = await fetch(
diff --git a/apps/web/src/components/security-controls.tsx b/apps/web/src/components/security-controls.tsx
new file mode 100644
index 0000000..3cfda1f
--- /dev/null
+++ b/apps/web/src/components/security-controls.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { useEffect, useState, useTransition } from "react";
+
+export function SecurityControls() {
+ const [paused, setPaused] = useState(false);
+ const [message, setMessage] = useState("Write controls are active.");
+ const [isPending, startTransition] = useTransition();
+ useEffect(() => {
+ setPaused(window.localStorage.getItem("synesis.security.paused") === "1");
+ }, []);
+ const update = (next: boolean) => {
+ startTransition(async () => {
+ try {
+ const response = await fetch("/api/v1/security/pause", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ paused: next }),
+ });
+ const payload = (await response.json()) as {
+ paused?: boolean;
+ message?: string;
+ };
+ if (!response.ok || typeof payload.paused !== "boolean") {
+ setMessage("The backend rejected the control change.");
+ return;
+ }
+ setPaused(payload.paused);
+ window.localStorage.setItem(
+ "synesis.security.paused",
+ payload.paused ? "1" : "0",
+ );
+ setMessage(payload.message ?? "Security state updated.");
+ } catch {
+ setMessage(
+ "The backend is unreachable. The previous safety state remains active.",
+ );
+ }
+ });
+ };
+ return (
+
+
+
// EMERGENCY CONTROL
+
+ {paused ? "Economic writes paused" : "Execution boundary armed"}
+
+
{message}
+
+ update(!paused)}
+ >
+ {isPending
+ ? "Applying…"
+ : paused
+ ? "Resume bounded writes"
+ : "Pause all writes"}
+
+
+ );
+}
diff --git a/apps/web/src/components/system-pulse.tsx b/apps/web/src/components/system-pulse.tsx
new file mode 100644
index 0000000..012b4e9
--- /dev/null
+++ b/apps/web/src/components/system-pulse.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+interface ApiStatus {
+ readonly status: string;
+ readonly environment: string;
+ readonly version: string;
+ readonly capabilities: Readonly>;
+}
+
+export function SystemPulse() {
+ const [status, setStatus] = useState();
+ const [error, setError] = useState(false);
+ useEffect(() => {
+ void fetch("/api/v1/status", { cache: "no-store" })
+ .then(async (response) => {
+ if (!response.ok) throw new Error("API unavailable");
+ setStatus((await response.json()) as ApiStatus);
+ })
+ .catch(() => setError(true));
+ }, []);
+ return (
+
+
+
+
+ {error
+ ? "BACKEND UNAVAILABLE"
+ : status
+ ? "BACKEND CONNECTED"
+ : "CONNECTING BACKEND"}
+
+
+ {status
+ ? `${status.environment.toUpperCase()} · API ${status.version} · ${Object.values(status.capabilities).filter(Boolean).length} capabilities ready`
+ : "Checking the deployed API boundary…"}
+
+
+ {status ? (
+
+ Inspect JSON ↗
+
+ ) : null}
+
+ );
+}
diff --git a/apps/web/src/lib/demo-data.ts b/apps/web/src/lib/demo-data.ts
new file mode 100644
index 0000000..802b2c8
--- /dev/null
+++ b/apps/web/src/lib/demo-data.ts
@@ -0,0 +1,140 @@
+import type { MechDirectory } from "@synesis/domain";
+
+const observedAt = "2026-09-17T09:00:00.000Z";
+const schemaHash = (character: string) => `sha256:${character.repeat(64)}`;
+
+export const demoMechDirectory: MechDirectory = {
+ chainId: 8453,
+ status: "ready",
+ source: "olas-mech-client",
+ observedAt,
+ observedVersion: "demo-snapshot-2026-09-17",
+ mechs: [
+ {
+ chainId: 8453,
+ address: "0x21a091b000000000000000000000000000000001",
+ serviceId: 218,
+ factoryAddress: "0x1000000000000000000000000000000000000001",
+ name: "Gaia Risk Mech",
+ description: "Independent market and protocol risk analysis.",
+ metadataCid: "bafybeigaia-risk-mech-demo",
+ paymentType: "native",
+ unitAmount: 6_200_000,
+ paymentDecimals: 6,
+ totalDeliveries: 1284,
+ health: "active",
+ eligible: true,
+ compatibilityScore: 99,
+ reasons: [],
+ tools: [
+ {
+ name: "risk-analysis/v3",
+ description: "Structured Base and Aave risk recommendation.",
+ inputSchema: { type: "object", required: ["asset", "amount"] },
+ outputSchema: {
+ type: "object",
+ required: ["action", "confidenceBps", "riskScore"],
+ },
+ schemaHash: schemaHash("a"),
+ },
+ ],
+ observedAt,
+ observedVersion: "olas-base-2026.09",
+ },
+ {
+ chainId: 8453,
+ address: "0xa8b1100000000000000000000000000000000002",
+ serviceId: 431,
+ factoryAddress: "0x1000000000000000000000000000000000000001",
+ name: "Valory DeFi Analyst",
+ description: "Yield quality and liquidity analysis for DeFi positions.",
+ metadataCid: "bafybeivalory-defi-demo",
+ paymentType: "native",
+ unitAmount: 5_900_000,
+ paymentDecimals: 6,
+ totalDeliveries: 954,
+ health: "active",
+ eligible: true,
+ compatibilityScore: 97,
+ reasons: [],
+ tools: [
+ {
+ name: "yield-review/v2",
+ description: "Structured yield sustainability recommendation.",
+ inputSchema: { type: "object", required: ["protocol", "asset"] },
+ outputSchema: {
+ type: "object",
+ required: ["action", "confidenceBps", "riskScore"],
+ },
+ schemaHash: schemaHash("b"),
+ },
+ ],
+ observedAt,
+ observedVersion: "olas-base-2026.09",
+ },
+ {
+ chainId: 8453,
+ address: "0xf3c4200000000000000000000000000000000003",
+ serviceId: 577,
+ factoryAddress: "0x1000000000000000000000000000000000000001",
+ name: "Sentinel Markets",
+ description: "Market anomaly detection with an outdated output schema.",
+ metadataCid: "bafybeisentinel-markets-demo",
+ paymentType: "native",
+ unitAmount: 6_500_000,
+ paymentDecimals: 6,
+ totalDeliveries: 721,
+ health: "degraded",
+ eligible: false,
+ compatibilityScore: 62,
+ reasons: [
+ {
+ code: "SCHEMA_DRIFT",
+ message: "The current tool omits the required expiry field.",
+ },
+ ],
+ tools: [
+ {
+ name: "market-risk/v1",
+ description: "Legacy market-risk response.",
+ inputSchema: { type: "object" },
+ outputSchema: { type: "object" },
+ schemaHash: schemaHash("c"),
+ },
+ ],
+ observedAt,
+ observedVersion: "olas-base-2026.09",
+ },
+ ],
+};
+
+export const demoActivity = [
+ {
+ id: "evt-proof",
+ time: "12:04:18",
+ label: "Proof bundle PRF-1041 sealed",
+ status: "VERIFIED",
+ href: "/verify/PRF-1041",
+ },
+ {
+ id: "evt-receipt",
+ time: "12:03:51",
+ label: "KeeperHub receipt KH-8831 confirmed",
+ status: "FINAL",
+ href: "/app/executions/KH-8831",
+ },
+ {
+ id: "evt-quorum",
+ time: "12:02:07",
+ label: "Deterministic quorum reached 2 / 2",
+ status: "PASSED",
+ href: "/app/intents/SYN-1041",
+ },
+ {
+ id: "evt-olas",
+ time: "11:59:42",
+ label: "Two Olas deliveries normalized",
+ status: "READY",
+ href: "/app/mechs",
+ },
+] as const;
diff --git a/apps/web/src/lib/mech-data.ts b/apps/web/src/lib/mech-data.ts
index 4a3953f..25cc64b 100644
--- a/apps/web/src/lib/mech-data.ts
+++ b/apps/web/src/lib/mech-data.ts
@@ -6,15 +6,16 @@ import {
} from "@synesis/domain";
import type { ViewState } from "@synesis/ui";
+import { demoMechDirectory } from "./demo-data";
+
const apiOrigin = () =>
process.env.SYNESIS_API_INTERNAL_URL ?? "http://localhost:4000";
export async function loadMechDirectory(): Promise> {
if (process.env.SYNESIS_MODE !== "live") {
return {
- status: "empty",
- message:
- "Live Olas discovery is disabled in demo mode. No provider records have been fabricated.",
+ status: "ready",
+ data: demoMechDirectory,
};
}
try {
@@ -56,11 +57,17 @@ export async function loadMech(
): Promise> {
if (!/^0x[0-9a-fA-F]{40}$/u.test(address))
return { status: "empty", message: "That is not a full EVM Mech address." };
- if (process.env.SYNESIS_MODE !== "live")
- return {
- status: "empty",
- message: "Live Mech profiles are disabled in demo mode.",
- };
+ if (process.env.SYNESIS_MODE !== "live") {
+ const demoMech = demoMechDirectory.mechs.find(
+ (mech) => mech.address.toLowerCase() === address.toLowerCase(),
+ );
+ return demoMech
+ ? {
+ status: "ready",
+ data: demoMech,
+ }
+ : { status: "empty", message: "Mech not found in the demo snapshot." };
+ }
try {
const response = await fetch(
new URL(`/api/v1/mechs/${encodeURIComponent(address)}`, apiOrigin()),
diff --git a/docs/SYNESIS_DEMO_SCRIPT.html b/docs/SYNESIS_DEMO_SCRIPT.html
new file mode 100644
index 0000000..3a03302
--- /dev/null
+++ b/docs/SYNESIS_DEMO_SCRIPT.html
@@ -0,0 +1,374 @@
+
+
+
+
+ Synesis Demo Script
+
+
+
+
+ SYNESIS 4-MINUTE JUDGE DEMO
+
+ Paid intelligence. Proven execution.
+
+ One-sentence story: Synesis pays two independent Olas
+ Mechs for structured analysis, applies deterministic quorum, uses
+ KeeperHub as the only value-moving path, and publishes a proof connecting
+ the original intent to the verified Base and Aave outcome.
+
+
+ Pre-demo setup
+
+
+ Open before presenting
+ Expected state
+
+
+ https://synesis-web.vercel.app/app
+ DEMO / NO VALUE badge and BACKEND CONNECTED indicator
+
+
+ /app/intents/new
+ Two eligible demo Mechs available
+
+
+ /verify/PRF-1041
+ HASH VALID and canonical JSON download
+
+
+ /api/v1/status
+ synesis-api status: ok
+
+
+
+ Live narration
+
+
0:00–0:25
+
+
Hook
+
+ “Agents can recommend, and wallets can transact. The dangerous gap is
+ deciding when an agent’s recommendation deserves to move real value.
+ Synesis closes that gap.”
+
+
+ DO: Show the landing page. Point to Olas
+ intelligence, KeeperHub execution, and Base settlement.
+
+
+
+
+
+
0:25–0:55
+
+
Prove this is a product
+
+ “This is a connected operating surface, not a one-page prototype. The
+ command center links intents, agents, policies, KeeperHub executions,
+ treasury exposure, integrations, security, and public proofs.”
+
+
+ DO: Open the command center. Point to BACKEND
+ CONNECTED, the environment badge, primary navigation, and live
+ activity drawer.
+
+
+
+
+
+
0:55–1:35
+
+
Create the economic intent
+
+ “I’ll supply 2.5 USDC to Aave on Base. Before anything moves, Synesis
+ freezes the maximum principal, expiry, active policy, and exactly two
+ independent Mechs. Their addresses, metadata CIDs, and tool-schema
+ hashes become immutable evidence.”
+
+
+ DO: Open New intent. Enter
+ 2500000 . Select Gaia Risk Mech and Valory
+ DeFi Analyst. Click Freeze & create intent .
+
+
+
+
+
+
1:35–2:10
+
+
Show the KeeperHub boundary
+
+ “The Mechs produce evidence; they never produce executable calldata.
+ The deterministic policy checks quorum, confidence, risk, freshness,
+ amount caps, and independence. KeeperHub is the only component allowed
+ to simulate, sign, and broadcast.”
+
+
+ DO: In the Intent Room, click
+ Simulate exact call . Show the returned simulation
+ hash and demo-no-broadcast guard. Then
+ click Approve execution and point out the idempotent
+ state transition.
+
+
+
+
+
+
2:10–2:40
+
+
Explain the live integration
+
+ “In live mode, KeeperHub first pays for the Olas requests. Confirmed
+ Olas delivery events trigger a saved KeeperHub workflow and signed
+ webhook. After quorum, KeeperHub broadcasts the unchanged Aave call
+ using a stable economic idempotency key.”
+
+
+ DO: Open Mech marketplace, then Execution ledger.
+ Point to the Olas payment, Aave supply, confirmed and unconfirmed
+ states.
+
+
+
+
+
+
2:40–3:10
+
+
Show fail-closed controls
+
+ “Synesis never treats HTTP acceptance or a transaction hash as
+ success. KeeperHub’s verified receipt, an independently read Base
+ receipt, and the Aave position delta must all agree. Any mismatch
+ freezes the intent as unconfirmed.”
+
+
+ DO: Open Treasury, Integrations, then Security.
+ Engage and release the emergency pause to demonstrate that the backend
+ blocks new economic writes.
+
+
+
+
+
+
3:10–3:45
+
+
Deliver the proof
+
+ “This is the differentiator: PRF-1041 connects the frozen cause, paid
+ agent intelligence, deterministic policy, KeeperHub simulation and
+ execution, receipt, and final Aave state. The verifier recalculates
+ every content hash and the ordered root independently.”
+
+
+ DO: Open /verify/PRF-1041 .
+ Show HASH VALID, evidence entries, public BaseScan reference, and
+ download the canonical JSON.
+
+
+
+
+
+
3:45–4:00
+
+
Close
+
+ “Synesis turns agent output into accountable economic action:
+ independent intelligence, deterministic authorization, KeeperHub-only
+ value movement, and proof anyone can verify.”
+
+
+ DO: Return to the command center and leave the
+ backend-connected status visible.
+
+
+
+
+ Evidence checklist
+
+
+ Claim
+ Proof to show
+
+
+ Multipage working product
+ Connected navigation and interactive intent room
+
+
+ Frontend/backend integration
+
+ /api/v1/status , intent POST, simulation
+ response
+
+
+
+ KeeperHub value path
+
+ Execution ledger + architecture; live evidence only after authorized
+ acceptance run
+
+
+
+ Olas integration
+ Mech addresses, schema versions, request/delivery lifecycle
+
+
+ Public verification
+ PRF-1041 hash recalculation and JSON download
+
+
+
+ Guardrails for the presenter
+
+
+ Always name the visible environment: DEMO / NO VALUE or
+ LIVE / BASE MAINNET .
+
+
+ Use “simulated” for demo responses. Use “executed” only with a verified
+ live receipt.
+
+
+ Never present seeded IDs, balances, or PRF-1041 as evidence of a new
+ live hackathon transaction.
+
+
+ If a live dependency fails, show the failure honestly and continue with
+ the deterministic demo path.
+
+
+
+
+ Synesis · KeeperHub Agent Economy Hackathon ·
+ github.com/EcstaceeLOR/Synesis · synesis-web.vercel.app
+
+
+
diff --git a/docs/SYNESIS_PITCH_DECK.html b/docs/SYNESIS_PITCH_DECK.html
new file mode 100644
index 0000000..685d5e1
--- /dev/null
+++ b/docs/SYNESIS_PITCH_DECK.html
@@ -0,0 +1,655 @@
+
+
+
+
+ Synesis — KeeperHub Agent Economy Hackathon
+
+
+
+
+
+
+
KeeperHub Agent Economy Hackathon
+
+
+ Paid intelligence.Proven execution.
+
+
+ The procurement and execution layer where independent Olas agents
+ produce evidence, deterministic policy authorizes action, and KeeperHub
+ moves bounded value on Base.
+
+ synesis-web.vercel.app
+
+
+
+
+
+ The problem Autonomy without proof is a liability
+
+
+ Agents can recommend. Wallets can transact. The missing layer is
+ accountable execution.
+
+
+
+ 01
+ Single-agent risk
+
+ One probabilistic answer can be stale, manipulated, or simply wrong.
+
+
+
+ 02
+ Unsafe handoff
+
+ Free-form model output is too often converted directly into
+ transaction intent.
+
+
+
+ 03
+ No audit chain
+
+ A transaction hash alone does not prove why value moved or whether
+ the result matched policy.
+
+
+
+
+ The agent economy needs a control plane that can pay for intelligence,
+ verify consensus, and prove every economic consequence.
+
+
+
+
+
+
+ The solution Agents produce evidence. Policies authorize action.
+
+ One deterministic lifecycle from question to receipt.
+
+
+ 01 / INTENT
+ Bound exposure
+
+ Freeze strategy, amount, expiry, Mechs, policy, and maximum spend.
+
+
+
→
+
+ 02 / PROCURE
+ Buy intelligence
+
+ KeeperHub pays two independent Olas Mechs for structured analysis.
+
+
+
→
+
+ 03 / DECIDE
+ Reach quorum
+
+ A network-isolated policy engine checks confidence, risk, freshness,
+ and caps.
+
+
+
→
+
+ 04 / EXECUTE
+ Move value
+ KeeperHub simulates and broadcasts the unchanged Aave call.
+
+
→
+
+ 05 / PROVE
+ Seal evidence
+
+ Receipt, onchain state delta, and ordered content hashes become a
+ public proof.
+
+
+
+
+
+
+
+
+ Why KeeperHub The execution authority
+
+
+ KeeperHub is not a badge. It is the only path that can move value.
+
+
+
+ Olas triggers KeeperHub
+
+ Confirmed Mech delivery events trigger a saved KeeperHub workflow
+ that sends a signed, replay-protected webhook into Synesis.
+
+
+ Live external project trigger
+ Onchain request and delivery correlation
+ Idempotent event inbox
+
+
+
+ Synesis consumes KeeperHub
+
+ Every approval and Aave supply is simulated and executed by the
+ KeeperHub organization wallet using stable economic idempotency
+ keys.
+
+
+ Exact simulation-to-broadcast hash binding
+ Target, selector, chain, and amount allowlists
+ Verified receipt plus independent Base confirmation
+
+
+
+
+ KeeperHub executes the value movement that Olas intelligence unlocks—and
+ Synesis makes the reason independently verifiable.
+
+
+
+
+
+
+ Architecture Production-shaped, fail-closed
+
+ Clear trust boundaries. Durable state. No hidden signer.
+
+
+
+ Next.js product Multipage app + public
+ verifier
+
+
+ Fastify API Auth, validation, approvals, reads
+
+
+ PostgreSQL + Redis Lifecycle state + durable
+ jobs
+
+
+
+
+ Coordinator Persisted state machine
+
+
+ Policy engine No network, no LLM
+
+
+ Proof assembler Canonical evidence root
+
+
+
+
+ Olas adapter Official Mech client, no private
+ key
+
+
+ KeeperHub gateway Only signing + broadcast
+ path
+
+
+ Base + Aave V3 Independent receipt and state
+ delta
+
+
+
+
+
+
0
+
private keys stored
+
+
+
2/2
+
independent Mechs required
+
+
+
3
+
proofs must agree
+
+
+
1
+
economic execution per intent
+
+
+
+
+
+
+
+ Product A real operating surface, not a landing-page prototype
+
+ Every entity connects to the evidence around it.
+
+
+ Command center
+
+ Health, active intents, approvals, execution activity, and proof
+ count.
+
+
+
+ Intent room
+
+ Create, freeze, simulate, approve, cancel, and follow the lifecycle.
+
+
+
+ Mech marketplace
+
+ Compatibility, price, tool schema, delivery history, and
+ eligibility.
+
+
+
+ Execution ledger
+ Simulation, broadcast, receipt, retry identity, and finality.
+
+
+ Treasury
+
+ Wallet balances, Aave position, prepaid intelligence, and exposure.
+
+
+
+ Policies
+ Immutable versions, canonical rules, content hashes, and caps.
+
+
+ Security
+ Allowlist, approval threshold, sessions, and emergency pause.
+
+
+ Public proofs
+ Independent recalculation and canonical JSON download.
+
+
+
+
+
+
+
+ Proof, not promises PRF-1041
+
+ One evidence bundle connects the complete economic story.
+
+
+
+
CANONICAL ROOT
+
+ Intent → Mech requests → deliveries → policy → simulation →
+ execution → receipt → Aave delta
+
+
HASH VALID
+
+
+
+
+ Frozen cause — strategy, amount, expiry, tool
+ schemas, policy version
+
+
+ Paid intelligence — Olas request IDs, CIDs,
+ delivery events, normalized responses
+
+
+ Deterministic authorization — every rule and
+ result, not a model opinion
+
+
+ KeeperHub execution — simulation hash, execution
+ ID, verified receipt
+
+
+ Independent outcome — Base receipt and Aave
+ position delta
+
+
+
+ synesis-web.vercel.app/verify/PRF-1041
+
+
+
+
+
+ Safety and reliability Value movement deserves stronger guarantees
+
+ Designed to fail closed at every dangerous boundary.
+
+
+ Before execution
+
+ Base-only manifest
+ Contract + selector allowlist
+ Hard principal and procurement caps
+ Fresh approval and emergency pause
+
+
+
+ During execution
+
+ Exact payload simulation
+ Stable idempotency keys
+ No raw calldata signer
+ At-least-once jobs, once-only economics
+
+
+
+ After execution
+
+ Verified KeeperHub receipt
+ Independent Base receipt
+ Aave position delta
+ Public redacted proof root
+
+
+
+
+ No mock transaction is ever labeled live. No HTTP acceptance is ever
+ labeled success. No transaction hash is accepted without a verified
+ outcome.
+
+
+
+
+
+
+ Why Synesis wins Integration depth + product completeness
+
+ The missing operating system for the agent economy.
+
+
+ UNIQUE LOOP
+ Agents unlock—not control—value
+
+ Independent paid intelligence becomes evidence inside a
+ deterministic authorization system.
+
+
+
+ REAL INTEGRATION
+ KeeperHub both reacts and executes
+
+ Olas delivery triggers a KeeperHub workflow; KeeperHub then performs
+ the bounded Aave write.
+
+
+
+ JUDGEABLE PROOF
+ Every claim is inspectable
+
+ Interactive multipage product, source code, API health, receipts,
+ and public canonical evidence.
+
+
+
+
+ Synesis turns agent output into accountable economic action.
+
+ github.com/EcstaceeLOR/Synesis · synesis-web.vercel.app
+
+
+
+