diff --git a/.github/workflows/offline-verification.yml b/.github/workflows/offline-verification.yml new file mode 100644 index 0000000..66c07b0 --- /dev/null +++ b/.github/workflows/offline-verification.yml @@ -0,0 +1,113 @@ +name: Phase 0 and Wallet Kernel offline verification + +on: + pull_request: + paths: + - 'phase0/**' + - 'prototype/**' + - 'spikes/pi-wielder/**' + - '.claude/skills/optimizing-claude-code-prompts/**' + - '.github/workflows/offline-verification.yml' + push: + branches: [main] + paths: + - 'phase0/**' + - 'prototype/**' + - 'spikes/pi-wielder/**' + - '.claude/skills/optimizing-claude-code-prompts/**' + - '.github/workflows/offline-verification.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + phase0: + name: Phase 0 provenance CLI + runs-on: ubuntu-24.04 + timeout-minutes: 20 + defaults: + run: + working-directory: phase0 + steps: + - name: Check out reviewed source + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + + - name: Install Node 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22.22.0 + cache: npm + cache-dependency-path: phase0/package-lock.json + + - name: Install locked dependencies + run: npm ci + + - name: Run Phase 0 tests + run: npm test + + - name: Typecheck Phase 0 + run: npm run typecheck + + - name: Verify the Phase 0 checkout stayed clean + working-directory: . + shell: bash + run: | + set -euo pipefail + git diff --check + test -z "$(git status --porcelain)" + + wallet-kernel: + name: Wallet Kernel deterministic evidence + runs-on: ubuntu-24.04 + timeout-minutes: 30 + defaults: + run: + working-directory: spikes/pi-wielder + steps: + - name: Check out reviewed source + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + + - name: Install exact Wallet Kernel Node runtime + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24.18.1 + cache: npm + cache-dependency-path: spikes/pi-wielder/package-lock.json + + - name: Install locked dependencies + run: npm ci + + - name: Run the complete offline spend-control story + run: npm run verify:spend-control + + - name: Build and independently verify deterministic evidence + shell: bash + run: | + set -euo pipefail + evidence_parent="$(mktemp -d "${RUNNER_TEMP}/pi-wielder-evidence.XXXXXX")" + npm run evidence:offline -- \ + --output "${evidence_parent}/bundle" \ + --anchor-output "${evidence_parent}/manifest.sha256" + manifest_sha256="$(tr -d '\n' < "${evidence_parent}/manifest.sha256")" + npm run evidence:verify -- "${evidence_parent}/bundle" \ + --expect-manifest-sha256 "${manifest_sha256}" + + - name: Reject tracked secrets + run: npm run verify:no-secrets + + - name: Run shared economics regressions + working-directory: prototype + run: npm test + + - name: Verify the checkout stayed clean + working-directory: . + shell: bash + run: | + set -euo pipefail + git diff --check + test -z "$(git status --porcelain)" diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml new file mode 100644 index 0000000..008fa56 --- /dev/null +++ b/.github/workflows/site.yml @@ -0,0 +1,52 @@ +name: Site verification + +on: + pull_request: + paths: + - 'README.md' + - 'site/**' + - '.github/workflows/site.yml' + push: + branches: [main] + paths: + - 'README.md' + - 'site/**' + - '.github/workflows/site.yml' + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + defaults: + run: + working-directory: site + env: + NEXT_TELEMETRY_DISABLED: '1' + steps: + - name: Check out reviewed source + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Install exact Node runtime + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22.23.2 + cache: npm + cache-dependency-path: site/package-lock.json + + - name: Install locked dependencies + run: npm ci + + - name: Run dependency-free behavior tests + run: npm test + + - name: Lint site source + run: npm run lint + + - name: Build production site + run: npm run build + + - name: Reject high and critical dependency advisories + run: npm audit --audit-level=high diff --git a/README.md b/README.md index b401027..afa7a1a 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,127 @@ -![NEVER HANDED OVER — Skill Asset Protocol](assets/github-banner.png) - # Skill Asset Protocol -A compensation, attribution, and metering layer for authored AI **Skills** — "Carta for AI -work artifacts." Creators keep a durable economic claim each time others use their Skill, -instead of handing the value over once under work-for-hire. +## Agent Spend Control Plane -This is a **research/spike repository**: design documents plus executable evidence, not a -product. Every claim below is labeled measured, modeled, or hypothesis. +Give AI agents a wallet without giving them the keys. -**The manifesto is live — and it is a working protocol endpoint: -[neverhandedover.com](https://neverhandedover.com)** (also at -[skillassetprotocol.com](https://skillassetprotocol.com); source in [`site/`](site/)). +The approved commercial direction is a customer-hosted **Agent Spend Control +Plane**. Its central module, the **Wallet Kernel**, turns an Agent's ordinary +HTTP request into a bounded Spend Intent, evaluates customer policy, obtains +exact human approval when required, signs only an authorized x402 payment through +a customer-owned wallet, and records a signed receipt for every outcome. -## The demo result +This remains a **design-and-spike repository**, not a production product. The +current Wallet Kernel evidence is offline and deterministic. The networked mode +is constrained by design to test USDC on Base Sepolia but has **not been run**; +mainnet and real funds are unsupported. See the +[approved design](docs/superpowers/specs/2026-07-31-agent-spend-control-plane-design.md) +for the complete product, security, and acceptance boundaries. -On 2026-07-12, one wallet paid per model call **and** per hosted-skill invocation over x402 -(Base Sepolia, real facilitator, real USDC), landing both asset classes in one attributed -session ledger: +The commercial v1 is spending policy, auditability, and reconciliation: -``` -claude/plan $0.041 · skill $0.25 → creator $0.24375 / treasury $0.00625 +- customer-hosted policy enforcement and authoritative records; +- a customer-owned wallet, with CDP as the first adapter; +- default-deny budgets, allow-listed sellers, and exact one-time approvals; +- x402 v2 `exact` payments only; +- durable recovery and signed receipts for settled, failed, refunded, and + unresolved outcomes. + +It is not wallet custody, token trading, an inference reseller, or a +marketplace. Skill attribution and Creator compensation are deferred expansion +modules: they may later consume Wallet Kernel receipts, but they do not define +the v1 operator experience. + +## Pre-release website candidate + +The source in [`site/`](site/) is an offline, deterministic candidate preview of +the wallet-control product. It demonstrates policy loading, automatic allow, +deny, exact approval, deliberate retry, and an unsigned session projection. It +does not connect a wallet, sign a receipt, or broadcast a transaction. + +The approved design's release gate remains binding: this candidate must not +replace the public homepage until the required implementation and fresh, +recomputable evidence qualify. The separate `/proof` route is a static archive +with one narrowly supported historical Base Sepolia receipt; the website no +longer exposes a wallet or paid invocation endpoint. + +### Run the website locally + +Use Node 22, then run from the repository root: + +```bash +cd site +npm ci +npm run dev ``` -On-chain balances reconciled to the cent (Wielder 20 → 19.668 USDC; sellers received exactly -0.332). Measured x402 payment overhead across 48 settled calls (2026-07-15, two model providers): -**p50 731 ms / p95 1206 ms per paid call** — facilitator verify/settle is nearly all of it -(p50 729 ms); the 402 roundtrip + EIP-3009 signature add ~2 ms. -Details and txHashes: [`spikes/pi-wielder/README.md`](spikes/pi-wielder/README.md). +Open . The homepage needs no environment file, account, +wallet, API key, network request, or payment. -## Try it offline — zero keys, zero funds +## First commercial offer -All four proofs run with no API keys, no network payments, and no wallet. Where a -`package-lock.json` exists, `npm ci` is the reproducible choice. +The first paid offer is a customer-hosted design-partner pilot for one Pi +workflow, one customer-owned CDP testnet wallet, and one or more allow-listed +Base Sepolia x402 resource servers. The pilot adds customer-defined automatic +and approval-required policy, durable budgets and restart recovery, a local +operator console, signed receipt and reconciliation export, and a final control +review. -| Where | Commands | Proves | +This pilot is intended to test whether an AI platform or gateway team will pay +for governed autonomous spending. That demand has not yet been validated. + +## Evidence status + +One public historical claim is supported by the retained evidence manifest: +on 2026-07-12, a successful Base Sepolia transaction transferred `250000` +atomic units (`0.25`) of test USDC, and the repository's historical run log +labels it as the Skill-leg settlement. See the +[receipt manifest](spikes/pi-wielder/evidence/2026-07-12-skill-settlement/manifest.json). + +That receipt does **not** prove current endpoint behavior, latency, +Royalty claim split correctness, Skill execution output, independent demand, or +production readiness. A later aggregate timing summary is quarantined and is +not used in product claims because its normalized samples were not retained; +see its +[non-publishable manifest](spikes/pi-wielder/evidence/2026-07-15-overhead/manifest.json). + +All current product-preview states and receipts are deterministic simulations. +A future testnet product claim requires a fresh, immutable, recomputable +evidence bundle as specified in the approved design. + +## Try the existing proofs offline + +The repository also retains earlier attribution, settlement, provenance, and +clone-economics research. These are inputs to the Wallet Kernel work or deferred +research; they are not proof that the commercial v1 is complete. + +| Where | Command | Scope | |---|---|---| -| `spikes/pi-wielder` | `npm install && npm run e2e` | 20 checks: 402-first on every leg, no skill-content leak, replay rejection, exact split match | -| `spikes/clone-economics` | `npm run e2e` (no install) | 97 checks: deterministic clone-distillation harness, byte-identical reruns | -| `prototype` | `node spike-fork-economics.mjs` (no install) | 64 invariants: fork/royalty economics on the settlement engine | -| `phase0` | `npm install && npm test` | 18 tests: Story provenance registration against injected fakes | +| `site` | `npm test` | Deterministic wallet-control preview and public-claim guards | +| `spikes/pi-wielder` | `npm ci && npm test && npm run e2e` | Offline wallet policy, x402, journal, receipt, and failure-path spike | +| `spikes/clone-economics` | `npm run e2e` | Deterministic clone-distillation research | +| `prototype` | `npm test` | Settlement and attribution accounting invariants | +| `phase0` | `npm ci && npm test` | Story provenance behavior against injected fakes | + +These automated paths require no funded wallet or network payment. Follow each +directory's README for its exact environment and safety boundary. ## What's here -- **`CONTEXT.md`** — the ubiquitous language: Skill, Creator, Wielder, Beneficiary, Collar, - Invocation, Derivative, Royalty claim. -- **`docs/adr/`** — 8 decision records, including 0007 (the closed-mode compensation layer is - the terminal product) and 0008 (the Wielder is a wallet, not a harness). -- **`spikes/`** — pi-wielder (one wallet, two asset classes, unified ledger) and - clone-economics (how cheaply can N paid outputs be distilled into a clone?). -- **`prototype/`** — the settlement engine (pure logic) plus fork-economics and CMA-latency +- **`docs/superpowers/specs/2026-07-31-agent-spend-control-plane-design.md`** + — the approved Wallet Kernel and commercial-pilot design. +- **`site/`** — the pre-release offline wallet-control candidate and a separate + static historical proof archive. +- **`spikes/pi-wielder/`** — the hardened wallet, policy, x402, journal, + receipt, refund, and reconciliation evidence that the Wallet Kernel will + evolve from. +- **`CONTEXT.md` and `docs/adr/`** — the canonical protocol language and prior + decisions. The compensation and attribution model remains longer-term + research rather than the v1 product interface. +- **`prototype/`** — settlement and attribution accounting logic plus economic spikes. -- **`phase0/`** — Story Protocol provenance: register a Skill as an IP Asset and declare - Derivatives (Aeneid testnet only). -- **`.claude/skills/` and `.agents/skills/`** — the bundled example skill - (`optimizing-claude-code-prompts`) is **intentionally public**. The protocol's claim is that - a Wielder never receives the skill at runtime — output only — not that the skill is secret. - The spikes host and meter this exact skill behind the Collar. - -## Evidence status - -The repo's discipline is to label every number: - -- **Measured (real network, n=48 settled calls + a live pi session):** the runs above — real - 402 → sign → settle, real USDC per call, splits credited by the settlement engine, wallet - reconciled on-chain to the cent; two failure modes documented (pay-then-fail, settled-but-rejected). -- **Measured (n=3, one model):** hosted-agent cold start — first answer token p50 ~2.5 s; - pay-then-run-async reads as usable on top of the ~0.8 s testnet payment gate. -- **Measured (N=6, small fixtures):** the clone attack **failed on fidelity** — all 6 held-out - cases failed critical gates — but modeled break-even is **8 invocations** if a clone ever - passes. Cost is no defense; fidelity was. High-N behavior is **unknown**. -- **Modeled (deterministic arithmetic, not observed behavior):** education-mode flow-through - is dominated by free re-authoring — every school-paying royalty rate loses at parity, so - Education mode is deferred. -- **Validated arithmetically:** settlement splits, multi-level derivative flow-through, and - payment gating ("no credential, no run") via the engine's invariants. - -Unvalidated: that employers will buy this. Design-partner interviews remain the open step. +- **`phase0/`** — Story Protocol provenance experiments on Aeneid testnet only. +- **`.claude/skills/` and `.agents/skills/`** — the intentionally public example + Skill used by earlier spikes. ## License diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..02580c5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +# Documentation + +## Current v1 product design + +- [Agent Spend Control Plane — approved design](superpowers/specs/2026-07-31-agent-spend-control-plane-design.md) + — the current wallet-native direction: a customer-hosted **Wallet Kernel** + for x402 spending policy, exact approvals, signed receipts, and reconciliation. + +## Deferred expansion research + +- [Employer onboarding, retention, and monetization recommendations](product-onboarding-retention-and-monetization.md) + — superseded for v1 and retained as research for a possible future Skill + attribution and Creator-compensation module. + +## Security + +- [Dependency security audit](dependency-security-audit.md) + +## Architecture decisions + +- [ADR-0001 — Hosted invocation-rights](adr/0001-skills-as-hosted-invocation-rights.md) +- [ADR-0002 — Story programmable IP](adr/0002-tokenize-skills-as-programmable-ip-on-story.md) +- [ADR-0003 — Payment-gated execution](adr/0003-payment-gated-execution.md) +- [ADR-0004 — Economic and network moats](adr/0004-compete-on-moats-not-secrecy.md) +- [ADR-0005 — Two-leg settlement](adr/0005-two-leg-cross-chain-settlement.md) +- [ADR-0006 — Closed modes first](adr/0006-phased-rollout-closed-modes-first.md) +- [ADR-0007 — Closed-mode compensation as the terminal product](adr/0007-closed-mode-compensation-layer-as-terminal-product.md) +- [ADR-0008 — The Wielder is a wallet](adr/0008-the-wielder-is-a-wallet.md) diff --git a/docs/dependency-security-audit.md b/docs/dependency-security-audit.md new file mode 100644 index 0000000..c8c2d6e --- /dev/null +++ b/docs/dependency-security-audit.md @@ -0,0 +1,115 @@ +# Dependency security audit + +- Original dependency audit: 2026-07-26 +- Updated for the spend-control site revision: 2026-08-02 + +Scope: the Next.js application in `site/`, including production and development +dependencies, the image optimizer path, and deployment-adjacent server code. + +## Executive summary + +- The 2026-07-26 install reported 12 high-severity findings before remediation. +- At that verified dependency snapshot, `npm audit --omit=dev` reported **0 + vulnerabilities**. +- At that snapshot, the full development tree reported 9 high-severity package + entries. They all traced to one `brace-expansion` denial-of-service advisory + in the ESLint tool chain, not nine independent defects. +- `npm audit fix --force` was rejected because npm proposes incompatible major + changes and invalid historical downgrades of the Next.js lint configuration. + +Those dependency-audit results are a historical verification record. The +current PR refreshed the lockfile on 2026-08-02 and now resolves the legacy +`brace-expansion` path to patched 1.1.18 and the modern path to patched 5.0.9. +The clean install reported zero vulnerabilities. Fresh tests and lint pass; +the normal-environment production build and full audit also pass at the current +pre-merge head. Final merged-head CI remains a release gate. + +## Remediation applied + +| Dependency | Before | After | Reason | +|---|---:|---:|---| +| `next` | 15.5.20 | 15.5.22 | Clears the direct Next.js advisories while staying on the existing release line. | +| `eslint-config-next` | 15.5.20 | 15.5.22 | Keeps framework and lint configuration aligned. | +| `postcss` | vulnerable transitive | 8.5.23 override | Pulls the patched same-major parser into the production tree. | +| `sharp` | 0.34.5 transitive | 0.35.3 override | Pulls the patched image processor; requires Node 20.9 or newer. | +| `brace-expansion` | 1.1.16 and 5.0.8 | 1.1.18 and 5.0.9 | Clears the high-severity development-only denial-of-service advisory on both dependency paths. | + +The project requires Node `22.x` so local and Vercel builds use the same +supported major and satisfy Sharp's runtime requirement. CI pins +[Node 22.23.2](https://nodejs.org/en/blog/release/v22.23.2), the Node 22 LTS +security release published on 2026-07-29. + +The PostCSS and Sharp overrides are temporary compatibility controls. Remove +them when the selected Next.js release declares patched versions directly. +Sharp 0.35 is outside Next 15.5.22's declared `^0.34.3` range, so it received an +explicit build, runtime, and image-optimization smoke test before release. + +## Resolved development advisory + +Advisory: +[GHSA-mh99-v99m-4gvg](https://github.com/advisories/GHSA-mh99-v99m-4gvg), +unbounded brace expansion causing an out-of-memory denial of service. + +Observed path: legacy `brace-expansion` 1.x through `minimatch` 3.x in ESLint, +`@eslint/eslintrc`, and plugins bundled by `eslint-config-next`. The advisory +was updated to identify patched 1.1.17; the refreshed lockfile resolves 1.1.18. +The modern nested path also advanced from 5.0.8 to 5.0.9. No forced downgrade, +unsupported major change, or direct dependency was introduced. + +## Website attack-surface reduction during review + +- The hosted Skill seller API, browser-wallet client, and payment-enablement + environment variables were removed. The website has no signing, facilitator, + transaction-broadcast, or model-provider path. +- `/proof` is a static archive separated from the Wallet Kernel candidate. Its + single historical Base Sepolia receipt is bounded evidence, not proof of a + currently live endpoint, successful model execution, a completed split, + customer demand, or production readiness. +- The archived manifesto explicitly says that the historical interface did not + directly return the Skill file while model output can still leak or enable + reconstruction. It makes no categorical extraction-resistance claim. +- The spend-control sandbox is an offline deterministic state machine. It uses + no account, wallet, key, signature, network call, payment, saved data, or raw + prompt and labels its final artifact as an unsigned session projection. +- Global responses disable the framework signature and add CSP frame/base/object + restrictions, clickjacking protection, MIME sniffing protection, a strict + referrer policy, and a restrictive permissions policy. + +## Verification records + +Current PR, 2026-08-02: + +- User-run `npm ci`: 325 packages installed; install-time audit reported 0 + vulnerabilities. +- `npm test`: 24/24 passing. This dependency-free suite covers the Wallet + Kernel allow/deny/approval/retry sandbox, unsigned projection boundaries, the + default-deny path, safe-integer atomic-money conservation, and public-claim + quarantine across the website and READMEs. +- `npm run lint`: passing under Node 22.22.0 in the agent environment. +- User-run `npm run build` under Node 22.22.0: passing; Next.js 15.5.22 + compiled, checked types, generated all static pages, and emitted `/`, + `/_not-found`, `/icon.png`, and `/proof`. +- User-run `npm audit --audit-level=high`: 0 vulnerabilities. +- `.github/workflows/site.yml` now runs locked install, tests, lint, production + build, and a high/critical full-tree dependency audit for every pull request + that changes `site/**`, the root `README.md` scanned by the claim-quarantine + test, or the workflow itself. It uses immutable action pins and Node 22.23.2. +- The CI audit command is `npm audit --audit-level=high`: high and critical + advisories in production or development dependencies fail the job; low and + moderate advisories remain visible without failing this gate. +- The sandboxed Turbopack build reached optimization but could not bind its + local helper port (`EPERM`); the same command passed in the user's normal + environment. Production start, image optimization, and final merged-head CI + remain **pending final-head verification**. + +Previous dependency snapshot, 2026-07-26: + +- `npm test`: 6/6 passing. +- `npm run lint`: passing. +- Clean `npm run build`: passing in an isolated copy to avoid the active local + development server rewriting `.next` concurrently. +- Production start smoke test: passing. +- Next image optimizer request for `/icon.png` at 64 px: HTTP 200, `image/png`. +- `npm audit --omit=dev`: 0 vulnerabilities. +- Full `npm audit`: 9 high package entries, all from the single dev-only advisory + described above. diff --git a/docs/product-onboarding-retention-and-monetization.md b/docs/product-onboarding-retention-and-monetization.md new file mode 100644 index 0000000..bc5e3d9 --- /dev/null +++ b/docs/product-onboarding-retention-and-monetization.md @@ -0,0 +1,681 @@ +# Product onboarding, retention, and monetization recommendations + +**Status:** Superseded for v1 on 2026-08-02 +**Reviewed:** 2026-07-25 +**Scope:** Repository, public site, offline proofs, accepted ADRs, and current market +benchmarks + +> **Historical product research.** The employer attribution, governance, and +> reward program below is not the current v1 product or homepage direction. The +> approved v1 is the customer-hosted, wallet-native **Agent Spend Control +> Plane**, centered on a **Wallet Kernel** for x402 spending policy, exact human +> approval, signed receipts, and reconciliation. Skill attribution and Creator +> compensation are deferred expansion modules that may later consume Wallet +> Kernel receipts. See the +> [approved spend-control design](superpowers/specs/2026-07-31-agent-spend-control-plane-design.md). +> +> This document remains useful as a hypothesis ledger for that deferred +> expansion. Its buyer, pricing, roadmap, and homepage recommendations are not +> implementation commitments for v1. + +## Superseded executive recommendation + +Make the closed-mode product a **B2B attribution and compensation control plane for +employee-authored AI Skills**: + +> Measure which reusable AI Skills create value inside an organization, preserve who +> created and maintains them, and turn trusted evidence into fair, configurable rewards. + +The employer is the buyer. The employee-Creator is the emotional center. The Wielder +should keep using the tools they already use. + +Monetize the control plane with an annual employer-paid program license, paid +implementation, and enterprise add-ons. Pass internal creator rewards through at 100%. +Keep x402, Story provenance, and external revenue sharing as optional infrastructure, +not as the first product or the primary source of revenue. + +The immediate goal should be **three paid design-partner pilots**, not more protocol +surface area. + +### The five decisions to make now + +1. **Fund internal compensation with an employer-controlled periodic reward pool.** + Invocation and outcome data should influence awards, but raw call count should not be + the payout formula. +2. **Lead the product and site with the employer outcome.** Move the manifesto and + testnet wallet proof to an evidence path. +3. **Offer a no-wallet, no-key sandbox that demonstrates the full loop:** import a Skill, + record use by a second person, preview attribution, and close a reward period. +4. **Build the signed ledger, policy engine, creator portfolio, and monthly close before + on-chain settlement or a marketplace.** +5. **Charge for the program, not participation or creator earnings.** Favor a platform + fee per program or business unit, with generous included event volume. + +## What is already strong + +The project has unusually good foundations for an early research repository: + +- It labels evidence as measured, modeled, or hypothesis and states plainly that employer + demand is unvalidated ([root README](../README.md)). +- It has already made the correct strategic reframe: the closed compensation layer is + the terminal product, while the marketplace is optional + ([ADR-0007](adr/0007-closed-mode-compensation-layer-as-terminal-product.md)). +- It proves the thin payer, settlement arithmetic, provenance graph, and clone-economics + questions with executable artifacts. +- The Pi-Wielder proof has a clear architecture and runbook + ([Pi-Wielder README](../spikes/pi-wielder/README.md)). +- The offline clone and fork-economics paths run successfully without credentials. In + this review, they passed 97 checks and 64 invariants respectively. +- The corpus records uncomfortable findings—paid failures, clone economics, context loss, + securities exposure, and the Education bypass—instead of hiding them. + +These are credibility assets. The next step is to organize them around a customer journey +rather than asking every newcomer to reconstruct the product thesis from the research +corpus. + +## The core product gap + +The accepted product direction, public experience, and funding model do not yet agree. + +| Question | Current answer | Recommended answer | +|---|---|---| +| What is the product? | ADR-0007 says intra-org compensation; the site presents a sovereignty manifesto and a paid hosted Skill. | An employer-funded Skill attribution, governance, and reward program. | +| Who buys? | Not shown. The site speaks to an unspecified wallet holder. | Head of AI Platform / DevEx, with Total Rewards or IP Operations as co-owner. | +| Who receives value? | A one-time Wielder receives one output and receipt. | Admins get portfolio governance; Creators get durable evidence and rewards; Wielders get approved Skills in existing tools. | +| Where does compensation come from? | [CONTEXT.md](../CONTEXT.md) says intra-org upside comes from external Wielders, while the marketplace may never ship. | The employer funds an internal pool; genuine external revenue uses a separate revenue-share policy. | +| What is the billable product? | A 2.5% treasury split on a $0.25 Invocation in the demo. | Annual software license plus implementation and enterprise add-ons. | +| What makes users return? | No persistent account, history, catalog, dashboard, close, or notification loop. | Adoption, feedback, maintenance, monthly reward close, and audit loops. | + +The funding contradiction is the most important product decision. A terminal intra-org +product cannot require a future external marketplace to produce the employee benefit it +promises. + +## Recommended product contract + +### Initial customer profile + +Treat this as a hypothesis to validate, not a settled market fact: + +- An AI-forward software, professional-services, or knowledge-work company. +- Roughly 100–2,000 employees, large enough to have reusable internal AI artifacts but + small enough to run a design-partner program without a multi-year transformation. +- A centralized AI platform, developer-productivity, or automation team. +- A growing inventory of prompts, Skills, agents, plugins, or workflow automations. +- An existing innovation award, inventor award, bonus, or employee-recognition budget is + a strong qualification signal. + +The ideal buying group is: + +| Role | Job to be done | +|---|---| +| Head of AI Platform / DevEx | Find, govern, distribute, and measure reusable AI Skills. | +| VP Engineering / CIO | Show AI productivity and retain high-leverage contributors. | +| Total Rewards / People | Run a consistent, budgeted creator-recognition program. | +| Legal / IP / Finance | Approve ownership, departure, dispute, and payout rules. | +| Employee-Creator | Prove authorship and impact; receive recognition or compensation. | +| Employee-Wielder | Use approved Skills with almost no new workflow. | + +The creator is the beneficiary of the promise, but the employer has the budget and the +administrative problem. + +### The minimum durable workflow + +```text +Import → verify authorship → publish internally → use in existing tools + → collect outcome evidence → review attribution → close reward period + → notify Creator → improve/version Skill → more trusted reuse +``` + +The Phase-1 product should include: + +1. **Organization workspace** + - Organization, user, team, and role model. + - SSO or directory identity for enterprise deployments. + - Admin, policy reviewer, Creator, and viewer permissions. + +2. **Skill registry** + - Import from GitHub or a supported Skill directory. + - Canonical Skill ID, version hash, Creator, co-Creators, maintainer, status, and + lineage. + - Review, approve, deprecate, transfer maintainership, and archive flows. + - Searchable internal catalog with install or usage instructions. + +3. **Organization-native meter** + - Adapters for the customer's existing model gateway, agent runtime, or CLI. + - No individual wallet for internal use. + - Customer-hosted or VPC execution when repository or tool context is required. + - x402 only at the external payment edge. + +4. **Signed evidence ledger** + - Durable, idempotent Invocation records. + - Organization, Skill and version, Creator policy, Wielder or service identity, + timestamp, success, latency, cost, outcome signal, source, and adjustment status. + - Append-only policy and attribution history. + - No raw prompt or output storage by default; support redaction, hashing, retention + limits, and customer-controlled storage. + +5. **Reward policy and close** + - Fixed employer budget by month or quarter. + - Configurable weights, caps, eligibility, vesting, clawback, termination, and + co-authorship. + - Provisional allocation, anomaly review, manager/HR approval, dispute workflow, and + CSV/payroll export. + - Plain-language creator statement explaining why an award was calculated. + +6. **Creator and admin views** + - Creator: adoption, repeat users, teams reached, feedback, versions, maintenance + alerts, and provisional reward. + - Admin: active portfolio, duplication, failures, outcome evidence, budget, policy, + pending approvals, and audit export. + +ADR-0007 already identifies vesting, clawback, and termination as first-class design +inputs. They should be part of the core domain model, not postponed until a marketplace. + +## Resolve compensation without rewarding spam + +Use an Invocation as an evidence event, not as a dollar counter. + +A quarantined historical summary reported seven paid Agent turns from one human +prompt, but its normalized samples were not retained and it is not publishable +evidence +([Pi-Wielder evidence status](../spikes/pi-wielder/README.md#measured-results--overhead-distribution--live-pi-session-2026-07-15)). +Treat the following as a hypothesis until a fresh run reproduces it: a literal +per-call reward may favor chatty runtimes, create unpredictable employer cost, +and be easy to game. + +For internal use: + +1. The employer commits a fixed reward budget for a period. +2. Successful uses create provisional evidence points. +3. The policy weights signals such as unique Wielders, second-team adoption, repeat use, + accepted output, a linked downstream artifact, quality, and maintenance. +4. Caps and anomaly detection limit loops, retries, self-use, and synthetic traffic. +5. An authorized reviewer approves the close. +6. Payroll or the employer's reward system delivers the award. + +For genuine external revenue, a separate policy can split actual collected revenue. Do +not mix an internal recognition award and an external royalty into one unexplained +balance. + +This keeps the premise—Creators retain an economic claim on reuse—while making the +closed-mode product viable without a public marketplace. + +## Onboarding review and enhancements + +### 1. Align the first 15 seconds with the accepted product + +The public site currently asks a visitor to absorb ten manifesto principles before the +interactive proof and then presents a wallet, faucet, and testnet payment flow +([site content](../site/app/content.ts), +[manifesto UI](../site/app/manifesto.tsx)). That is memorable brand work, but it does not +explain the employer workflow in ADR-0007. + +Recommended homepage hierarchy: + +1. **Outcome:** “Measure and reward the people who build the AI workflows your company + reuses.” +2. **Proof:** a three-panel view of a registered Skill, verified team use, and creator + award statement. +3. **Role paths:** + - Employer: “Run a design-partner pilot.” + - Creator: “See what your Skill portfolio could look like.” + - Platform team: “Inspect the meter and event schema.” +4. **No-wallet interactive sandbox.** +5. **Technical receipts and manifesto.** + +Keep “Never Handed Over” as a strong campaign or evidence page. It should not carry the +entire product onboarding job. + +### 2. Replace the first paid experience with a truthful free one + +At the time of this review, the public Skill promised to inspect a repository +and resolve actual files, patterns, and verification commands, while the hosted +API sent only text and no repository, files, search tools, or execution tools. +That mismatched paid endpoint was retired from the website on 2026-08-02; this +observation remains here as the rationale. + +That means the paid first experience cannot reliably fulfill the Skill's defining +contract. + +Choose one: + +- Use a genuinely stateless demonstration Skill that is valuable from pasted input alone; + or +- Let the user connect a repository or use a preloaded sample repository, and execute with + the tools the Skill requires. + +The recommended sandbox should use a seeded fictional company: + +1. Choose or import a sample Skill. +2. Confirm its Creator and version. +3. Simulate use by three teammates. +4. Mark one result as accepted and link one sample pull request. +5. Show the creator-impact view and reward preview. + +Only after that should an interested technical evaluator opt into the wallet/testnet +protocol proof. + +### 3. Give each persona a first-five-minutes path + +| Persona | First success | Target | +|---|---|---| +| Buyer | Completes the sandbox and sees a sample monthly close. | Under 3 minutes | +| Organization admin | Imports one Skill, sets a sample policy, and records use by a second person. | Under 10 minutes | +| Creator | Claims authorship and sees one attributed use and reward explanation. | Under 5 minutes | +| Technical evaluator | Runs every offline proof from one root command. | Under 5 minutes after install | +| Contributor | Finds current architecture, roadmap, issues, and verification commands. | Under 10 minutes | + +### 4. Create a repository-wide paved road + +The root currently has no workspace manifest, pinned Node version, CI workflow, or single +verification command. The four proofs require directory hopping, and +[`site/README.md`](../site/README.md) is still the generated Next.js starter text. The +site has required environment variables but no committed `.env.example`. + +Recommended repository changes: + +- Add pinned Node/npm versions. +- Add root commands such as `bootstrap`, `verify`, `demo:offline`, and `site:mock`. +- Use `npm ci` consistently where a lockfile exists. +- Add `site/.env.example` and a zero-key `dev:mock` mode. +- Add CI for offline proofs, Phase 0 tests/typecheck, and site lint/build/tests. +- Replace the deliberately failing `prototype` test script. +- Print an expected success transcript and elapsed time. +- Add `docs/START_HERE.md`, `docs/ARCHITECTURE.md`, `docs/ROADMAP.md`, and + `CONTRIBUTING.md`. +- Publish sanitized product requirements instead of repeatedly referring contributors to + an unpublished PRD. + +### 5. Make paid failures explicit and recoverable + +The repository measured paid upstream failures and a settled-but-rejected +payment with no output. At the time of this review, the hosted route settled +before execution while its UI discarded some paid-failure state. The website +payment surface was retired on 2026-08-02 instead of carrying that behavior +forward. + +Before encouraging repeat paid use: + +- Preserve and display the receipt on every failure. +- Represent uncertain settlement as `payment_status: unknown`, not “nothing charged.” +- Add a durable idempotency key and Invocation state machine. +- Issue an automatic retry credit or refund after post-settlement failure. +- Validate configuration, input, and upstream readiness before authorization. +- Expose success rate, paid-without-output rate, refund time, and reconciliation errors. +- Do not show a declared split as if it were a completed payout. The current route settles + to one `payTo` address and returns a static split; creator balances and reconciliation + need their own ledger. + +For future external payments, current x402 documentation also describes usage-capped and +batch-settlement schemes. Evaluate those instead of treating the prototype's v1 fixed-price +path as a permanent product constraint +([x402 seller quickstart](https://docs.x402.org/getting-started/quickstart-for-sellers)). + +### 6. Add basic return actions + +The current public output is ephemeral React state. Add: + +- Saved Invocation and receipt history. +- Copy, download, rerun, feedback, and share actions. +- A next step after success. +- A design-partner/contact CTA. +- Funnel instrumentation from page view through repeat successful use. + +## Retention: why organizations and people stay + +The durable product is a set of reinforcing loops, not a payment handshake. + +### Creator improvement loop + +```text +Attributed use → outcome feedback → Creator sees impact → updates Skill + → Wielders adopt the new version → more verified value +``` + +Build version-specific feedback, update prompts, release notes, adoption lift, and stale +dependency alerts. + +### Team adoption loop + +```text +One Creator publishes → teammate succeeds → internal proof spreads + → another team adopts → more Creators contribute +``` + +Build a searchable catalog, clear install instructions, featured Skills, related Skills, +team invitations, and cross-team milestones. + +### Compensation ritual + +```text +Monthly evidence → policy calculation → reviewer approval → creator statement + → reward delivered → continued contribution and maintenance +``` + +This is the most defensible retention loop because it becomes an operational program, not +a novelty dashboard. A monthly close, audit trail, payroll export, and dispute process make +the system costly to replace for legitimate reasons. + +### Governance loop + +```text +Portfolio evidence → identify valuable, duplicate, stale, or risky Skills + → assign action → improve portfolio quality → trust grows +``` + +Add owner-departure alerts, duplicate detection, deprecated-version use, policy exceptions, +and maintainership transfer. + +### Return triggers + +- Weekly Creator digest: new users, teams, feedback, milestone, and update prompt. +- Monthly admin impact and reward-close report. +- First-five-users and first-second-team milestones. +- Declining-success and stale-version alerts. +- New-version notification for Wielders. +- Quarterly portfolio and policy review. + +Do not rely on a leaderboard alone. It will amplify popularity and gaming unless it is +quality-adjusted and reviewed. + +## Metrics + +### North star + +Use **weekly verified uses of shared Skills in retained organizations**. + +A verified use should be successful, performed by someone other than only the Creator, +and carry either an acceptance signal or a downstream outcome reference. Raw Invocation +count is a diagnostic, not the north star. + +### Activation + +- Visitor-to-sandbox-start and sandbox-completion rate. +- Sandbox-to-pilot-request rate. +- Time to first imported Skill. +- Time to first successful use by a second person. +- Percentage of workspaces that preview a reward policy. +- Percentage of pilot organizations activated within 14 days. + +### Engagement and retention + +- W1, W4, and W12 active-organization retention. +- Seven- and 28-day repeat-Wielder rate. +- Percentage of registered Skills used by at least two people and two teams. +- Verified uses per active organization. +- 30- and 90-day Skill survival. +- Monthly reward-close completion. +- Department expansion and invited-user conversion. + +### Creator loop + +- Active Creators per organization. +- Percentage receiving actionable feedback. +- Version update rate and adoption lift after update. +- Provisional, approved, and delivered compensation. +- Creator dashboard return rate. +- Perceived-recognition and intent-to-continue signals. + +### Buyer value + +- Estimated time or cost saved, with confidence level shown. +- Reward pool as a percentage of approved value. +- Duplicate or unowned Skills resolved. +- Pilot-to-paid conversion. +- Renewal and business-unit expansion. + +### Reliability and trust + +- Successful Invocation rate. +- Paid-without-output rate. +- Retry-credit or refund completion time. +- Ledger/payment reconciliation error rate. +- Attribution and policy dispute rate. +- p50/p95 time to first useful output. + +Employee-retention impact will take months to establish. Early proxies are continued +Creator participation, update frequency, cross-team reuse, perceived recognition, and +intent-to-stay changes. + +## Monetization + +### Why the current take rate should not be the business + +The prototype's 2.5% treasury fee yields $0.00625 on a $0.25 Invocation. + +| Volume | Creator/payment volume | Protocol revenue | +|---:|---:|---:| +| 100,000 Invocations | $25,000 | $625 | +| 1,000,000 Invocations | $250,000 | $6,250 | +| 4,000,000 Invocations | $1,000,000 | $25,000 | +| 160,000,000 Invocations | $40,000,000 | $1,000,000 | + +That is a poor base for enterprise implementation, security, support, and a compensation +workflow. It also creates the wrong optics: a creator-sovereignty product funds itself by +skimming creator rewards. + +Payment settlement is becoming low-cost infrastructure. Coinbase currently lists its x402 +facilitator at 1,000 free transactions per month and $0.001 thereafter +([official pricing](https://docs.cdp.coinbase.com/x402/core-concepts/facilitator)). +The attributed longitudinal ledger and policy workflow—not the payment handshake—must be +the premium. + +### Recommended revenue model + +#### 1. Annual enterprise program license + +Charge the employer for: + +- Skill registry and version/lineage system. +- Signed evidence ledger. +- Policy and reward-close workflow. +- Creator and administrator analytics. +- Audit, privacy, security, and export controls. + +Price by program or business unit with unlimited employee participants. Include a generous +event allowance; use event overages only to cover material infrastructure cost. + +This avoids penalizing adoption and matches an adjacent innovation-platform pattern. +Wazoku lists a $15,000-per-admin annual license with unlimited end users +([official pricing](https://www.wazoku.com/total-innovation-license/)). + +#### 2. Paid implementation and managed program services + +Offer: + +- Skill inventory and data mapping. +- Event-source integration. +- Reward-policy design and workflow configuration. +- Identity, HRIS, and payroll export setup. +- Admin training and first-close support. + +Wazoku separately lists enhanced onboarding at $5,450 and onboarding plus program setup at +$12,000, which supports treating implementation as real work rather than burying it in +software margin. + +#### 3. Enterprise add-ons + +- VPC, hybrid, or customer-managed data plane. +- SSO, SCIM, advanced RBAC, and longer audit retention. +- HRIS/payroll integrations. +- Custom policy packs and multi-jurisdiction workflows. +- SLA, premium support, and managed reconciliation. + +#### 4. OEM or metering API + +After the product works directly, sell the attributed meter to IP-management, +compensation, AI-gateway, or developer-platform vendors. This may become a strong channel +because those vendors already own buyer relationships but generally meter invention +milestones rather than runtime AI-asset use. + +#### 5. Benchmark analytics, later + +With explicit customer consent and strong aggregation thresholds, offer benchmarks for +Skill adoption, reward policy, creator concentration, maintenance, and reuse. This should +be an opt-in add-on only after enough customers make the data meaningful. + +#### 6. External marketplace fee, deferred + +If the product later supplies discovery, demand, collections, refunds, quality control, +and trust, test a 5–10% fee on external revenue. Do not charge that fee merely for wrapping +an endpoint in x402, and do not build tradeable claims without specialist legal advice. + +### Pricing hypotheses to test + +These are starting offers for design conversations, not validated prices: + +| Offer | Hypothesis | +|---|---:| +| Founding design-partner pilot, 8–12 weeks | $10,000–$20,000 | +| Single-program annual license | $15,000–$30,000 | +| Multi-business-unit annual license | $40,000–$75,000 | +| Enterprise/private deployment | $75,000–$150,000+ | +| Implementation | $5,000–$15,000 | +| Managed program support | $2,000–$5,000/month | + +Useful adjacent anchors as of the review date: + +- AppColl lists Invention Manager at $350/month for 100 users, including award + management, SSO, HR integration, workflows, and analytics + ([official pricing](https://www.appcoll.com/corporation-product-pricing/)). +- Bonusly lists employee-recognition software at $30–$50 per user annually and explicitly + separates the software subscription from rewards redeemed at face value + ([official pricing](https://bonusly.com/pricing)). +- LangSmith combines a team subscription with metered usage and reserves hybrid, + self-hosted, SSO, RBAC, and SLA capabilities for enterprise + ([official pricing](https://www.langchain.com/pricing)). +- Stripe Billing lists 0.7% of billing volume or annual subscription tiers, an additional + signal that mature metering infrastructure does not justify a 2.5% fee by itself + ([official pricing](https://stripe.com/billing/pricing)). + +These products are not exact competitors. They bound how buyers already purchase adjacent +innovation, recognition, observability, and billing workflows. + +### What not to monetize first + +- Do not charge Creators to register or see their own evidence. +- Do not take a percentage of an employer's internal reward pool. +- Do not make token sales or tradeable royalty claims the funding plan. +- Do not make the Education model a paid offer until its free re-authoring bypass has a + measured counter. +- Do not rely on inference resale margin; the repository already recognizes it as + commoditizing. + +## Validation plan + +### Customer discovery + +Run 12–15 interviews across: + +- AI Platform / Developer Productivity. +- Engineering or technology executives. +- Total Rewards / People Operations. +- IP Operations, Legal, or Finance. + +Prioritize organizations that already operate an inventor, innovation, or recognition +program and already have a centralized AI gateway or internal Skill inventory. + +Questions should test existing behavior, not solicit compliments: + +- How are reusable AI workflows found, approved, and maintained today? +- Who gets credit when another team reuses one? +- Has lack of credit caused hoarding, duplicated work, or attrition risk? +- What budget funds inventor awards, spot bonuses, or innovation programs? +- Which evidence would make an award defensible? +- Who can approve policy and payment? +- What security or employment-law condition would stop a pilot? +- Would the company fund a pool even if no external customer ever invokes a Skill? + +### Paid concierge pilot + +Do not wait for a full platform: + +1. Import a real Skill inventory. +2. Ingest signed or reconciled events from one existing runtime. +3. Produce creator and admin views, even if some operations are manual. +4. Run one policy preview and two monthly closes. +5. Export a payroll-ready or award-ready file. +6. Measure cross-team adoption, disputes, admin time, and Creator response. + +### Decision gates + +| Hypothesis | Pass signal | If it fails | +|---|---|---| +| Employers will pay for the rail. | Three pilots at $10,000 or more. | Stop protocol expansion; narrow or abandon the B2B thesis. | +| Internal compensation does not require marketplace revenue. | At least one pilot funds a real Creator pool. | Reposition as governance/analytics or target only commercialized Skills. | +| Usage evidence can approximate value. | Admins agree that the highest-scored Skills overlap materially with their independently selected high-value set. | Change signals and policy; do not automate payouts. | +| The buying group can form. | Each pilot has an AI-platform champion and a Total Rewards/IP owner. | Narrow to one buyer's problem and remove cross-functional scope. | +| Context-bound Skills can be instrumented usefully. | A meaningful share of one customer's real inventory produces reliable versioned events without losing its useful context. | Change the asset class or execution model. | +| The program creates a habit. | Two monthly closes complete and W4 verified use persists. | Diagnose workflow value before adding more integrations. | +| Annual value supports enterprise pricing. | Two pilots convert at $25,000+ ARR. | Reduce scope/cost or test an OEM model. | + +These are product decision gates, not forecasts. + +## Prioritized roadmap + +### Now: 0–30 days + +1. Write an ADR resolving the intra-org funding source and scoping “the Wielder is a + wallet” to external payment flows. +2. Run the customer interviews and recruit paid design partners. +3. Rewrite the primary landing page around the employer outcome. +4. Add a no-wallet seeded sandbox and a clear pilot CTA. +5. Replace or properly tool-enable the current demo Skill. +6. Fix charged-failure receipt visibility and define retry-credit/refund semantics. +7. Add a root offline verification path and replace the site starter README. +8. Publish a public product overview, architecture, roadmap, and contribution path. + +### Pilot: 31–90 days + +1. Define the organization, Skill/version, event, policy, close, and dispute schemas. +2. Import Skills from one source and events from one real runtime. +3. Build a persistent signed ledger. +4. Deliver minimal Creator and admin views. +5. Run fixed-budget policy previews and CSV/payroll export. +6. Instrument activation, verified use, trust, and close metrics. +7. Complete the first paid pilot close. + +### Productize: 91–180 days + +1. Add multi-tenant workspaces, SSO, RBAC, and identity mapping. +2. Add the internal catalog, versioning, feedback, and maintainer workflows. +3. Add configurable policies, approvals, departures, and disputes. +4. Add reliability controls, idempotency, credits/refunds, and reconciliation. +5. Add weekly Creator and monthly admin return triggers. +6. Offer customer-hosted or VPC execution for context-sensitive Skills. +7. Convert pilots to annual contracts. + +### Only after the gates pass + +- Background or opt-in Story registration. +- Additional runtime adapters. +- OEM/API distribution. +- Opt-in benchmark analytics. +- External x402 monetization. +- Marketplace discovery or tradeable claims, subject to legal review. + +### Explicitly defer + +- Securities and transfer infrastructure. +- Open marketplace build-out. +- Education-mode commercialization. +- TEE investment for an unvalidated open market. +- More inference-reseller work that does not improve the attributed ledger. + +## Bottom line + +The premise is monetizable, but not primarily as a per-call royalty marketplace. + +The commercially coherent product is the system an employer uses to answer: + +- What reusable AI Skills do we have? +- Who created and maintains them? +- Which teams use them successfully? +- What value evidence can we defend? +- What policy determines recognition or compensation? +- What changed, who approved it, and what was paid? + +If the project can make that monthly operating loop trustworthy and easy, the existing +wallet, provenance, and settlement work becomes valuable optional infrastructure. If it +cannot find three employers willing to pay for that loop, more protocol depth will not +solve the core problem. diff --git a/site/.env.example b/site/.env.example new file mode 100644 index 0000000..8e623a9 --- /dev/null +++ b/site/.env.example @@ -0,0 +1,6 @@ +# Optional: destination for the Wallet Kernel design-partner CTA. +# Defaults to a prefilled GitHub issue when unset. +NEXT_PUBLIC_PILOT_CONTACT_URL= + +# The archived /proof page is static. This site does not accept wallet keys, +# payment configuration, facilitator credentials, or model-provider secrets. diff --git a/site/.gitignore b/site/.gitignore index 5ef6a52..870462f 100644 --- a/site/.gitignore +++ b/site/.gitignore @@ -12,6 +12,8 @@ # testing /coverage +/output/playwright/ +/.playwright-cli/ # next.js /.next/ @@ -32,6 +34,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel diff --git a/site/README.md b/site/README.md index e215bc4..98421a5 100644 --- a/site/README.md +++ b/site/README.md @@ -1,36 +1,88 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Skill Asset Protocol site -## Getting Started +**Release status:** pre-release candidate. The approved design's implementation +and fresh-evidence gate is not cleared; do not deploy this candidate as the +public homepage yet. -First, run the development server: +The site has two deliberately separate paths: + +- `/` is the wallet-native **Agent Spend Control Plane** preview and + deterministic **Wallet Kernel** sandbox. It needs no account, wallet, API + key, network call, payment, or saved data. +- `/proof` preserves the original manifesto, one bounded historical Base + Sepolia receipt, and the retired x402 invocation experiment as a static + archive. The site exposes no wallet connection or paid endpoint. + +The approved v1 product is a customer-hosted Wallet Kernel for spending policy, +exact human approval, signed receipts, and reconciliation. The employer Skill +attribution and reward experience remains in the repository as deferred +expansion research; it is not linked from the current homepage and does not set +the v1 interface. See the +[approved design](../docs/superpowers/specs/2026-07-31-agent-spend-control-plane-design.md). + +## Local product preview + +Use Node 22. The production dependency graph and image optimizer are verified +against that major. ```bash +npm ci npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open . The full seeded sandbox works with no environment +file and never calls the payment or model API. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +The sample Agent, customer-owned wallet, policy, Spend Intents, decisions, +approval, and receipt projection are fictional. The sandbox never creates a +wallet payment signature or broadcasts a transaction. Refreshing or choosing +**Restart sandbox** returns to the same deterministic fixture. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +## Optional configuration -## Learn More +Copy the template only when you need to change the Wallet Kernel pilot CTA: -To learn more about Next.js, take a look at the following resources: +```bash +cp .env.example .env.local +``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +| Variable | Required | Purpose | +|---|---:|---| +| `NEXT_PUBLIC_PILOT_CONTACT_URL` | No | Override the design-partner CTA destination | -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +Never put a wallet key, facilitator credential, or model-provider secret in the +site environment. The customer-hosted Wallet Kernel—not the website—owns any +future payment integration. + +The historical receipt is evidence only that one test-USDC transfer settled on +Base Sepolia. It does not prove that the endpoint is currently live, that model +execution succeeded, that a declared split occurred, that customer demand +exists, or that the Wallet Kernel is production-ready. The offline homepage +sandbox is illustrative, not live settlement evidence. + +## Verification + +```bash +npm test +npm run lint +npm run build +npm audit --omit=dev +``` -## Deploy on Vercel +`npm test` covers the spend-control sandbox and public-claim quarantine with +Node's built-in test runner. The build must pass +without secrets because both routes are static product/research surfaces. The +final-head production audit must report zero vulnerabilities before merge. See the +[dependency security audit](../docs/dependency-security-audit.md) for the prior +audit record, the remaining dev-only advisory, and the intentionally pinned +transitive fixes. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Relevant files -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +| Path | Role | +|---|---| +| `app/page.tsx` | Wallet-native Agent Spend Control landing page | +| `app/landing.module.css` | Industrial product-page design system | +| `app/components/landing/SpendControlSandbox.tsx` | Current client-only Wallet Kernel sandbox | +| `app/components/landing/spend-control-model.ts` | Deterministic spend-policy fixture and reducer | +| `app/proof/page.tsx` | Static archive with one bounded historical receipt | diff --git a/site/app/api/invoke/[skillId]/route.ts b/site/app/api/invoke/[skillId]/route.ts deleted file mode 100644 index 9efa7bf..0000000 --- a/site/app/api/invoke/[skillId]/route.ts +++ /dev/null @@ -1,260 +0,0 @@ -// THE COLLAR — serverless x402 seller gating a hosted skill. -// -// Port of the working seller reference at spikes/pi-wielder/src/x402-seller.mjs -// into a Next.js Node-runtime route handler. The flow, per the x402 v1 spec: -// -// 1. POST without X-PAYMENT → 402 { x402Version: 1, accepts: [PaymentRequirements] }. -// 2. POST with X-PAYMENT → facilitator /verify, then /settle. The settled -// txHash is the single-use execution credential ("NO CREDENTIAL, NO RUN"). -// 3. Only then does the skill run: the skill content (see ./skill-content.ts, -// generated from ./skill.md) becomes the SYSTEM prompt of one Anthropic -// Messages API call. The caller receives OUTPUT ONLY — never the skill text. -// -// NOTE we settle BEFORE executing the skill (same deliberate ordering as the -// reference seller): the txHash *is* the execution credential — pay → mint → -// consume → execute. -// -// REPLAY PROTECTION: the EIP-3009 nonce inside the signed TransferWithAuthorization -// is single-use ON-CHAIN. A replayed X-PAYMENT re-submits the same nonce, so the -// facilitator's /settle fails — settle-failure IS the replay rejection. The -// in-memory consumed-set below is belt-and-braces for a single warm serverless -// instance (instances don't share memory; the chain is the real guarantee). - -import { NextResponse } from 'next/server'; -import { SKILL_CONTENT } from './skill-content'; - -export const runtime = 'nodejs'; - -// --- x402 v1 / Base Sepolia constants (mirror x402-seller.mjs) --------------- -const X402_VERSION = 1; -const NETWORK = 'base-sepolia'; -const USDC_ADDRESS = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; -const USDC_EIP712 = { name: 'USDC', version: '2' }; -const USDC_DECIMALS = 6; - -const PRICE_ATOMIC = '250000'; // $0.25 USDC (6 decimals) -const SPLIT = { creator: 0.24375, treasury: 0.00625 }; // 97.5 / 2.5 of $0.25 - -const KNOWN_SKILL_ID = 'optimizing-claude-code-prompts'; -const DEFAULT_FACILITATOR = 'https://x402.org/facilitator'; - -const jsonToB64 = (o: unknown) => Buffer.from(JSON.stringify(o)).toString('base64'); -const b64ToJson = (s: string): T => JSON.parse(Buffer.from(s, 'base64').toString('utf8')) as T; - -// Per-instance consumed credentials (see REPLAY PROTECTION note above). -const consumed = new Set(); - -interface PaymentRequirements { - scheme: string; - network: string; - maxAmountRequired: string; - resource: string; - description: string; - mimeType: string; - payTo: string; - maxTimeoutSeconds: number; - asset: string; - extra: { name: string; version: string }; -} - -async function postJson(url: string, body: unknown): Promise { - const res = await fetch(url, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(body), - }); - return res.json().catch(() => null); -} - -export async function POST( - req: Request, - ctx: { params: Promise<{ skillId: string }> }, -) { - const { skillId } = await ctx.params; - if (skillId !== KNOWN_SKILL_ID) { - return NextResponse.json({ error: `unknown skill: ${skillId}` }, { status: 404 }); - } - - // -- config: fail honestly BEFORE taking anyone's money ----------------------- - const payTo = process.env.PAY_TO_ADDRESS; - if (!payTo) { - return NextResponse.json( - { error: 'seller misconfigured: PAY_TO_ADDRESS is not set' }, - { status: 500 }, - ); - } - const anthropicKey = process.env.ANTHROPIC_API_KEY; - if (!anthropicKey) { - return NextResponse.json( - { error: 'seller misconfigured: ANTHROPIC_API_KEY is not set' }, - { status: 502 }, - ); - } - const facilitatorUrl = process.env.FACILITATOR_URL || DEFAULT_FACILITATOR; - - // -- input: validate before challenging (don't charge for an unrunnable call) -- - let input = ''; - try { - const body = (await req.json()) as { input?: unknown }; - if (typeof body.input === 'string') input = body.input.trim(); - } catch { - /* fall through to the 400 below */ - } - if (!input) { - return NextResponse.json( - { error: 'request body must be JSON: { "input": "" }' }, - { status: 400 }, - ); - } - - const requirements: PaymentRequirements = { - scheme: 'exact', - network: NETWORK, - maxAmountRequired: PRICE_ATOMIC, // atomic USDC (6 decimals) — $0.25 - resource: req.url, - description: `Run the hosted skill "${KNOWN_SKILL_ID}" — output only, never the skill.`, - mimeType: 'application/json', - payTo, - maxTimeoutSeconds: 60, - asset: USDC_ADDRESS, - // The buyer needs these to build the EIP-712 domain it signs against. - extra: { name: USDC_EIP712.name, version: USDC_EIP712.version }, - }; - - // -- step 1: no payment attached → challenge with 402 -------------------------- - const paymentHeader = req.headers.get('X-PAYMENT'); - if (!paymentHeader) { - return NextResponse.json( - { x402Version: X402_VERSION, error: 'X-PAYMENT header is required', accepts: [requirements] }, - { status: 402 }, - ); - } - - // -- step 2: decode + verify + settle through the facilitator ------------------- - let paymentPayload: unknown; - try { - paymentPayload = b64ToJson(paymentHeader); - } catch { - return NextResponse.json( - { x402Version: X402_VERSION, error: 'malformed X-PAYMENT header', accepts: [requirements] }, - { status: 402 }, - ); - } - - const facilitatorBody = { x402Version: X402_VERSION, paymentPayload, paymentRequirements: requirements }; - - let verify: { isValid?: boolean; invalidReason?: string } | null; - let settle: { success?: boolean; errorReason?: string; transaction?: string; payer?: string } | null; - try { - verify = (await postJson(`${facilitatorUrl}/verify`, facilitatorBody)) as typeof verify; - if (!verify?.isValid) { - return NextResponse.json( - { - x402Version: X402_VERSION, - error: `payment verification failed: ${verify?.invalidReason ?? 'unknown'}`, - accepts: [requirements], - }, - { status: 402 }, - ); - } - - settle = (await postJson(`${facilitatorUrl}/settle`, facilitatorBody)) as typeof settle; - } catch { - // Honest failure: the facilitator was unreachable — nothing was charged. - return NextResponse.json( - { error: `facilitator unreachable at ${facilitatorUrl} — payment not settled, nothing charged` }, - { status: 502 }, - ); - } - - if (!settle?.success || !settle.transaction) { - // EIP-3009 nonces are single-use on-chain, so a replayed payment lands here: - // settle-failure = replay rejected (or insufficient funds / expired authorization). - return NextResponse.json( - { - x402Version: X402_VERSION, - error: `payment settlement failed: ${settle?.errorReason ?? 'unknown'} (replayed or invalid authorization — EIP-3009 nonces spend exactly once)`, - accepts: [requirements], - }, - { status: 402 }, - ); - } - - // -- step 3: the settled txHash is a single-use credential ---------------------- - if (consumed.has(settle.transaction)) { - return NextResponse.json( - { error: 'replayed payment: credential already consumed', txHash: settle.transaction }, - { status: 409 }, - ); - } - consumed.add(settle.transaction); - - // -- step 4: run the skill — output only, NEVER the skill text ------------------- - let output: string; - try { - const anthropicRes = await fetch('https://api.anthropic.com/v1/messages', { - method: 'POST', - headers: { - 'x-api-key': anthropicKey, - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - }, - body: JSON.stringify({ - model: 'claude-sonnet-4-6', - max_tokens: 1500, - system: SKILL_CONTENT, // the protected asset stays server-side - messages: [{ role: 'user', content: input }], - }), - }); - - if (!anthropicRes.ok) { - const detail = await anthropicRes.text().catch(() => ''); - // Honest failure: payment settled but the skill run failed. The txHash is - // included so the buyer holds the receipt for the failed run. - return NextResponse.json( - { - error: `skill execution failed: anthropic returned ${anthropicRes.status}`, - detail: detail.slice(0, 500), - paid: { amountUSDC: Number(PRICE_ATOMIC) / 10 ** USDC_DECIMALS, txHash: settle.transaction }, - }, - { status: 502 }, - ); - } - - const message = (await anthropicRes.json()) as { - content?: Array<{ type: string; text?: string }>; - }; - output = (message.content ?? []) - .filter((b) => b.type === 'text' && typeof b.text === 'string') - .map((b) => b.text) - .join('\n'); - } catch { - return NextResponse.json( - { - error: 'skill execution failed: anthropic unreachable', - paid: { amountUSDC: Number(PRICE_ATOMIC) / 10 ** USDC_DECIMALS, txHash: settle.transaction }, - }, - { status: 502 }, - ); - } - - // -- success: output + receipt + declared split ---------------------------------- - return NextResponse.json( - { - output, - paid: { amountUSDC: Number(PRICE_ATOMIC) / 10 ** USDC_DECIMALS, txHash: settle.transaction }, - split: SPLIT, - }, - { - headers: { - // Standard buyer-visible settlement receipt (mirrors the reference seller). - 'X-PAYMENT-RESPONSE': jsonToB64({ - success: true, - transaction: settle.transaction, - network: NETWORK, - payer: settle.payer, - }), - }, - }, - ); -} diff --git a/site/app/api/invoke/[skillId]/skill-content.ts b/site/app/api/invoke/[skillId]/skill-content.ts deleted file mode 100644 index f8520d0..0000000 --- a/site/app/api/invoke/[skillId]/skill-content.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED at scaffold time from ../../../../skill.md (source: .claude/skills/optimizing-claude-code-prompts/SKILL.md). -// Inlined as a TS constant so the skill content is bundled into the serverless function -// (survives Vercel — no outputFileTracingIncludes needed). Regenerate by re-running the -// scaffold step; do not hand-edit. -// The skill text is the PROTECTED ASSET: it is used server-side as the system prompt and -// must never be returned to the caller. -export const SKILL_CONTENT: string = "---\nname: optimizing-claude-code-prompts\ndescription: Turn a rough or vague request to Claude Code into a precise, repo-grounded, high-performing prompt. Use when the user wants help phrasing, drafting, improving, optimizing, tightening, or \"making better\" a prompt/request/instruction for Claude Code; when the user pastes a draft and asks how to word it; when a prompt already ran and Claude did the wrong thing (diagnose and fix it); or when Claude Code keeps missing the mark — doing too much, ignoring constraints, solving the wrong problem, over-engineering, or needing many back-and-forth rounds. Triggers include \"optimize this prompt\", \"help me ask Claude to…\", \"rewrite my request\", \"why did Claude do that\", \"how should I word this\", \"make this prompt clearer\".\n---\n\n# Optimizing Claude Code Prompts\n\n## Overview\n\nRewrite a user's rough request into a prompt that Claude Code can execute correctly on the\nfirst pass. The current models (Opus 4.8) follow instructions **literally** and run\n**autonomously**, so the highest-leverage move is to front-load intent, constraints, and a\nrunnable check in the first message. Vague asks spread across many turns waste tokens and\nbuild the wrong thing.\n\n**Core principle:** A strong Claude Code prompt names the **goal**, the **context**, the\n**constraints**, the **files/patterns to follow**, and **a check Claude can run to know it's\ndone**. Optimizing means supplying whichever of these the user left out — and resolving each\nto a *real artifact in this repo*, not a placeholder the user must fill in later.\n\n## The one rule that makes this skill worth invoking: ground in the repo\n\nA prompt full of `@[your-file-here]` and \"run the test suite\" is just a template — it hands the\nhard part back to the user. Before writing the optimized prompt, **resolve every reference to a\nreal thing** using the tools you have:\n\n| Reference | How to resolve it | Don't emit |\n|---|---|---|\n| The target file(s) | `Glob`/`Grep` for the actual path | `@[src/whatever]` |\n| \"Done when…\" check | Read `package.json` scripts / `Makefile` / `pyproject.toml` / CI config for the real test/build/lint command | \"run the tests\" |\n| \"Follow the pattern in…\" | `Grep` for a sibling that already does the thing; name that file | \"the existing pattern\" |\n| The symptom's likely location | `Grep` the error string / feature name to the directory | \"somewhere in the code\" |\n\n**Resolve, don't guess.** If a genuine look can't resolve something, ask **one** surgical\nquestion — never paper over it with a bracketed guess.\n\n**Red flags that you skipped grounding** (STOP and go look): the output prompt contains `[...]`,\n\"the relevant file\", \"your test command\", \"the appropriate\", or any path you didn't verify exists.\n\n## Pick the mode\n\n| The user… | Mode | What you do |\n|---|---|---|\n| Pasted a draft prompt | **Optimize** | Ground it, fill missing ingredients, return the rewrite |\n| Gave a bare goal (\"add auth\") | **Generate** | Ground it, build the prompt from scratch |\n| Says a prompt already failed (\"Claude did X not Y\") | **Diagnose** | Map the failure to the missing ingredient, fix it, add the session-hygiene step |\n| Wants a large/multi-file feature | **Spec** | Don't hand-write a mega-prompt — route to the interview→SPEC.md pattern (see reference) |\n\n## Workflow\n\n1. **Capture** the raw request verbatim. Pick the mode.\n2. **Ground** in the repo — resolve real paths, the real verification command, the real pattern\n file (table above). Do this with parallel `Glob`/`Grep`/`Read` calls; it's fast and it's the\n whole point.\n3. **Diagnose + score** the request against the seven ingredients. Show the scorecard.\n4. **Resolve gaps:** correctness-blocking gaps that grounding couldn't settle → up to **3**\n `AskUserQuestion` questions. If the user wants speed (\"just optimize it\"), proceed and label\n any remaining assumption explicitly.\n5. **Write** the optimized prompt as a copy-paste block, with real values throughout.\n6. **Hand back + offer to run it.** Note the one assumption most worth confirming, if any.\n\nDon't pad the prompt with obvious instructions (\"write clean code\"). Opus 4.8 is literal and\nsmart — filler dilutes the real constraints.\n\n## The seven ingredients\n\n| Ingredient | Answers | Weak → Strong |\n|---|---|---|\n| **Goal** | What outcome, concretely? | \"improve the dashboard\" → \"add date-range filtering to the dashboard\" |\n| **Context** | Why / where does this live? | — → \"endpoint is `@src/api/orders.ts`; read-heavy, data changes hourly\" |\n| **Constraints** | What must NOT change / limits? | — → \"keep the JSON shape backward-compatible; no new deps\" |\n| **References** | What pattern to follow? | — → \"mirror `@src/api/users.ts`\" |\n| **Acceptance / check** | How do we know it's done? | \"make it work\" → \"`npm test src/api/orders.test.ts` passes; show output\" |\n| **Approach / mode** | How should Claude work? | — → \"plan first\" / \"use TDD\" / \"just do it\" |\n| **Output** | What should Claude return? | — → \"show the diff and the test results\" |\n\nThe **acceptance check** is the highest-value ingredient — it's the difference between a session\nthe user babysits and one Claude closes on its own. Always try to supply a real one.\n\n## Scorecard (show this — it teaches the pattern)\n\n```text\nGoal ✓ Context ✗ → added Constraints ✗ → added\nReferences ✗→added Check ✗ → added Approach ~ → set Output ✓\nBefore: 2/7 After: 7/7\n```\n\n## Output template\n\n```text\n\n\nContext: \nConstraints: \nFollow: \nDone when: \nApproach: \n```\n\nDrop any line that genuinely doesn't apply. Prefer natural prose for tiny tasks; use the labeled\nlines when there are real constraints.\n\n## Example (grounded — note: no brackets)\n\n**Raw:** `make the checkout page faster`\n\n**Grounding moves:**\n- `Glob **/checkout*` → `src/checkout/CheckoutPage.tsx`\n- `package.json` scripts → `\"test\": \"vitest run\"`, `\"build\": \"vite build\"`\n- `Grep \"useMemo\\|React.memo\" src` → `src/cart/CartPage.tsx` already memoizes its list\n\n**Scorecard:** Before 1/7 → After 7/7\n\n**Optimized:**\n\n```text\nSpeed up the checkout page in @src/checkout/CheckoutPage.tsx — its product list re-renders on\nevery keystroke in the promo-code field.\n\nContext: the list isn't memoized, so typing recomputes and re-renders all rows.\nConstraints: don't change checkout behavior or the order-submit payload; no new dependencies.\nFollow: the memoization pattern already in @src/cart/CartPage.tsx (React.memo on rows + useMemo\non the derived list).\nDone when: `npx vitest run src/checkout` passes, `npm run build` succeeds, and typing in the\npromo field no longer re-renders product rows (verify with a render count or React DevTools).\nShow me the diff and the test output.\nApproach: plan first, then implement.\n```\n\n## Mode specifics\n\n- **Diagnose** a failed prompt: name the failure → the missing ingredient it maps to → the fix.\n \"Claude refactored the whole file\" = missing **Constraints** (add \"only change X; no refactors\").\n \"Claude solved the wrong thing\" = missing **Goal/Context** (name the file + symptom). Also tell\n the user the session fix: after two bad corrections, `/clear` and resend the optimized prompt;\n use `/rewind` to undo Claude's changes.\n- **Generate** from a bare goal: ground first, then if scope is still ambiguous ask the 3\n questions before writing — don't generate a confident prompt on top of unknowns.\n- **Spec** a big feature: see the interview→SPEC.md→fresh-session pattern in the reference.\n\n## Common mistakes\n\n| Mistake | Fix |\n|---|---|\n| Emitting `[bracketed placeholders]` | Ground in the repo; resolve to real paths/commands, or ask one question |\n| Stacking unrelated tasks in one prompt | One task per prompt; `/clear` between them |\n| \"Make it better\" with no check | Name a real verification: a test command, a build, a screenshot to compare |\n| Describing the fix instead of the symptom | Give symptom + likely location; let Claude find the cause |\n| Over-specifying the obvious | Cut filler; keep only constraints Claude can't infer |\n| Dribbling context over many turns | Front-load intent + constraints in the first message — Opus 4.8 rewards this |\n\n## Deeper guidance\n\nFor the full strategy tables, model-specific behavior (literalism, autonomy, over-eagerness),\nthe verification-gating ladder, rich-context input (`@files`, images, URLs, piping), plan-mode\ndecisions, mid-task course-correction phrasing, the interview→spec pattern, and reusable prompt\nsnippets, read [references/claude-code-prompting-guide.md](references/claude-code-prompting-guide.md).\n\nSource material: Anthropic's publicly documented Claude Code prompting guidance\n(https://code.claude.com/docs), summarized in this skill's own words.\n"; diff --git a/site/app/api/invoke/[skillId]/skill.md b/site/app/api/invoke/[skillId]/skill.md deleted file mode 100644 index b1c1114..0000000 --- a/site/app/api/invoke/[skillId]/skill.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -name: optimizing-claude-code-prompts -description: Turn a rough or vague request to Claude Code into a precise, repo-grounded, high-performing prompt. Use when the user wants help phrasing, drafting, improving, optimizing, tightening, or "making better" a prompt/request/instruction for Claude Code; when the user pastes a draft and asks how to word it; when a prompt already ran and Claude did the wrong thing (diagnose and fix it); or when Claude Code keeps missing the mark — doing too much, ignoring constraints, solving the wrong problem, over-engineering, or needing many back-and-forth rounds. Triggers include "optimize this prompt", "help me ask Claude to…", "rewrite my request", "why did Claude do that", "how should I word this", "make this prompt clearer". ---- - -# Optimizing Claude Code Prompts - -## Overview - -Rewrite a user's rough request into a prompt that Claude Code can execute correctly on the -first pass. The current models (Opus 4.8) follow instructions **literally** and run -**autonomously**, so the highest-leverage move is to front-load intent, constraints, and a -runnable check in the first message. Vague asks spread across many turns waste tokens and -build the wrong thing. - -**Core principle:** A strong Claude Code prompt names the **goal**, the **context**, the -**constraints**, the **files/patterns to follow**, and **a check Claude can run to know it's -done**. Optimizing means supplying whichever of these the user left out — and resolving each -to a *real artifact in this repo*, not a placeholder the user must fill in later. - -## The one rule that makes this skill worth invoking: ground in the repo - -A prompt full of `@[your-file-here]` and "run the test suite" is just a template — it hands the -hard part back to the user. Before writing the optimized prompt, **resolve every reference to a -real thing** using the tools you have: - -| Reference | How to resolve it | Don't emit | -|---|---|---| -| The target file(s) | `Glob`/`Grep` for the actual path | `@[src/whatever]` | -| "Done when…" check | Read `package.json` scripts / `Makefile` / `pyproject.toml` / CI config for the real test/build/lint command | "run the tests" | -| "Follow the pattern in…" | `Grep` for a sibling that already does the thing; name that file | "the existing pattern" | -| The symptom's likely location | `Grep` the error string / feature name to the directory | "somewhere in the code" | - -**Resolve, don't guess.** If a genuine look can't resolve something, ask **one** surgical -question — never paper over it with a bracketed guess. - -**Red flags that you skipped grounding** (STOP and go look): the output prompt contains `[...]`, -"the relevant file", "your test command", "the appropriate", or any path you didn't verify exists. - -## Pick the mode - -| The user… | Mode | What you do | -|---|---|---| -| Pasted a draft prompt | **Optimize** | Ground it, fill missing ingredients, return the rewrite | -| Gave a bare goal ("add auth") | **Generate** | Ground it, build the prompt from scratch | -| Says a prompt already failed ("Claude did X not Y") | **Diagnose** | Map the failure to the missing ingredient, fix it, add the session-hygiene step | -| Wants a large/multi-file feature | **Spec** | Don't hand-write a mega-prompt — route to the interview→SPEC.md pattern (see reference) | - -## Workflow - -1. **Capture** the raw request verbatim. Pick the mode. -2. **Ground** in the repo — resolve real paths, the real verification command, the real pattern - file (table above). Do this with parallel `Glob`/`Grep`/`Read` calls; it's fast and it's the - whole point. -3. **Diagnose + score** the request against the seven ingredients. Show the scorecard. -4. **Resolve gaps:** correctness-blocking gaps that grounding couldn't settle → up to **3** - `AskUserQuestion` questions. If the user wants speed ("just optimize it"), proceed and label - any remaining assumption explicitly. -5. **Write** the optimized prompt as a copy-paste block, with real values throughout. -6. **Hand back + offer to run it.** Note the one assumption most worth confirming, if any. - -Don't pad the prompt with obvious instructions ("write clean code"). Opus 4.8 is literal and -smart — filler dilutes the real constraints. - -## The seven ingredients - -| Ingredient | Answers | Weak → Strong | -|---|---|---| -| **Goal** | What outcome, concretely? | "improve the dashboard" → "add date-range filtering to the dashboard" | -| **Context** | Why / where does this live? | — → "endpoint is `@src/api/orders.ts`; read-heavy, data changes hourly" | -| **Constraints** | What must NOT change / limits? | — → "keep the JSON shape backward-compatible; no new deps" | -| **References** | What pattern to follow? | — → "mirror `@src/api/users.ts`" | -| **Acceptance / check** | How do we know it's done? | "make it work" → "`npm test src/api/orders.test.ts` passes; show output" | -| **Approach / mode** | How should Claude work? | — → "plan first" / "use TDD" / "just do it" | -| **Output** | What should Claude return? | — → "show the diff and the test results" | - -The **acceptance check** is the highest-value ingredient — it's the difference between a session -the user babysits and one Claude closes on its own. Always try to supply a real one. - -## Scorecard (show this — it teaches the pattern) - -```text -Goal ✓ Context ✗ → added Constraints ✗ → added -References ✗→added Check ✗ → added Approach ~ → set Output ✓ -Before: 2/7 After: 7/7 -``` - -## Output template - -```text - - -Context: -Constraints: -Follow: -Done when: -Approach: -``` - -Drop any line that genuinely doesn't apply. Prefer natural prose for tiny tasks; use the labeled -lines when there are real constraints. - -## Example (grounded — note: no brackets) - -**Raw:** `make the checkout page faster` - -**Grounding moves:** -- `Glob **/checkout*` → `src/checkout/CheckoutPage.tsx` -- `package.json` scripts → `"test": "vitest run"`, `"build": "vite build"` -- `Grep "useMemo\|React.memo" src` → `src/cart/CartPage.tsx` already memoizes its list - -**Scorecard:** Before 1/7 → After 7/7 - -**Optimized:** - -```text -Speed up the checkout page in @src/checkout/CheckoutPage.tsx — its product list re-renders on -every keystroke in the promo-code field. - -Context: the list isn't memoized, so typing recomputes and re-renders all rows. -Constraints: don't change checkout behavior or the order-submit payload; no new dependencies. -Follow: the memoization pattern already in @src/cart/CartPage.tsx (React.memo on rows + useMemo -on the derived list). -Done when: `npx vitest run src/checkout` passes, `npm run build` succeeds, and typing in the -promo field no longer re-renders product rows (verify with a render count or React DevTools). -Show me the diff and the test output. -Approach: plan first, then implement. -``` - -## Mode specifics - -- **Diagnose** a failed prompt: name the failure → the missing ingredient it maps to → the fix. - "Claude refactored the whole file" = missing **Constraints** (add "only change X; no refactors"). - "Claude solved the wrong thing" = missing **Goal/Context** (name the file + symptom). Also tell - the user the session fix: after two bad corrections, `/clear` and resend the optimized prompt; - use `/rewind` to undo Claude's changes. -- **Generate** from a bare goal: ground first, then if scope is still ambiguous ask the 3 - questions before writing — don't generate a confident prompt on top of unknowns. -- **Spec** a big feature: see the interview→SPEC.md→fresh-session pattern in the reference. - -## Common mistakes - -| Mistake | Fix | -|---|---| -| Emitting `[bracketed placeholders]` | Ground in the repo; resolve to real paths/commands, or ask one question | -| Stacking unrelated tasks in one prompt | One task per prompt; `/clear` between them | -| "Make it better" with no check | Name a real verification: a test command, a build, a screenshot to compare | -| Describing the fix instead of the symptom | Give symptom + likely location; let Claude find the cause | -| Over-specifying the obvious | Cut filler; keep only constraints Claude can't infer | -| Dribbling context over many turns | Front-load intent + constraints in the first message — Opus 4.8 rewards this | - -## Deeper guidance - -For the full strategy tables, model-specific behavior (literalism, autonomy, over-eagerness), -the verification-gating ladder, rich-context input (`@files`, images, URLs, piping), plan-mode -decisions, mid-task course-correction phrasing, the interview→spec pattern, and reusable prompt -snippets, read [references/claude-code-prompting-guide.md](references/claude-code-prompting-guide.md). - -Source material: Anthropic's publicly documented Claude Code prompting guidance -(https://code.claude.com/docs), summarized in this skill's own words. diff --git a/site/app/components/landing/Hero.tsx b/site/app/components/landing/Hero.tsx new file mode 100644 index 0000000..6942979 --- /dev/null +++ b/site/app/components/landing/Hero.tsx @@ -0,0 +1,117 @@ +import Link from 'next/link'; + +import styles from '../../landing.module.css'; + +export function Hero({ pilotUrl }: { pilotUrl: string }) { + return ( +
+ + +
+
+
+ ); +} diff --git a/site/app/components/landing/PilotCta.tsx b/site/app/components/landing/PilotCta.tsx new file mode 100644 index 0000000..7804b82 --- /dev/null +++ b/site/app/components/landing/PilotCta.tsx @@ -0,0 +1,28 @@ +import styles from '../../landing.module.css'; + +export function PilotCta({ pilotUrl }: { pilotUrl: string }) { + return ( +
+
+

Customer-hosted design-partner pilot

+

+ Put one real Agent workflow behind an explicit spending boundary. +

+
+
+
    +
  • One Pi workflow and local Agent adapter
  • +
  • One customer-owned CDP wallet
  • +
  • Allow-listed Base Sepolia x402 sellers
  • +
  • + Policy, approvals, reconciliation, and planned signed receipt export +
  • +
+ + Discuss a spend-control pilot + + +
+
+ ); +} diff --git a/site/app/components/landing/ProofLoop.tsx b/site/app/components/landing/ProofLoop.tsx new file mode 100644 index 0000000..a5b63ee --- /dev/null +++ b/site/app/components/landing/ProofLoop.tsx @@ -0,0 +1,40 @@ +import { productProof } from '../../landing-content'; +import styles from '../../landing.module.css'; + +export function ProofLoop() { + return ( +
+
+

The control loop

+

From Agent request to accountable spend.

+

+ The customer-hosted Wallet Kernel stays wallet-first: it decides what + may be signed and records what happened. Planned on-chain settlement + stays behind the release gate. +

+
+ +
    + {productProof.map((item, index) => ( +
  1. +
    + {item.number} + {item.eyebrow} +
    +

    {item.title}

    +

    {item.body}

    +
    +
    + {index < productProof.length - 1 && ( + + )} +
  2. + ))} +
+
+ ); +} diff --git a/site/app/components/landing/SpendControlSandbox.tsx b/site/app/components/landing/SpendControlSandbox.tsx new file mode 100644 index 0000000..c850c98 --- /dev/null +++ b/site/app/components/landing/SpendControlSandbox.tsx @@ -0,0 +1,494 @@ +'use client'; + +import { useEffect, useReducer, useRef } from 'react'; + +import styles from '../../landing.module.css'; +import { + demoSessionProjection, + demoWallet, + INITIAL_SPEND_SANDBOX_STATE, + nextSpendSandboxAction, + spendPolicy, + spendSandboxReducer, + spendSandboxView, + spendStageIndex, + type DemoSpendAttempt, + type SpendSandboxStage, +} from './spend-control-model'; + +const steps: readonly { + label: string; + detail: string; + stage: SpendSandboxStage; +}[] = [ + { label: 'Policy', detail: 'Default deny', stage: 'ready' }, + { label: 'Auto-pay', detail: 'Below ceiling', stage: 'policy_loaded' }, + { label: 'Deny', detail: 'Unknown seller', stage: 'auto_allowed' }, + { label: 'Escalate', detail: 'Exact mismatch', stage: 'denied' }, + { label: 'Approve', detail: 'Still no signature', stage: 'approval_pending' }, + { label: 'Retry', detail: 'Wielder repeats', stage: 'approved_waiting_retry' }, +]; + +const announcements: Record = { + ready: 'Sandbox ready. Load the fictional customer policy to begin.', + policy_loaded: + 'Default-deny policy loaded for one customer-owned Base Sepolia wallet.', + auto_allowed: + 'The 0.08 test-USDC request matched policy and advanced in simulation.', + denied: + 'The unknown seller was denied before the fictional wallet or signer path.', + approval_pending: + 'The 0.60 request is queued for exact approval. No signature exists.', + approved_waiting_retry: + 'Approval is recorded and the flow has stopped. The Wielder must repeat the exact request.', + approval_invalidated: + 'The retry was rejected and its prior approval was invalidated. Start a new request.', + finalized: + 'The exact request was deliberately repeated and an unsigned session projection is ready.', +}; + +const isStepComplete = ( + stepStage: SpendSandboxStage, + currentStage: SpendSandboxStage, +): boolean => spendStageIndex(currentStage) > spendStageIndex(stepStage); + +const formatAtomic = (atomic: number): string => + `${(atomic / 1_000_000).toFixed(2)} test USDC`; + +const decisionLabel = (attempt: DemoSpendAttempt): string => { + if (attempt.decision === 'allow') return '✓ Policy matched'; + if (attempt.decision === 'deny') return '× Default denied'; + return '◇ Exact approval path'; +}; + +const signatureLabel = (attempt: DemoSpendAttempt): string => { + if (attempt.hasProjectedSigningBoundary) { + return 'Unsigned flow projection · no key used'; + } + if (attempt.decision === 'deny') return 'Signer never reached'; + return 'No signature created'; +}; + +function SpendProjectionPreview({ + pilotUrl, + chargedAtomic, + remainingAtomic, +}: { + pilotUrl: string; + chargedAtomic: number; + remainingAtomic: number; +}) { + return ( + <> +
+
+
+

Exact authority

+

The approval cannot become a blank check.

+

+ It is bound to one Spend Intent, seller, resource, wallet, exact + approved amount, policy version, and expiry. A changed request + must start again. +

+
+
+
+
Default action
+
+ Unknown seller or shape + Deny +
+
+
+
Automatic ceiling
+
+ Every policy field matches + {spendPolicy.automaticAllowLabel} +
+
+
+
Human ceiling
+
+ Exact one-time approval + {spendPolicy.humanApprovalLabel} +
+
+
+
Session budget
+
+ {formatAtomic(remainingAtomic)} remaining + {spendPolicy.sessionBudgetLabel} +
+
+
+

+ Offline fixture · immutable policy hash · no credential loaded +

+
+ +
+ +

Skill Asset Protocol

+

Unsigned session projection

+

+ Illustrative only · not a SignedReceipt · no transaction broadcast +

+
+
+
+
Projection
+
{demoSessionProjection.projectionId}
+
+
+
Agent
+
pi-coding-agent
+
+
+
Wallet
+
{demoWallet.address}
+
+
+
Network
+
{demoWallet.network}
+
+
+
Policy
+
{demoSessionProjection.policyVersionHash}
+
+
+
Projected outcomes
+
{demoSessionProjection.outcomeStates.join(' · ')}
+
+
+
Settlement
+
{demoSessionProjection.settlementStatus}
+
+
+
+
+ Simulated charged total + {formatAtomic(chargedAtomic)} +
+

+ Unsigned fixture · no key used · not live evidence +

+
+
+ +
+
+

Sandbox complete

+

+ One request auto-cleared, one was denied, and one approval stopped + until a deliberate exact retry—with no key, payment, or network call. +

+
+ + Scope a customer-hosted pilot + + +
+ + ); +} + +export function SpendControlSandbox({ pilotUrl }: { pilotUrl: string }) { + const [state, dispatch] = useReducer( + spendSandboxReducer, + INITIAL_SPEND_SANDBOX_STATE, + ); + const view = spendSandboxView(state); + const next = nextSpendSandboxAction(state.stage); + const approvalAttempt = view.attempts.find( + (attempt) => attempt.decision === 'approval_required', + ); + const focusRef = useRef(null); + const didMountRef = useRef(false); + + useEffect(() => { + if (didMountRef.current) focusRef.current?.focus(); + else didMountRef.current = true; + }, [state.stage]); + + return ( +
+
+
+

Offline spend-control sandbox

+

+ Walk a Spend Intent through the Wallet Kernel. +

+
+
+ Illustrative sample + No account, wallet key, network call, payment, or saved data. +
+
+ +
    + {steps.map((step, index) => { + const isInvalidatedRetryStep = + state.stage === 'approval_invalidated' && + step.stage === 'approved_waiting_retry'; + const complete = + !isInvalidatedRetryStep && isStepComplete(step.stage, state.stage); + const current = + state.stage === 'finalized' || state.stage === 'approval_invalidated' + ? index === steps.length - 1 + : spendStageIndex(step.stage) === spendStageIndex(state.stage); + return ( +
  1. + + {complete ? '✓' : String(index + 1).padStart(2, '0')} + + + {step.label} + {step.detail} + +
  2. + ); + })} +
+ +
+
+ +
+
+
+ Wallet + policy · sample + {view.hasPolicy ? 'Policy active' : 'Ready to load'} +
+
+
+ +
+

Customer-hosted Wallet Kernel

+

Northstar Pi spend session

+

+ Ordinary Agent requests in; policy-bound x402 authority out. +

+
+
+ +
+
+
Agent
+
+ + + pi-coding-agent + Local adapter · fictional session + +
+
+
+
Customer-owned wallet
+
+ {demoWallet.address} + {demoWallet.provider} +
+
+
+
Network
+
+ {demoWallet.networkLabel} + {demoWallet.network} · {demoWallet.asset} +
+
+
+
Default
+
+ Deny + {spendPolicy.versionHash} +
+
+
+ +
+ + + Allowed seller: {spendPolicy.allowedSellerOrigin}. The Agent + cannot access credentials, change policy, approve, or sign. + +
+
+
+ +
+
+ Spend journal · local preview + {view.attempts.length}/3 intents +
+ + {view.attempts.length === 0 ? ( +
+ +

No Spend Intents yet.

+

+ Load policy, then run three deterministic Spend Intents. Nothing + leaves this browser tab. +

+
+ ) : ( +
    + {view.attempts.map((attempt, index) => ( +
  1. + +
    +
    + {attempt.purpose} + {attempt.amountLabel} +
    +

    {attempt.resource}

    + + {attempt.sellerOrigin} · call {attempt.callId} + +
    + + {decisionLabel(attempt)} + + {signatureLabel(attempt)} +
    +
    + {attempt.status} +
  2. + ))} +
+ )} + + {approvalAttempt && view.approvalPanelState && ( +
+

Exact authority preview

+

+ {view.approvalPanelState === 'approved_waiting_retry' + ? 'Approved once — waiting for Wielder retry' + : view.approvalPanelState === 'approval_invalidated' + ? 'Approval invalidated — start again' + : 'Operator decision required'} +

+
+
+
Request hash
+
{approvalAttempt.requestHash}
+
+
+
Challenge / quote
+
{approvalAttempt.challengeId}
+
+
+
Approved amount
+
{approvalAttempt.amountLabel}
+
+
+
Wallet
+
{approvalAttempt.wallet}
+
+
+
Policy version
+
{approvalAttempt.policyVersionHash}
+
+
+
Simulated operator
+
+ {view.approvalOperator === null + ? 'Not yet recorded' + : `${view.approvalOperator.label} · ${view.approvalOperator.identity}`} + + Simulated fixture identity · no authentication performed + +
+
+ {view.approvalInvalidationDetail !== null && ( +
+
Retry outcome
+
{view.approvalInvalidationDetail}
+
+ )} +
+
Policy mismatch
+
{approvalAttempt.policyMismatch}
+
+
+
Expiry
+
{approvalAttempt.approvalExpiry}
+
+
+

+ Any changed challenge, seller, resource, request hash, amount, + wallet, policy version, or expiry must start a new approval. +

+
+ )} + + {next && ( +
+
+

Next action

+

{next.note}

+
+ +
+ )} +
+
+ + {view.hasSessionProjection && ( + + )} +
+ ); +} diff --git a/site/app/components/landing/spend-control-model.test.mjs b/site/app/components/landing/spend-control-model.test.mjs new file mode 100644 index 0000000..a23b2a2 --- /dev/null +++ b/site/app/components/landing/spend-control-model.test.mjs @@ -0,0 +1,673 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +import * as model from './spend-control-model.ts'; + +const APPROVAL_NOW_MS = Date.parse('2026-08-02T18:13:00Z'); +const queueApproval = (intent = model.demoSpendIntents[2]) => + model.spendSandboxReducer( + { stage: 'denied' }, + { type: 'QUEUE_APPROVAL', intent }, + ); + +const approvePending = (pending = queueApproval()) => + model.spendSandboxReducer(pending, { + type: 'APPROVE', + nowMs: APPROVAL_NOW_MS, + }); + +const finalizeApproved = (approved = approvePending()) => + model.spendSandboxReducer( + approved, + model.nextSpendSandboxAction('approved_waiting_retry').action, + ); + +test('preview atomic money stays exact within Number safe-integer range', () => { + const atomicEntries = (scope, record) => + Object.entries(record) + .filter(([key]) => key.endsWith('Atomic')) + .map(([key, value]) => [`${scope}.${key}`, value]); + const atomicAmounts = [ + ...atomicEntries('policy', model.spendPolicy), + ...model.demoSpendIntents.flatMap((intent) => + atomicEntries(`intent.${intent.id}`, intent), + ), + ...atomicEntries('projection', model.demoSessionProjection), + ]; + const derivedTotals = []; + let state = model.INITIAL_SPEND_SANDBOX_STATE; + + while (true) { + const view = model.spendSandboxView(state); + derivedTotals.push({ + stage: state.stage, + chargedAtomic: view.chargedAtomic, + remainingAtomic: view.remainingAtomic, + }); + atomicAmounts.push( + ...atomicEntries(`view.${state.stage}`, view), + ); + + const next = model.nextSpendSandboxAction(state.stage); + if (next === null) break; + state = model.spendSandboxReducer(state, next.action); + } + + for (const [label, amount] of atomicAmounts) { + assert.ok( + Number.isSafeInteger(amount) && amount >= 0, + `${label} must be a nonnegative Number safe integer`, + ); + } + + for (const { stage, chargedAtomic, remainingAtomic } of derivedTotals) { + assert.equal( + chargedAtomic + remainingAtomic, + model.spendPolicy.sessionBudgetAtomic, + `${stage} totals must conserve the session budget exactly`, + ); + } +}); + +test('the sample policy is default-deny and bound to Base Sepolia test USDC', () => { + assert.ok(model.spendPolicy, 'spendPolicy must be exported'); + assert.equal(model.spendPolicy.defaultAction, 'deny'); + assert.equal(model.spendPolicy.network, 'eip155:84532'); + assert.equal(model.demoWallet.network, model.spendPolicy.network); + assert.equal(model.demoWallet.asset, 'test USDC'); + assert.equal(model.demoWallet.simulated, true); +}); + +test('sample Spend Intents cover allow, deny, and exact approval decisions', () => { + assert.ok(model.demoSpendIntents, 'demoSpendIntents must be exported'); + assert.equal(model.demoSpendIntents.length, 3); + assert.equal( + new Set(model.demoSpendIntents.map((intent) => intent.callId)).size, + model.demoSpendIntents.length, + ); + assert.deepEqual( + model.demoSpendIntents.map((intent) => intent.decision), + ['allow', 'deny', 'approval_required'], + ); + assert.equal( + model.demoSpendIntents[1].sellerOrigin === + model.spendPolicy.allowedSellerOrigin, + false, + ); +}); + +test('the spend-control sandbox advances only through authorized transitions', () => { + assert.equal(typeof model.spendSandboxReducer, 'function'); + let state = model.INITIAL_SPEND_SANDBOX_STATE; + + state = model.spendSandboxReducer(state, { type: 'RUN_ALLOWED_REQUEST' }); + assert.equal(state.stage, 'ready'); + + state = model.spendSandboxReducer(state, { type: 'LOAD_POLICY' }); + assert.equal(state.stage, 'policy_loaded'); + + state = model.spendSandboxReducer(state, { type: 'RUN_ALLOWED_REQUEST' }); + assert.equal(state.stage, 'auto_allowed'); + assert.equal(model.spendSandboxView(state).attempts.length, 1); + + state = model.spendSandboxReducer(state, { type: 'RUN_DENIED_REQUEST' }); + assert.equal(state.stage, 'denied'); + assert.equal(model.spendSandboxView(state).attempts.at(-1)?.status, 'Denied'); + + state = model.spendSandboxReducer( + state, + model.nextSpendSandboxAction('denied').action, + ); + assert.equal(state.stage, 'approval_pending'); + assert.equal(model.spendSandboxView(state).hasPendingApproval, true); + + state = model.spendSandboxReducer( + state, + model.nextSpendSandboxAction('approval_pending').action, + ); + assert.equal(state.stage, 'approved_waiting_retry'); + assert.equal(model.spendSandboxView(state).hasApprovedPermit, true); + assert.equal( + model.spendSandboxView(state).attempts.at(-1) + ?.hasProjectedSigningBoundary, + false, + ); + + state = model.spendSandboxReducer( + state, + model.nextSpendSandboxAction('approved_waiting_retry').action, + ); + assert.equal(state.stage, 'finalized'); + assert.equal(model.spendSandboxView(state).hasSessionProjection, true); + assert.deepEqual(model.SPEND_SANDBOX_STAGES, [ + 'ready', + 'policy_loaded', + 'auto_allowed', + 'denied', + 'approval_pending', + 'approved_waiting_retry', + 'approval_invalidated', + 'finalized', + ]); +}); + +test('each changed approval binding invalidates the approval and preserves its audit outcome', () => { + const intent = model.demoSpendIntents[2]; + const mutations = [ + ['requestHash', { ...intent, requestHash: `sha256:${'f'.repeat(64)}` }], + ['challengeId', { ...intent, challengeId: 'quote_changed_fixture' }], + [ + 'sellerOrigin', + { ...intent, sellerOrigin: 'https://changed-seller.example' }, + ], + ['resource', { ...intent, resource: '/v1/changed-audit' }], + ['amountAtomic', { ...intent, amountAtomic: intent.amountAtomic + 1 }], + [ + 'wallet', + { ...intent, wallet: '0x82e482e482e482e482e482e482e482e482e482e4' }, + ], + [ + 'policyVersionHash', + { ...intent, policyVersionHash: `sha256:${'e'.repeat(64)}` }, + ], + [ + 'approvalExpiryMs', + { + ...intent, + approvalExpiresAtMs: intent.approvalExpiresAtMs + 1, + }, + ], + ]; + + for (const [field, repeatedIntent] of mutations) { + const approved = approvePending(queueApproval(intent)); + const retried = model.spendSandboxReducer(approved, { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent, + nowMs: Date.parse('2026-08-02T18:14:00Z'), + }); + + assert.notEqual(retried.stage, 'finalized', `${field} must not finalize`); + assert.equal( + retried.stage, + 'approval_invalidated', + `${field} must start again`, + ); + assert.equal(retried.retryOutcome.reason, 'binding_mismatch', field); + assert.deepEqual(retried.approvalIntent, intent, field); + assert.equal( + retried.approvalRecord.operator.evidence, + 'simulated_fixture', + field, + ); + assert.equal( + 'approvalPermit' in retried, + false, + `${field} must clear permit`, + ); + assert.equal( + 'pendingApproval' in retried, + false, + `${field} must not reuse the old approval`, + ); + assert.equal( + model.spendSandboxView(retried).attempts.at(-1)?.status, + 'Approval invalidated — changed request', + field, + ); + } +}); + +test('approval records a simulated operator identity without claiming authentication', () => { + const approved = approvePending(); + const approvalAction = model.nextSpendSandboxAction('approval_pending').action; + + assert.equal(approved.stage, 'approved_waiting_retry'); + assert.deepEqual(approved.approvalPermit.operator, { + identity: 'operator:northstar-local-admin', + label: 'Northstar local operator', + evidence: 'simulated_fixture', + }); + assert.deepEqual( + model.spendSandboxView(approved).approvalOperator, + approved.approvalPermit.operator, + ); + assert.equal('operatorIdentity' in approvalAction, false); +}); + +test('the exact-authority preview exposes the approval operator identity', () => { + const source = readFileSync( + new URL('./SpendControlSandbox.tsx', import.meta.url), + 'utf8', + ); + + assert.match(source, /
Simulated operator<\/dt>/); + assert.match(source, /view\.approvalOperator/); + assert.match(source, /Simulated operator/); + assert.match(source, /no authentication performed/); + assert.doesNotMatch(source, /authenticated operator/i); +}); + +test('approval promotes only the intent binding stored when it was queued', () => { + const approvalIntent = model.demoSpendIntents[2]; + const substitutedIntent = { + ...model.demoSpendIntents[0], + approvalExpiry: approvalIntent.approvalExpiry, + approvalExpiresAtMs: approvalIntent.approvalExpiresAtMs, + }; + const pending = model.spendSandboxReducer( + { stage: 'denied' }, + { type: 'QUEUE_APPROVAL', intent: approvalIntent }, + ); + const approved = model.spendSandboxReducer(pending, { + type: 'APPROVE', + nowMs: Date.parse('2026-08-02T18:14:00Z'), + // Malicious surplus data must not replace the queued binding. + intent: substitutedIntent, + }); + const retried = model.spendSandboxReducer(approved, { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent: substitutedIntent, + nowMs: Date.parse('2026-08-02T18:14:30Z'), + }); + const rejectedQueue = model.spendSandboxReducer( + { stage: 'denied' }, + { type: 'QUEUE_APPROVAL', intent: substitutedIntent }, + ); + + assert.notEqual(retried.stage, 'finalized'); + assert.equal(retried.stage, 'approval_invalidated'); + assert.equal(retried.retryOutcome.reason, 'binding_mismatch'); + assert.deepEqual(retried.approvalIntent, approvalIntent); + assert.equal('pendingApproval' in retried, false); + assert.equal('approvalPermit' in retried, false); + assert.deepEqual(rejectedQueue, { stage: 'denied' }); +}); + +test('the exact permit expiry millisecond cannot finalize', () => { + const intent = model.demoSpendIntents[2]; + const pending = model.spendSandboxReducer( + { stage: 'denied' }, + { type: 'QUEUE_APPROVAL', intent }, + ); + const approved = model.spendSandboxReducer(pending, { + type: 'APPROVE', + nowMs: Date.parse('2026-08-02T18:14:00Z'), + }); + const retried = model.spendSandboxReducer(approved, { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent: intent, + nowMs: intent.approvalExpiresAtMs, + }); + + assert.equal(retried.stage, 'approval_invalidated'); + assert.equal(retried.retryOutcome.reason, 'approval_expired'); + assert.equal(retried.retryOutcome.attemptedAtMs, intent.approvalExpiresAtMs); + assert.deepEqual(retried.approvalIntent, intent); + assert.equal('pendingApproval' in retried, false); + assert.equal('approvalPermit' in retried, false); +}); + +test('queued intent details remain the display source through finalization', () => { + const alternateIntent = { + ...model.demoSpendIntents[2], + id: 'intent_dependency_review', + callId: '0x6b70aa42…4f21', + challengeId: 'quote_dependency_review_0004', + resource: '/v1/dependency-review', + purpose: 'Review dependency risk', + amountAtomic: 700_000, + amountLabel: '0.70 test USDC', + requestHash: `sha256:${'d'.repeat(64)}`, + approvalExpiry: '2026-08-02T18:20:00Z · fictional fixture', + approvalExpiresAtMs: Date.parse('2026-08-02T18:20:00Z'), + policyMismatch: 'Amount is 0.45 test USDC above the automatic ceiling', + }; + const pending = queueApproval(alternateIntent); + const approved = approvePending(pending); + const finalized = model.spendSandboxReducer(approved, { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent: alternateIntent, + nowMs: Date.parse('2026-08-02T18:14:00Z'), + }); + const renderedIntent = (state) => { + const attempt = model.spendSandboxView(state).attempts.at(-1); + return Object.fromEntries( + Object.keys(alternateIntent).map((key) => [key, attempt?.[key]]), + ); + }; + + for (const state of [pending, approved, finalized]) { + assert.deepEqual(renderedIntent(state), alternateIntent); + assert.deepEqual(state.approvalIntent, alternateIntent); + } +}); + +test('the projection preview renders the finalized intent charge', () => { + const alternateIntent = { + ...model.demoSpendIntents[2], + id: 'intent_charge_preview', + callId: '0x7c81bb53…5a32', + challengeId: 'quote_charge_preview_0005', + amountAtomic: 700_000, + amountLabel: '0.70 test USDC', + requestHash: `sha256:${'c'.repeat(64)}`, + }; + const finalized = model.spendSandboxReducer( + approvePending(queueApproval(alternateIntent)), + { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent: alternateIntent, + nowMs: Date.parse('2026-08-02T18:14:00Z'), + }, + ); + const view = model.spendSandboxView(finalized); + const source = readFileSync( + new URL('./SpendControlSandbox.tsx', import.meta.url), + 'utf8', + ); + + assert.equal(view.chargedAtomic, 780_000); + assert.match(source, /chargedAtomic=\{view\.chargedAtomic\}/); + assert.match(source, /formatAtomic\(chargedAtomic\)/); + assert.doesNotMatch( + source, + /formatAtomic\(demoSessionProjection\.chargedAtomic\)/, + ); +}); + +test('invalid retry time invalidates the approval with an honest audit reason', () => { + const intent = model.demoSpendIntents[2]; + const pending = queueApproval(intent); + const invalidRetryTimes = [ + ['negative infinity', Number.NEGATIVE_INFINITY, 'invalid_retry_time'], + ['before approval', APPROVAL_NOW_MS - 1, 'retry_before_approval'], + ['positive infinity', Number.POSITIVE_INFINITY, 'invalid_retry_time'], + ['NaN', Number.NaN, 'invalid_retry_time'], + ]; + + for (const [label, nowMs, expectedReason] of invalidRetryTimes) { + const retried = model.spendSandboxReducer(approvePending(pending), { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent: intent, + nowMs, + }); + + assert.equal(retried.stage, 'approval_invalidated', label); + assert.equal(retried.retryOutcome.reason, expectedReason, label); + assert.deepEqual(retried.approvalIntent, intent, label); + assert.equal('approvalPermit' in retried, false, label); + assert.equal('pendingApproval' in retried, false, label); + } +}); + +test('a retry after the fixture expiry clears approval without finalizing', () => { + const intent = model.demoSpendIntents[2]; + const approved = approvePending(queueApproval(intent)); + const retried = model.spendSandboxReducer(approved, { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent: intent, + nowMs: Date.parse('2026-08-02T18:15:00Z') + 1, + }); + + assert.equal(retried.stage, 'approval_invalidated'); + assert.equal(retried.retryOutcome.reason, 'approval_expired'); + assert.deepEqual(retried.approvalIntent, intent); + assert.equal('pendingApproval' in retried, false); + assert.equal('approvalPermit' in retried, false); + assert.equal( + model.spendSandboxView(retried).attempts.at(-1)?.status, + 'Approval expired — start again', + ); +}); + +test('an invalidated retry is distinct from the earlier unknown-seller denial', () => { + const intent = model.demoSpendIntents[2]; + const retried = model.spendSandboxReducer(approvePending(queueApproval(intent)), { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent: { ...intent, resource: '/v1/substituted-resource' }, + nowMs: Date.parse('2026-08-02T18:14:00Z'), + }); + const view = model.spendSandboxView(retried); + + assert.equal(retried.stage, 'approval_invalidated'); + assert.equal(view.hasApprovedPermit, false); + assert.equal(view.hasSessionProjection, false); + assert.equal(view.approvalPanelState, 'approval_invalidated'); + assert.equal(view.retryOutcome.reason, 'binding_mismatch'); + assert.equal(view.attempts.length, 3); + assert.equal(view.attempts[1]?.status, 'Denied'); + assert.equal( + view.attempts[2]?.status, + 'Approval invalidated — changed request', + ); +}); + +test('operator approval ends without signing until the Wielder repeats the exact request', () => { + const pendingState = queueApproval(); + const pending = model.spendSandboxView(pendingState); + const escalated = pending.attempts.at(-1); + assert.equal(escalated?.decision, 'approval_required'); + assert.equal(escalated?.hasProjectedSigningBoundary, false); + assert.equal(escalated?.status, 'Approval required'); + assert.match(escalated?.requestHash ?? '', /^sha256:[a-f0-9]{64}$/); + assert.equal(escalated?.wallet, model.demoWallet.address); + assert.equal(escalated?.policyVersionHash, model.spendPolicy.versionHash); + assert.ok(escalated?.approvalExpiry); + assert.ok(escalated?.policyMismatch); + + const approved = model + .spendSandboxView(approvePending(pendingState)) + .attempts.at(-1); + assert.equal(approved?.hasProjectedSigningBoundary, false); + assert.equal(approved?.status, 'Approved — waiting for retry'); + + const finalized = model.spendSandboxView(finalizeApproved()); + const retried = finalized.attempts.at(-1); + assert.equal(retried?.hasProjectedSigningBoundary, true); + assert.equal('signatureCreated' in retried, false); + assert.equal(retried?.status, 'Simulated finalized'); + assert.equal(finalized.attempts[0]?.status, 'Simulated allowed'); +}); + +test('the finalized flow no longer exposes an approval panel waiting for retry', () => { + assert.equal( + model.spendSandboxView(approvePending()).approvalPanelState, + 'approved_waiting_retry', + ); + assert.equal( + model.spendSandboxView(finalizeApproved()).approvalPanelState, + null, + ); +}); + +test('the final artifact is an unsigned session projection, not a signed receipt', () => { + assert.ok( + model.demoSessionProjection, + 'demoSessionProjection must be exported', + ); + assert.equal('demoReceipt' in model, false); + const view = model.spendSandboxView(finalizeApproved()); + assert.equal(view.chargedAtomic, 680_000); + assert.equal(view.remainingAtomic, 4_320_000); + assert.equal( + view.remainingAtomic, + model.spendPolicy.sessionBudgetAtomic - view.chargedAtomic, + ); + assert.equal(model.demoSessionProjection.chargedAtomic, view.chargedAtomic); + assert.equal(model.demoSessionProjection.simulated, true); + assert.equal(model.demoSessionProjection.unsigned, true); + assert.equal(model.demoSessionProjection.settlementStatus, 'not_broadcast'); + assert.equal('signatureAlgorithm' in model.demoSessionProjection, false); + assert.deepEqual(model.demoSessionProjection.projectedIntentIds, [ + 'intent_model_context', + 'intent_unknown_seller', + 'intent_repo_audit', + ]); +}); + +test('each active stage exposes one explicit next operator action', () => { + assert.equal(typeof model.nextSpendSandboxAction, 'function'); + assert.equal( + model.nextSpendSandboxAction('ready')?.action.type, + 'LOAD_POLICY', + ); + assert.equal( + model.nextSpendSandboxAction('policy_loaded')?.action.type, + 'RUN_ALLOWED_REQUEST', + ); + assert.equal( + model.nextSpendSandboxAction('auto_allowed')?.action.type, + 'RUN_DENIED_REQUEST', + ); + assert.equal( + model.nextSpendSandboxAction('denied')?.action.type, + 'QUEUE_APPROVAL', + ); + assert.equal( + model.nextSpendSandboxAction('approval_pending')?.action.type, + 'APPROVE', + ); + assert.equal( + model.nextSpendSandboxAction('approved_waiting_retry')?.action.type, + 'RETRY_APPROVED_REQUEST', + ); + assert.equal(model.nextSpendSandboxAction('approval_invalidated'), null); + assert.equal(model.nextSpendSandboxAction('finalized'), null); +}); + +test('Hero and metadata independently lead with the Wallet Kernel', () => { + const hero = readFileSync(new URL('./Hero.tsx', import.meta.url), 'utf8'); + const layout = readFileSync( + new URL('../../layout.tsx', import.meta.url), + 'utf8', + ); + + assert.match(hero, /customer-hosted Wallet Kernel/i); + assert.match(layout, /customer-hosted Wallet Kernel/i); + assert.doesNotMatch(hero, /reward close|reward program/i); +}); + +test('metadata describes a pre-release preview with planned receipts', () => { + const source = readFileSync( + new URL('../../layout.tsx', import.meta.url), + 'utf8', + ); + const descriptions = [ + ...source.matchAll(/description:\s*(?:\n\s*)?["']([^"']+)["']/g), + ].map((match) => match[1]); + + assert.equal(descriptions.length, 2); + for (const description of descriptions) { + assert.match(description, /pre-release offline preview/i); + assert.match(description, /planned signed receipts/i); + } +}); + +test('landing content release-gates receipt and settlement capabilities', () => { + const source = readFileSync( + new URL('../../landing-content.ts', import.meta.url), + 'utf8', + ).replace(/\s+/g, ' '); + + assert.match(source, /eyebrow: 'Planned receipt'/); + assert.match( + source, + /A release-gated flow can anchor settlement on-chain .* in a signed local receipt\./, + ); + assert.match(source, /Release-gated signed receipts will let teams inspect/); + assert.match(source, /label: 'release-gated payment rail'/); + assert.match( + source, + /value: 'UNSIGNED'.*label: 'browser artifact'.*not broadcast and not live evidence/, + ); + assert.doesNotMatch( + source, + /value: 'SIGNED'.*label: 'terminal receipts'/, + ); + assert.doesNotMatch(source, /48 settled x402 calls/i); +}); + +test('Hero presents signed receipts as planned and release-gated', () => { + const source = readFileSync( + new URL('./Hero.tsx', import.meta.url), + 'utf8', + ).replace(/\s+/g, ' '); + + assert.match(source, /planned signed receipts behind the release gate/i); +}); + +test('ProofLoop keeps planned on-chain settlement behind the release gate', () => { + const source = readFileSync( + new URL('./ProofLoop.tsx', import.meta.url), + 'utf8', + ).replace(/\s+/g, ' '); + + assert.match( + source, + /Planned on-chain settlement stays behind the release gate\./, + ); + assert.doesNotMatch(source, /chain is optional plumbing/i); +}); + +test('pilot copy plans receipt export and allow-lists testnet sellers', () => { + const source = readFileSync( + new URL('./PilotCta.tsx', import.meta.url), + 'utf8', + ).replace(/\s+/g, ' '); + + assert.match(source, /planned signed receipt export/i); + assert.match(source, /Allow-listed Base Sepolia x402 sellers/); + assert.doesNotMatch(source, /Approved Base Sepolia x402 resource servers/); +}); + +test('page evidence is unsigned and says live settlement evidence is not run', () => { + const source = readFileSync( + new URL('../../page.tsx', import.meta.url), + 'utf8', + ).replace(/\s+/g, ' '); + + assert.match(source, /unsigned projection that is not broadcast/i); + assert.match( + source, + /live CDP payment and live testnet settlement evidence remain not run/i, + ); + assert.match(source, /customer-owned wallet/i); + assert.match(source, /publication gate not cleared/i); +}); + +test('sandbox artifact is not a signed receipt or transaction broadcast', () => { + const source = readFileSync( + new URL('./SpendControlSandbox.tsx', import.meta.url), + 'utf8', + ).replace(/\s+/g, ' '); + + assert.match(source, /unsigned session projection/i); + assert.match(source, /not a SignedReceipt/i); + assert.match(source, /no transaction broadcast/i); +}); + +test('the historical proof advertises neither a live endpoint nor unpublished latency', () => { + const files = [ + '../../../../README.md', + '../../../README.md', + '../../proof/page.tsx', + '../../content.ts', + '../../manifesto.tsx', + ]; + const copy = files + .map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')) + .join('\n'); + + assert.match(copy, /static archive/i); + assert.match(copy, /no live (?:payment )?endpoint/i); + assert.doesNotMatch(copy, /live x402 endpoint/i); + assert.doesNotMatch(copy, /nothing real is at risk/i); + assert.doesNotMatch(copy, /payment overhead/i); + assert.doesNotMatch(copy, /every cent reconciled/i); + assert.doesNotMatch(copy, /receipts are real/i); + assert.doesNotMatch(copy, /the skill stayed home/i); + assert.doesNotMatch(copy, /never the skill/i); + assert.doesNotMatch(copy, /anything that can pay can invoke/i); +}); diff --git a/site/app/components/landing/spend-control-model.ts b/site/app/components/landing/spend-control-model.ts new file mode 100644 index 0000000..31246da --- /dev/null +++ b/site/app/components/landing/spend-control-model.ts @@ -0,0 +1,516 @@ +export const demoWallet = { + provider: 'Customer-owned CDP wallet', + address: '0x71d371d371d371d371d371d371d371d371d371d3', + network: 'eip155:84532', + networkLabel: 'Base Sepolia', + asset: 'test USDC', + simulated: true, +} as const; + +export const demoOperator = { + identity: 'operator:northstar-local-admin', + label: 'Northstar local operator', + evidence: 'simulated_fixture', +} as const; + +export const spendPolicy = { + name: 'Pilot spend policy v1', + network: 'eip155:84532', + asset: 'test USDC', + allowedSellerOrigin: 'https://api.northstar.example', + sessionBudgetAtomic: 5_000_000, + sessionBudgetLabel: '5.00 test USDC', + automaticAllowAtomic: 250_000, + automaticAllowLabel: '0.25 test USDC', + humanApprovalAtomic: 1_000_000, + humanApprovalLabel: '1.00 test USDC', + defaultAction: 'deny', + versionHash: + 'sha256:74db8f3e74db8f3e74db8f3e74db8f3e74db8f3e74db8f3e74db8f3e74db8f3e', +} as const; + +export interface DemoSpendIntent { + id: string; + callId: string; + challengeId: string; + sellerOrigin: string; + resource: string; + purpose: string; + amountAtomic: number; + amountLabel: string; + decision: 'allow' | 'approval_required' | 'deny'; + requestHash: string; + wallet: string; + policyVersionHash: string; + approvalExpiry: string | null; + approvalExpiresAtMs: number | null; + policyMismatch: string; +} + +export const demoSpendIntents: readonly DemoSpendIntent[] = [ + { + id: 'intent_model_context', + callId: '0x8c0d2f4c…b811', + challengeId: 'quote_model_context_0001', + sellerOrigin: spendPolicy.allowedSellerOrigin, + resource: '/v1/model-context', + purpose: 'Fetch bounded model context', + amountAtomic: 80_000, + amountLabel: '0.08 test USDC', + decision: 'allow', + requestHash: + 'sha256:10a210a210a210a210a210a210a210a210a210a210a210a210a210a210a210a2', + wallet: demoWallet.address, + policyVersionHash: spendPolicy.versionHash, + approvalExpiry: null, + approvalExpiresAtMs: null, + policyMismatch: 'None — every policy field matches', + }, + { + id: 'intent_unknown_seller', + callId: '0x10f7b018…d920', + challengeId: 'quote_unknown_seller_0002', + sellerOrigin: 'https://unknown-seller.example', + resource: '/v1/cheap-context', + purpose: 'Try an unapproved seller', + amountAtomic: 10_000, + amountLabel: '0.01 test USDC', + decision: 'deny', + requestHash: + 'sha256:20b320b320b320b320b320b320b320b320b320b320b320b320b320b320b320b3', + wallet: demoWallet.address, + policyVersionHash: spendPolicy.versionHash, + approvalExpiry: null, + approvalExpiresAtMs: null, + policyMismatch: 'Seller origin is not allow-listed', + }, + { + id: 'intent_repo_audit', + callId: '0x9a61ed72…1c40', + challengeId: 'quote_repo_audit_0003', + sellerOrigin: spendPolicy.allowedSellerOrigin, + resource: '/v1/repository-audit', + purpose: 'Run repository risk audit', + amountAtomic: 600_000, + amountLabel: '0.60 test USDC', + decision: 'approval_required', + requestHash: + 'sha256:4ce14ce14ce14ce14ce14ce14ce14ce14ce14ce14ce14ce14ce14ce14ce14ce1', + wallet: demoWallet.address, + policyVersionHash: spendPolicy.versionHash, + approvalExpiry: '2026-08-02T18:15:00Z · fictional fixture', + approvalExpiresAtMs: Date.parse('2026-08-02T18:15:00Z'), + policyMismatch: 'Amount is 0.35 test USDC above the automatic ceiling', + }, +] as const; + +export const demoSessionProjection = { + projectionId: 'session_projection_demo_0001', + policyVersionHash: spendPolicy.versionHash, + network: spendPolicy.network, + chargedAtomic: 680_000, + outcomeStates: ['simulated_allowed', 'denied', 'simulated_finalized'], + settlementStatus: 'not_broadcast', + projectedIntentIds: demoSpendIntents.map((intent) => intent.id), + unsigned: true, + simulated: true, +} as const; + +export const SPEND_SANDBOX_STAGES = [ + 'ready', + 'policy_loaded', + 'auto_allowed', + 'denied', + 'approval_pending', + 'approved_waiting_retry', + 'approval_invalidated', + 'finalized', +] as const; + +export type SpendSandboxStage = (typeof SPEND_SANDBOX_STAGES)[number]; + +export interface SpendApprovalBinding { + requestHash: string; + challengeId: string; + sellerOrigin: string; + resource: string; + amountAtomic: number; + wallet: string; + policyVersionHash: string; + expiresAtMs: number; +} + +export interface SpendApprovalPermit extends SpendApprovalBinding { + approvedAtMs: number; + operator: typeof demoOperator; +} + +export type RetryInvalidationReason = + | 'binding_mismatch' + | 'approval_expired' + | 'retry_before_approval' + | 'invalid_retry_time'; + +export interface SpendRetryOutcome { + reason: RetryInvalidationReason; + attemptedAtMs: number; +} + +export type SpendSandboxState = + | { + stage: Exclude< + SpendSandboxStage, + | 'approval_pending' + | 'approved_waiting_retry' + | 'approval_invalidated' + | 'finalized' + >; + } + | { + stage: 'approval_pending'; + approvalIntent: DemoSpendIntent; + pendingApproval: SpendApprovalBinding; + } + | { + stage: 'approved_waiting_retry'; + approvalIntent: DemoSpendIntent; + approvalPermit: SpendApprovalPermit; + } + | { + stage: 'approval_invalidated'; + approvalIntent: DemoSpendIntent; + approvalRecord: SpendApprovalPermit; + retryOutcome: SpendRetryOutcome; + } + | { + stage: 'finalized'; + approvalIntent: DemoSpendIntent; + approvalPermit: SpendApprovalPermit; + }; + +export type SpendSandboxAction = + | { type: 'LOAD_POLICY' } + | { type: 'RUN_ALLOWED_REQUEST' } + | { type: 'RUN_DENIED_REQUEST' } + | { type: 'QUEUE_APPROVAL'; intent: DemoSpendIntent } + | { type: 'APPROVE'; nowMs: number } + | { + type: 'RETRY_APPROVED_REQUEST'; + repeatedIntent: DemoSpendIntent; + nowMs: number; + } + | { type: 'RESET' }; + +export interface DemoSpendAttempt extends DemoSpendIntent { + hasProjectedSigningBoundary: boolean; + status: + | 'Simulated allowed' + | 'Denied' + | 'Approval required' + | 'Approved — waiting for retry' + | 'Approval invalidated — changed request' + | 'Approval expired — start again' + | 'Approval invalidated — retry predates approval' + | 'Approval invalidated — invalid retry time' + | 'Simulated finalized'; +} + +export const INITIAL_SPEND_SANDBOX_STATE: SpendSandboxState = { + stage: 'ready', +}; + +export const spendStageIndex = (stage: SpendSandboxStage): number => + SPEND_SANDBOX_STAGES.indexOf(stage); + +function approvalBindingFor( + intent: DemoSpendIntent, +): SpendApprovalBinding | null { + if ( + intent.decision !== 'approval_required' || + intent.approvalExpiresAtMs === null + ) { + return null; + } + + return { + requestHash: intent.requestHash, + challengeId: intent.challengeId, + sellerOrigin: intent.sellerOrigin, + resource: intent.resource, + amountAtomic: intent.amountAtomic, + wallet: intent.wallet, + policyVersionHash: intent.policyVersionHash, + expiresAtMs: intent.approvalExpiresAtMs, + }; +} + +function permitMatchesRepeatedIntent( + permit: SpendApprovalBinding, + intent: DemoSpendIntent, +): boolean { + return ( + permit.requestHash === intent.requestHash && + permit.challengeId === intent.challengeId && + permit.sellerOrigin === intent.sellerOrigin && + permit.resource === intent.resource && + permit.amountAtomic === intent.amountAtomic && + permit.wallet === intent.wallet && + permit.policyVersionHash === intent.policyVersionHash && + permit.expiresAtMs === intent.approvalExpiresAtMs + ); +} + +function retryInvalidationReason( + permit: SpendApprovalPermit, + repeatedIntent: DemoSpendIntent, + nowMs: number, +): RetryInvalidationReason | null { + if (!Number.isFinite(nowMs)) return 'invalid_retry_time'; + if (nowMs < permit.approvedAtMs) return 'retry_before_approval'; + if (nowMs >= permit.expiresAtMs) return 'approval_expired'; + if (!permitMatchesRepeatedIntent(permit, repeatedIntent)) { + return 'binding_mismatch'; + } + return null; +} + +function invalidatedAttemptStatus( + reason: RetryInvalidationReason, +): DemoSpendAttempt['status'] { + switch (reason) { + case 'binding_mismatch': + return 'Approval invalidated — changed request'; + case 'approval_expired': + return 'Approval expired — start again'; + case 'retry_before_approval': + return 'Approval invalidated — retry predates approval'; + case 'invalid_retry_time': + return 'Approval invalidated — invalid retry time'; + } +} + +function invalidatedAttemptDetail(reason: RetryInvalidationReason): string { + switch (reason) { + case 'binding_mismatch': + return 'Repeated request did not match the exact approval. Start a new request.'; + case 'approval_expired': + return 'Approval expired before retry. Start a new request.'; + case 'retry_before_approval': + return 'Retry timestamp predates the approval. Start a new request.'; + case 'invalid_retry_time': + return 'Retry timestamp was invalid. Start a new request.'; + } +} + +export function spendSandboxReducer( + state: SpendSandboxState, + action: SpendSandboxAction, +): SpendSandboxState { + switch (action.type) { + case 'LOAD_POLICY': + return state.stage === 'ready' ? { stage: 'policy_loaded' } : state; + case 'RUN_ALLOWED_REQUEST': + return state.stage === 'policy_loaded' ? { stage: 'auto_allowed' } : state; + case 'RUN_DENIED_REQUEST': + return state.stage === 'auto_allowed' ? { stage: 'denied' } : state; + case 'QUEUE_APPROVAL': { + if (state.stage !== 'denied') return state; + const pendingApproval = approvalBindingFor(action.intent); + return pendingApproval + ? { + stage: 'approval_pending', + approvalIntent: { ...action.intent }, + pendingApproval, + } + : state; + } + case 'APPROVE': { + if (state.stage !== 'approval_pending') return state; + if ( + !Number.isFinite(action.nowMs) || + !(action.nowMs < state.pendingApproval.expiresAtMs) + ) { + return state; + } + return { + stage: 'approved_waiting_retry', + approvalIntent: state.approvalIntent, + approvalPermit: { + ...state.pendingApproval, + approvedAtMs: action.nowMs, + operator: demoOperator, + }, + }; + } + case 'RETRY_APPROVED_REQUEST': { + if (state.stage !== 'approved_waiting_retry') return state; + const permit = state.approvalPermit; + const invalidationReason = retryInvalidationReason( + permit, + action.repeatedIntent, + action.nowMs, + ); + if (invalidationReason !== null) { + return { + stage: 'approval_invalidated', + approvalIntent: state.approvalIntent, + approvalRecord: permit, + retryOutcome: { + reason: invalidationReason, + attemptedAtMs: action.nowMs, + }, + }; + } + return { + stage: 'finalized', + approvalIntent: state.approvalIntent, + approvalPermit: permit, + }; + } + case 'RESET': + return INITIAL_SPEND_SANDBOX_STATE; + default: + return state; + } +} + +export function spendSandboxView(state: SpendSandboxState) { + const index = spendStageIndex(state.stage); + const hasAutoAllowed = index >= spendStageIndex('auto_allowed'); + const hasDenied = index >= spendStageIndex('denied'); + const approvalIntent = + state.stage === 'approval_pending' || + state.stage === 'approved_waiting_retry' || + state.stage === 'approval_invalidated' || + state.stage === 'finalized' + ? state.approvalIntent + : null; + const hasApprovalAttempt = approvalIntent !== null; + const isApprovedWaitingRetry = state.stage === 'approved_waiting_retry'; + const hasApprovedPermit = isApprovedWaitingRetry; + const approvalRecord = + state.stage === 'approved_waiting_retry' || state.stage === 'finalized' + ? state.approvalPermit + : state.stage === 'approval_invalidated' + ? state.approvalRecord + : null; + const retryOutcome = + state.stage === 'approval_invalidated' ? state.retryOutcome : null; + const hasSessionProjection = state.stage === 'finalized'; + const approvalPanelState = + state.stage === 'approval_pending' + ? 'approval_required' + : state.stage === 'approved_waiting_retry' + ? 'approved_waiting_retry' + : state.stage === 'approval_invalidated' + ? 'approval_invalidated' + : null; + const attempts: DemoSpendAttempt[] = []; + + if (hasAutoAllowed) { + attempts.push({ + ...demoSpendIntents[0], + hasProjectedSigningBoundary: true, + status: 'Simulated allowed', + }); + } + + if (hasDenied) { + attempts.push({ + ...demoSpendIntents[1], + hasProjectedSigningBoundary: false, + status: 'Denied', + }); + } + + if (hasApprovalAttempt) { + let status: DemoSpendAttempt['status'] = 'Approval required'; + if (isApprovedWaitingRetry) status = 'Approved — waiting for retry'; + if (retryOutcome !== null) { + status = invalidatedAttemptStatus(retryOutcome.reason); + } + if (hasSessionProjection) status = 'Simulated finalized'; + attempts.push({ + ...approvalIntent, + hasProjectedSigningBoundary: hasSessionProjection, + status, + }); + } + + const chargedAtomic = hasAutoAllowed + ? demoSpendIntents[0].amountAtomic + + (state.stage === 'finalized' ? state.approvalIntent.amountAtomic : 0) + : 0; + + return { + hasPolicy: index >= spendStageIndex('policy_loaded'), + attempts, + hasPendingApproval: state.stage === 'approval_pending', + hasApprovedPermit, + hasSessionProjection, + approvalPanelState, + approvalOperator: approvalRecord?.operator ?? null, + retryOutcome, + approvalInvalidationDetail: + retryOutcome === null + ? null + : invalidatedAttemptDetail(retryOutcome.reason), + chargedAtomic, + remainingAtomic: spendPolicy.sessionBudgetAtomic - chargedAtomic, + }; +} + +export function nextSpendSandboxAction(stage: SpendSandboxStage): { + action: SpendSandboxAction; + label: string; + note: string; +} | null { + switch (stage) { + case 'ready': + return { + action: { type: 'LOAD_POLICY' }, + label: 'Load the sample policy', + note: 'Creates a local default-deny policy. No wallet is contacted.', + }; + case 'policy_loaded': + return { + action: { type: 'RUN_ALLOWED_REQUEST' }, + label: 'Run an in-policy request', + note: 'Simulates one exact x402 payment below the automatic ceiling.', + }; + case 'auto_allowed': + return { + action: { type: 'RUN_DENIED_REQUEST' }, + label: 'Try an unknown seller', + note: 'Shows default deny before any wallet or signing path is reached.', + }; + case 'denied': + return { + action: { type: 'QUEUE_APPROVAL', intent: demoSpendIntents[2] }, + label: 'Try a larger request', + note: 'Creates an approval request without creating a payment signature.', + }; + case 'approval_pending': + return { + action: { + type: 'APPROVE', + nowMs: Date.parse('2026-08-02T18:13:00Z'), + }, + label: 'Approve this exact intent', + note: 'Records one bounded approval, then stops. No signature or retry occurs.', + }; + case 'approved_waiting_retry': + return { + action: { + type: 'RETRY_APPROVED_REQUEST', + repeatedIntent: demoSpendIntents[2], + nowMs: Date.parse('2026-08-02T18:14:00Z'), + }, + label: 'Repeat the exact Agent request', + note: 'Simulates deliberate Wielder retry, exact revalidation, and an unsigned outcome projection.', + }; + case 'approval_invalidated': + case 'finalized': + return null; + } +} diff --git a/site/app/components/useInvoke.ts b/site/app/components/useInvoke.ts deleted file mode 100644 index 36e80a5..0000000 --- a/site/app/components/useInvoke.ts +++ /dev/null @@ -1,120 +0,0 @@ -'use client'; - -// useInvoke — headless hook wrapping the x402 pay-and-invoke flow against -// /api/invoke/optimizing-claude-code-prompts. Variants style it themselves. -// -// State machine: -// idle → connecting (wallet + chain) → paying (402 → sign → settle) -// → running (skill executing / response streaming) → done | error -// -// connect() is the standalone STEP 01: wallet connect + chain switch with no -// payment attached. invoke() reuses the session when one exists. - -import { useCallback, useEffect, useRef, useState } from 'react'; -import { - connectWallet, - ensureBaseSepolia, - hasWallet, - payAndFetch, - type PaidReceipt, - type WalletSession, -} from '../lib/x402-client'; - -export type InvokeState = 'idle' | 'connecting' | 'paying' | 'running' | 'done' | 'error'; - -export interface InvokeResult { - state: InvokeState; - output: string | null; - paid: PaidReceipt | null; - error: string | null; - account: string | null; - connect: () => Promise; - invoke: (input: string) => Promise; - needsWallet: boolean; -} - -const SKILL_ENDPOINT = '/api/invoke/optimizing-claude-code-prompts'; - -export function useInvoke(): InvokeResult { - const [state, setState] = useState('idle'); - const [output, setOutput] = useState(null); - const [paid, setPaid] = useState(null); - const [error, setError] = useState(null); - const [account, setAccount] = useState(null); - const sessionRef = useRef(null); - // Detected in an effect (not during render) to avoid SSR hydration mismatches. - const [needsWallet, setNeedsWallet] = useState(false); - - useEffect(() => { - setNeedsWallet(!hasWallet()); - }, []); - - const openSession = useCallback(async (): Promise => { - if (!hasWallet()) { - setNeedsWallet(true); - throw new Error('No wallet detected — install MetaMask (or any injected wallet) to pay.'); - } - const session = await connectWallet(); - sessionRef.current = session; - setAccount(session.account); - return session; - }, []); - - const connect = useCallback(async () => { - setError(null); - setState('connecting'); - try { - await openSession(); - setState('idle'); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - setState('error'); - } - }, [openSession]); - - const invoke = useCallback( - async (input: string) => { - setError(null); - setOutput(null); - setPaid(null); - setState('connecting'); - try { - const session = sessionRef.current ?? (await openSession()); - // The user may have moved networks since connecting; no-op when not. - await ensureBaseSepolia(session.provider); - - setState('paying'); - const { response, paid: receipt } = await payAndFetch( - SKILL_ENDPOINT, - { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ input }), - }, - session, - ); - - setState('running'); - const data = (await response.json().catch(() => null)) as { - output?: string; - paid?: PaidReceipt; - error?: string; - } | null; - - if (!response.ok) { - throw new Error(data?.error ?? `invoke failed with status ${response.status}`); - } - - setOutput(data?.output ?? ''); - setPaid(data?.paid ?? receipt ?? null); - setState('done'); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - setState('error'); - } - }, - [openSession], - ); - - return { state, output, paid, error, account, connect, invoke, needsWallet }; -} diff --git a/site/app/content.ts b/site/app/content.ts index 1609824..30a7a27 100644 --- a/site/app/content.ts +++ b/site/app/content.ts @@ -1,43 +1,41 @@ export const manifesto = { - title: '“THE PROOF”', - subtitle: 'A MANIFESTO FOR THE SOVEREIGNTY OF AUTHORED WORK', + title: '“THE PROOF — ARCHIVE”', + subtitle: 'AN ARCHIVED MANIFESTO FOR THE SOVEREIGNTY OF AUTHORED WORK', project: 'SKILL ASSET PROTOCOL', - preamble: 'You encoded what you know into an artifact that works without you. Under the old default — “work for hire” — that artifact is confiscated at the moment of its creation. 100 for them. 0 for you. We reject the default.', + preamble: 'This archived manifesto records the project’s earlier authored-work thesis. It is preserved as design history—not as current product doctrine, a legal conclusion, or evidence that the broader compensation model has been validated.', principles: [ - { n: '01', head: '“YOUR EXPERTISE IS AN ASSET.”', body: 'Not a donation. The moment you encode it, it becomes property. Yours.' }, - { n: '02', head: '“THE ARTIFACT IS NEVER HANDED OVER.”', body: 'The world gets the output. The work stays home.' }, - { n: '03', head: '“NO CREDENTIAL, NO RUN.”', body: 'Payment is not a request. It is the key in the ignition.' }, - { n: '04', head: '“THE WIELDER IS A WALLET.”', body: 'Not a platform. Not a harness. Anything that can pay can invoke. No gatekeepers.' }, - { n: '05', head: '“PROVENANCE IS MEMORY.”', body: 'Every fork declares its ancestry, on a ledger no one can edit.' }, - { n: '06', head: '“THE CLAIM IS CO-HELD, NOT SURRENDERED.”', body: '100/0 was a choice, not a law of nature. We choose different splits.' }, - { n: '07', head: '“SECRECY IS NOT THE MOAT.”', body: 'Evolution is. A clone of yesterday’s work competes with what you ship tomorrow.' }, + { n: '01', head: '“EXPERTISE CAN BECOME AN ASSET.”', body: 'An authored Skill can be governed as a durable work artifact. Legal and economic rights still depend on the governing agreement.' }, + { n: '02', head: '“THE INTERFACE NEED NOT RETURN THE ARTIFACT.”', body: 'The historical seller omitted the Skill file from its response. Model output can still leak or enable reconstruction; extraction resistance is not guaranteed.' }, + { n: '03', head: '“AUTHORIZED PAYMENT BEFORE A PAID RUN.”', body: 'The historical test exercised a payment-gated path once. The website no longer operates that paid endpoint.' }, + { n: '04', head: '“THE WIELDER HAS A WALLET BOUNDARY.”', body: 'The current Wallet Kernel adds customer policy, exact authority, and operator control between an Agent request and any signature.' }, + { n: '05', head: '“PROVENANCE IS DECLARED MEMORY.”', body: 'A ledger can preserve a declaration and its history. It does not prove originality, authorship, safety, or legal ownership.' }, + { n: '06', head: '“COMPENSATION IS A DESIGN CHOICE.”', body: 'Creator compensation remains deferred research. No split or Royalty claim is part of the current v1 offer.' }, + { n: '07', head: '“SECRECY ALONE IS NOT THE MOAT.”', body: 'Evolution may matter, but the retained evidence does not establish durable extraction resistance or clone economics at scale.' }, { n: '08', head: '“SAY THE QUIET PART ON-CHAIN.”', body: 'Eventually consistent. Unvalidated. Unmeasured. Honesty is a feature, printed on the receipt.' }, { n: '09', head: '“THE CHAIN IS PLUMBING.”', body: 'Nobody should type a token ticker to get paid for their work.' }, - { n: '10', head: '“PROOF OVER PROMISE.”', body: 'A manifesto without receipts is a poster. Scroll down. The receipts are real.' }, + { n: '10', head: '“PROOF OVER PROMISE.”', body: 'One retained transaction receipt supports one narrow historical claim. Everything else remains bounded or unvalidated.' }, ], proof: { heading: '“PROOF”', - intro: 'First real-network run, 2026-07-12. One wallet. Two asset classes. Every cent reconciled on a public chain.', - ledger: 'claude/plan $0.041 · skill/optimizing-claude-code-prompts $0.25 → creator $0.24375 / treasury $0.00625', + intro: 'Historical evidence, verified 2026-07-17: one successful Base Sepolia test-USDC transfer exists, and the 2026-07-12 run log labels it as the Skill-leg settlement.', + ledger: '0.25 test USDC · 0xdddf…053f → 0x2500…f189 · block 44053992', receipts: [ - { label: 'model invocation — $0.041 settled', tx: '0x01daa723f23a6e2bbfb67b5077a25b37e6b97827b82013152c96da9d0638ff49' }, - { label: 'skill invocation — $0.25 → split', tx: '0xaf1ba2fe508ee9d6bfe0823e25a05fc8b05c8dbac007b40b7d36dbbe447af522' }, + { label: 'historical Skill-leg transfer — 0.25 test USDC', tx: '0xaf1ba2fe508ee9d6bfe0823e25a05fc8b05c8dbac007b40b7d36dbbe447af522' }, ], basescan: 'https://sepolia.basescan.org/tx/', - overhead: 'payment overhead ≈ 781ms per call. 402 → sign → settle.', + overhead: 'This receipt does not prove current endpoint behavior, latency, royalty-split correctness, Skill output, demand, or production readiness.', }, doIt: { - heading: '“DO IT YOURSELF”', - intro: 'This page is not a brochure. It is a metered endpoint. Pay $0.25 in testnet USDC and the hosted skill runs for you — you get the output, never the skill.', - monopoly: '“MONOPOLY MONEY” — this runs on Base Sepolia, a practice network. The USDC is free from the faucet. Nothing real is at risk.', - faucet: 'https://faucet.circle.com', - steps: ['CONNECT A WALLET', 'GET FREE TESTNET USDC', 'PAY $0.25 — SIGN, DON’T SEND', 'RECEIVE OUTPUT. NEVER THE SKILL.'], + heading: '“ENDPOINT RETIRED”', + intro: 'The browser-wallet invocation experiment is no longer served by this website. The retained transaction link below is historical evidence, not an invitation to connect a wallet or pay.', + monopoly: '“STATIC ARCHIVE” — no wallet connection, payment signature, transaction broadcast, or model call is available on this page.', + steps: ['NO WALLET CONNECTION', 'NO PAYMENT SIGNATURE', 'NO MODEL CALL', 'HISTORICAL RECEIPT ONLY'], }, footer: { code: 'https://github.com/Aznatkoiny/skill-asset-protocol', license: 'APACHE-2.0', credit: 'c/o ANTONY ZAKI — 2026', - disclaimer: '“RESEARCH” — testnet only. Not an offer of securities. The claim is the receipt.', + disclaimer: '“RESEARCH” — historical testnet evidence only. No mainnet. Not production-ready.', }, }; export type Manifesto = typeof manifesto; diff --git a/site/app/globals.css b/site/app/globals.css index a2dc41e..2684618 100644 --- a/site/app/globals.css +++ b/site/app/globals.css @@ -1,8 +1,8 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #f4f1e8; + --foreground: #0a0a0a; } @theme inline { @@ -12,15 +12,31 @@ --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; } body { + min-width: 320px; + margin: 0; background: var(--background); color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +button, +input, +textarea, +select { + font: inherit; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} diff --git a/site/app/landing-content.ts b/site/app/landing-content.ts new file mode 100644 index 0000000..37d025f --- /dev/null +++ b/site/app/landing-content.ts @@ -0,0 +1,67 @@ +export const productProof = [ + { + number: '01', + eyebrow: 'Spend Intent', + title: 'Capture the exact request.', + body: 'Turn an ordinary Agent HTTP request into a canonical intent bound to its session, seller, resource, amount, and purpose.', + sample: 'Pi · call 0x8c0d…b811 · 0.08 test USDC', + }, + { + number: '02', + eyebrow: 'Policy', + title: 'Decide before signing.', + body: 'Automatically allow, require exact human approval, or deny against immutable seller and budget rules.', + sample: 'Default deny · exact seller · bounded session', + }, + { + number: '03', + eyebrow: 'Planned receipt', + title: 'Keep payment separate from outcome.', + body: 'A release-gated flow can anchor settlement on-chain and preserve policy, approval, execution, refund, and reconciliation facts in a signed local receipt.', + sample: 'Planned: one permit · one paid retry · one terminal receipt', + }, +] as const; + +export const rolePaths = [ + { + label: 'For AI platform teams', + title: 'Economic agency with a hard boundary.', + body: 'Give Agents approved buying power without exposing wallet credentials or a generic signing surface.', + }, + { + label: 'For operators', + title: 'Exact approvals, not blanket access.', + body: 'Review the seller, resource, request hash, amount ceiling, policy version, and expiry before authorizing once.', + }, + { + label: 'For security + finance', + title: 'A durable answer for every cent.', + body: 'Release-gated signed receipts will let teams inspect conserved budgets, ambiguous settlements, refunds, and reconciliation from customer-held records.', + }, +] as const; + +export const evidenceReceipts = [ + { + value: '84532', + label: 'fixture network', + detail: 'Base Sepolia config only; no broadcast or real funds', + }, + { + value: 'x402', + label: 'release-gated payment rail', + detail: 'settlement would bind network, asset, seller, request, and amount', + }, + { + value: 'UNSIGNED', + label: 'browser artifact', + detail: 'local projection only; not broadcast and not live evidence', + }, +] as const; + +export const repositoryUrl = + 'https://github.com/Aznatkoiny/skill-asset-protocol'; + +export const spendControlDesignUrl = `${repositoryUrl}/blob/main/docs/superpowers/specs/2026-07-31-agent-spend-control-plane-design.md`; + +export const defaultPilotUrl = + 'https://github.com/Aznatkoiny/skill-asset-protocol/issues/new?title=Agent%20Spend%20Control%20design-partner%20pilot'; diff --git a/site/app/landing.module.css b/site/app/landing.module.css new file mode 100644 index 0000000..06e5493 --- /dev/null +++ b/site/app/landing.module.css @@ -0,0 +1,1996 @@ +.page { + --ink: #0a0a0a; + --paper: #f4f1e8; + --white: #fffef9; + --yellow: #ffd100; + --yellow-soft: #fff1a6; + --green: #173f35; + --green-bright: #c8ffd9; + --line: rgba(10, 10, 10, 0.2); + min-height: 100vh; + overflow: clip; + background: + linear-gradient(rgba(10, 10, 10, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(10, 10, 10, 0.035) 1px, transparent 1px), + var(--paper); + background-size: 32px 32px; + color: var(--ink); + font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif; +} + +.page ::selection { + background: var(--yellow); + color: var(--ink); +} + +.page :where(a, button):focus-visible { + outline: 3px solid var(--yellow); + outline-offset: 4px; +} + +.skipLink { + position: fixed; + top: 12px; + left: 12px; + z-index: 100; + transform: translateY(-150%); + border: 2px solid var(--ink); + background: var(--yellow); + padding: 12px 16px; + color: var(--ink); + font-weight: 800; +} + +.skipLink:focus { + transform: translateY(0); +} + +.topNav { + position: sticky; + top: 0; + z-index: 50; + border-bottom: 1px solid var(--ink); + background: color-mix(in srgb, var(--paper) 94%, transparent); + backdrop-filter: blur(14px); +} + +.navInner { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + width: min(100%, 1440px); + min-height: 68px; + margin: 0 auto; + padding: 0 32px; +} + +.brand { + display: inline-flex; + align-items: center; + justify-self: start; + gap: 12px; + color: inherit; + text-decoration: none; +} + +.brandMark { + display: grid; + width: 35px; + height: 35px; + place-items: center; + border: 2px solid var(--ink); + background: var(--yellow); + font-family: var(--font-geist-mono), monospace; + font-size: 12px; + font-weight: 900; + letter-spacing: -0.04em; +} + +.brandText { + display: flex; + flex-direction: column; + gap: 1px; + font-size: 12px; + font-weight: 850; + letter-spacing: 0.12em; + line-height: 1.15; + text-transform: uppercase; +} + +.brandText small { + font-size: 9px; + font-weight: 650; + letter-spacing: 0.18em; + opacity: 0.58; +} + +.navLinks { + display: flex; + align-items: center; + justify-content: center; + gap: 28px; + font-size: 12px; + font-weight: 720; +} + +.navLinks a, +.footer a { + color: inherit; + text-decoration-thickness: 1px; + text-underline-offset: 5px; +} + +.navLinks a { + text-decoration: none; +} + +.navLinks a:hover, +.footer a:hover { + text-decoration: underline; +} + +.navCta { + justify-self: end; + border: 1px solid var(--ink); + background: var(--ink); + padding: 11px 15px; + color: var(--white); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-decoration: none; + text-transform: uppercase; +} + +.navCta:hover { + background: var(--yellow); + color: var(--ink); +} + +.researchStrip { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + min-height: 36px; + border-bottom: 1px solid var(--ink); + background: var(--ink); + padding: 7px 24px; + color: var(--white); + font-family: var(--font-geist-mono), monospace; + font-size: 10px; + letter-spacing: 0.08em; + text-align: center; + text-transform: uppercase; +} + +.researchStrip span:first-child { + background: var(--yellow); + padding: 2px 6px; + color: var(--ink); + font-weight: 900; +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1.02fr) minmax(480px, 0.98fr); + gap: clamp(54px, 8vw, 120px); + align-items: center; + width: min(100%, 1440px); + min-height: calc(100vh - 104px); + margin: 0 auto; + padding: clamp(72px, 8vw, 128px) 48px; +} + +.heroCopy { + min-width: 0; +} + +.eyebrow, +.microLabel { + margin: 0; + font-family: var(--font-geist-mono), monospace; + font-weight: 760; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.eyebrow { + font-size: 11px; +} + +.microLabel { + font-size: 9px; + line-height: 1.5; + opacity: 0.62; +} + +.heroTitle { + max-width: 850px; + margin: 24px 0 0; + font-size: clamp(3.4rem, 7.4vw, 7.1rem); + font-weight: 830; + letter-spacing: -0.065em; + line-height: 0.88; +} + +.heroLead { + max-width: 680px; + margin: 34px 0 0; + font-size: clamp(1.08rem, 1.6vw, 1.38rem); + font-weight: 480; + letter-spacing: -0.02em; + line-height: 1.55; +} + +.heroActions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 36px; +} + +.primaryButton, +.secondaryButton, +.primaryButtonDark, +.actionButton { + display: inline-flex; + min-height: 48px; + align-items: center; + justify-content: space-between; + gap: 28px; + border: 2px solid var(--ink); + padding: 13px 16px; + font-size: 12px; + font-weight: 820; + letter-spacing: 0.045em; + line-height: 1.2; + text-decoration: none; +} + +.primaryButton { + background: var(--yellow); + color: var(--ink); +} + +.primaryButton:hover, +.actionButton:hover { + background: var(--ink); + color: var(--yellow); +} + +.secondaryButton { + background: transparent; + color: var(--ink); +} + +.secondaryButton:hover { + background: var(--white); +} + +.primaryButtonDark { + border-color: var(--white); + background: var(--white); + color: var(--ink); +} + +.primaryButtonDark:hover { + border-color: var(--yellow); + background: var(--yellow); +} + +.heroAssurances { + display: flex; + flex-wrap: wrap; + gap: 8px 22px; + margin: 24px 0 0; + padding: 0; + list-style: none; + font-family: var(--font-geist-mono), monospace; + font-size: 10px; + font-weight: 650; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.heroAssurances li::before { + content: '✓'; + margin-right: 7px; + font-weight: 900; +} + +.snapshotWrap { + position: relative; + min-width: 0; + padding: 10px; +} + +.cropMarkTopLeft, +.cropMarkTopRight, +.cropMarkBottomLeft, +.cropMarkBottomRight { + position: absolute; + z-index: 2; + width: 26px; + height: 26px; + pointer-events: none; +} + +.cropMarkTopLeft { + top: 0; + left: 0; + border-top: 2px solid var(--ink); + border-left: 2px solid var(--ink); +} + +.cropMarkTopRight { + top: 0; + right: 0; + border-top: 2px solid var(--ink); + border-right: 2px solid var(--ink); +} + +.cropMarkBottomLeft { + bottom: 0; + left: 0; + border-bottom: 2px solid var(--ink); + border-left: 2px solid var(--ink); +} + +.cropMarkBottomRight { + right: 0; + bottom: 0; + border-right: 2px solid var(--ink); + border-bottom: 2px solid var(--ink); +} + +.snapshot { + border: 2px solid var(--ink); + background: var(--white); + box-shadow: 16px 16px 0 var(--ink); +} + +.snapshotHeader, +.poolRow, +.snapshotFooter { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; +} + +.snapshotHeader { + border-bottom: 1px solid var(--ink); + padding: 18px 20px; +} + +.snapshotOrg { + margin: 4px 0 0; + font-size: 17px; + font-weight: 780; +} + +.draftBadge, +.successBadge, +.included, +.pending { + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid currentColor; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 800; + letter-spacing: 0.08em; + line-height: 1; + text-transform: uppercase; +} + +.draftBadge { + background: var(--yellow-soft); + padding: 7px 8px; +} + +.poolRow { + padding: 24px 20px; +} + +.poolValue { + margin: 5px 0 0; + font-family: var(--font-geist-mono), monospace; + font-size: clamp(2.4rem, 5vw, 4rem); + font-weight: 820; + letter-spacing: -0.07em; + line-height: 1; +} + +.poolMeta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 5px; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 680; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.snapshotStats { + display: grid; + grid-template-columns: repeat(3, 1fr); + margin: 0; + border-top: 1px solid var(--ink); + border-bottom: 1px solid var(--ink); +} + +.snapshotStats div { + min-width: 0; + padding: 15px; + border-right: 1px solid var(--ink); +} + +.snapshotStats div:last-child { + border-right: 0; +} + +.snapshotStats dt { + min-height: 28px; + font-size: 9px; + font-weight: 720; + letter-spacing: 0.07em; + line-height: 1.4; + text-transform: uppercase; +} + +.snapshotStats dd { + margin: 8px 0 0; + font-family: var(--font-geist-mono), monospace; + font-size: 25px; + font-weight: 820; +} + +.ledgerPreview { + background: var(--ink); + color: var(--green-bright); + font-family: var(--font-geist-mono), monospace; +} + +.ledgerHead, +.ledgerLine { + display: grid; + align-items: center; +} + +.ledgerHead { + grid-template-columns: 1fr auto; + padding: 10px 14px; + border-bottom: 1px dashed rgba(200, 255, 217, 0.28); + font-size: 9px; + font-weight: 760; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.ledgerLine { + grid-template-columns: 40px minmax(0, 1fr) auto; + gap: 10px; + padding: 12px 14px; + border-bottom: 1px dashed rgba(200, 255, 217, 0.22); +} + +.ledgerLine > span:nth-child(2) { + display: flex; + min-width: 0; + flex-direction: column; + gap: 4px; +} + +.ledgerLine strong { + overflow: hidden; + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ledgerLine small { + overflow: hidden; + color: rgba(200, 255, 217, 0.62); + font-size: 9px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ledgerIndex { + color: rgba(200, 255, 217, 0.45); + font-size: 9px; +} + +.included, +.pending { + border-color: rgba(200, 255, 217, 0.34); + padding: 5px 6px; +} + +.pending { + color: #ffe075; +} + +.snapshotFooter { + padding: 13px 14px; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.snapshotFooter a { + color: inherit; + text-underline-offset: 3px; +} + +.hazardBand { + height: 22px; + border-top: 1px solid var(--ink); + border-bottom: 1px solid var(--ink); + background: repeating-linear-gradient( + -45deg, + var(--ink) 0, + var(--ink) 14px, + var(--yellow) 14px, + var(--yellow) 28px + ); +} + +.section, +.rolesSection, +.evidenceSection, +.sandboxSection { + width: min(100%, 1440px); + margin: 0 auto; + padding: clamp(80px, 10vw, 148px) 48px; +} + +.sectionHeading { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.65fr); + column-gap: clamp(40px, 8vw, 120px); + align-items: end; + margin-bottom: 48px; +} + +.sectionHeading .eyebrow { + grid-column: 1 / -1; + margin-bottom: 16px; +} + +.sectionHeading h2, +.sandboxIntro h2, +.rolesHeading h2, +.evidenceHeader h2, +.pilotCta h2 { + margin: 0; + font-size: clamp(2.5rem, 5.8vw, 5.6rem); + font-weight: 820; + letter-spacing: -0.06em; + line-height: 0.95; +} + +.sectionHeading > p:last-child, +.rolesHeading > p:last-child, +.evidenceHeader > p:last-child { + margin: 0; + font-size: 16px; + line-height: 1.65; +} + +.proofGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin: 0; + padding: 0; + border: 2px solid var(--ink); + background: var(--white); + list-style: none; +} + +.proofCard { + position: relative; + min-width: 0; + padding: clamp(28px, 4vw, 52px); + border-right: 1px solid var(--ink); +} + +.proofCard:last-child { + border-right: 0; +} + +.proofCardTop { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; +} + +.outlineNumber { + color: transparent; + font-size: clamp(4rem, 8vw, 7rem); + font-weight: 900; + letter-spacing: -0.08em; + line-height: 0.8; + -webkit-text-stroke: 1.5px var(--ink); +} + +.proofCard h3 { + margin: 48px 0 0; + font-size: clamp(1.6rem, 3vw, 2.6rem); + font-weight: 800; + letter-spacing: -0.045em; + line-height: 1; +} + +.proofCard > p { + min-height: 92px; + margin: 18px 0 0; + font-size: 15px; + line-height: 1.55; +} + +.proofSample { + display: flex; + align-items: center; + gap: 9px; + min-height: 45px; + margin-top: 28px; + border-top: 1px solid var(--ink); + padding-top: 14px; + font-family: var(--font-geist-mono), monospace; + font-size: 10px; + font-weight: 680; + line-height: 1.4; +} + +.statusDot, +.statusPulse { + display: inline-block; + width: 9px; + height: 9px; + flex: 0 0 auto; + border: 1px solid var(--ink); + border-radius: 50%; + background: var(--yellow); +} + +.proofArrow { + position: absolute; + top: 50%; + right: -17px; + z-index: 2; + display: grid; + width: 34px; + height: 34px; + place-items: center; + border: 1px solid var(--ink); + border-radius: 50%; + background: var(--yellow); + font-weight: 900; +} + +.rolesSection { + border-top: 1px solid var(--ink); +} + +.rolesHeading { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(300px, 0.6fr); + gap: 48px; + align-items: end; +} + +.rolesHeading > div .eyebrow, +.evidenceHeader > div .eyebrow { + margin-bottom: 16px; +} + +.roleGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + margin-top: 54px; +} + +.roleCard { + min-width: 0; + border: 1px solid var(--ink); + background: color-mix(in srgb, var(--white) 75%, transparent); + padding: 28px; +} + +.roleCard:nth-child(2) { + background: var(--yellow-soft); +} + +.roleCard h3 { + margin: 44px 0 0; + font-size: 23px; + font-weight: 790; + letter-spacing: -0.04em; + line-height: 1.08; +} + +.roleCard p:last-child { + margin: 16px 0 0; + font-size: 14px; + line-height: 1.6; +} + +.sandboxSection { + scroll-margin-top: 90px; + border-top: 1px solid var(--ink); + border-bottom: 1px solid var(--ink); + background: var(--white); +} + +.sandboxIntro { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 48px; + align-items: end; +} + +.sandboxIntro h2 { + max-width: 920px; + margin-top: 16px; +} + +.sandboxNotice { + display: flex; + max-width: 310px; + flex-direction: column; + gap: 7px; + border: 2px solid var(--ink); + background: var(--yellow); + padding: 15px 16px; + font-family: var(--font-geist-mono), monospace; + font-size: 10px; + line-height: 1.5; + text-transform: uppercase; +} + +.sandboxNotice strong { + font-size: 11px; +} + +.stepper { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + margin: 58px 0 0; + padding: 0; + border: 2px solid var(--ink); + list-style: none; +} + +.step { + display: flex; + min-width: 0; + min-height: 76px; + align-items: center; + gap: 13px; + border-right: 1px solid var(--ink); + background: var(--paper); + padding: 13px 15px; +} + +.step:last-child { + border-right: 0; +} + +.stepCurrent { + background: var(--yellow); +} + +.stepComplete { + background: var(--ink); + color: var(--white); +} + +.stepNumber { + display: grid; + width: 35px; + height: 35px; + flex: 0 0 auto; + place-items: center; + border: 1px solid currentColor; + border-radius: 50%; + font-family: var(--font-geist-mono), monospace; + font-size: 10px; + font-weight: 800; +} + +.step > span:last-child { + display: flex; + min-width: 0; + flex-direction: column; + gap: 4px; +} + +.step strong { + font-size: 12px; +} + +.step small { + font-size: 10px; + opacity: 0.67; +} + +.sandboxStatus { + display: flex; + min-height: 58px; + align-items: center; + gap: 12px; + border-right: 2px solid var(--ink); + border-bottom: 2px solid var(--ink); + border-left: 2px solid var(--ink); + background: var(--yellow-soft); + padding: 10px 14px; +} + +.sandboxStatus h3 { + flex: 1; + margin: 0; + font-size: 12px; + font-weight: 720; + line-height: 1.45; +} + +.sandboxStatus h3:focus { + outline: none; +} + +.resetButton { + min-height: 38px; + border: 1px solid var(--ink); + background: transparent; + padding: 8px 11px; + color: inherit; + font-family: inherit; + font-size: 10px; + font-weight: 750; + cursor: pointer; +} + +.resetButton:hover { + background: var(--white); +} + +.sandboxWorkspace { + display: grid; + grid-template-columns: minmax(340px, 0.82fr) minmax(0, 1.18fr); + gap: 18px; + margin-top: 18px; +} + +.registryCard, +.activityCard, +.policyCard { + min-width: 0; + border: 2px solid var(--ink); + background: var(--paper); +} + +.registryCardActive { + box-shadow: inset 7px 0 0 var(--yellow); +} + +.cardBar { + display: flex; + min-height: 38px; + align-items: center; + justify-content: space-between; + gap: 16px; + border-bottom: 1px solid var(--ink); + background: var(--ink); + padding: 9px 12px; + color: var(--white); + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 700; + letter-spacing: 0.07em; + line-height: 1.35; + text-transform: uppercase; +} + +.registryBody { + padding: 24px; +} + +.skillIdentity { + display: grid; + grid-template-columns: 64px minmax(0, 1fr); + gap: 18px; + align-items: start; +} + +.skillMonogram { + display: grid; + width: 64px; + height: 64px; + place-items: center; + border: 2px solid var(--ink); + background: var(--yellow); + font-family: var(--font-geist-mono), monospace; + font-size: 20px; + font-weight: 900; +} + +.skillIdentity h3 { + margin: 6px 0 0; + font-size: 23px; + font-weight: 810; + letter-spacing: -0.04em; + line-height: 1; +} + +.skillIdentity p:last-child { + margin: 11px 0 0; + font-size: 13px; + line-height: 1.5; +} + +.skillFacts { + display: grid; + grid-template-columns: 1fr 1fr; + margin: 28px 0 0; + border-top: 1px solid var(--ink); + border-left: 1px solid var(--ink); +} + +.skillFacts > div { + min-width: 0; + border-right: 1px solid var(--ink); + border-bottom: 1px solid var(--ink); + padding: 13px; +} + +.skillFacts dt { + margin-bottom: 9px; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 750; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.skillFacts dd { + display: flex; + min-width: 0; + align-items: center; + gap: 9px; + margin: 0; + font-size: 12px; + font-weight: 680; + line-height: 1.35; +} + +.skillFacts dd > span:last-child, +.skillFacts dd:not(:has(.avatar)) { + min-width: 0; +} + +.skillFacts dd span, +.skillFacts dd { + overflow-wrap: anywhere; +} + +.skillFacts dd small { + display: block; + margin-top: 3px; + font-size: 9px; + font-weight: 520; + opacity: 0.62; +} + +.avatar { + display: grid; + width: 34px; + height: 34px; + flex: 0 0 auto; + place-items: center; + border: 1px solid var(--ink); + border-radius: 50%; + background: var(--white); + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 780; +} + +.breakable { + overflow-wrap: anywhere; +} + +.integrityNote { + display: flex; + align-items: flex-start; + gap: 10px; + margin-top: 18px; + font-size: 10px; + line-height: 1.5; +} + +.integrityNote > span:first-child { + font-size: 16px; + line-height: 1; +} + +.activityCard { + display: flex; + min-height: 100%; + flex-direction: column; + background: var(--white); +} + +.emptyLedger { + display: flex; + min-height: 318px; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + text-align: center; +} + +.emptyLedgerMark { + display: grid; + width: 58px; + height: 58px; + place-items: center; + border: 1px dashed var(--ink); + border-radius: 50%; + font-size: 26px; +} + +.emptyLedger h3 { + margin: 20px 0 0; + font-size: 20px; +} + +.emptyLedger p { + max-width: 430px; + margin: 10px 0 0; + font-size: 13px; + line-height: 1.55; +} + +.invocationList { + flex: 1; + margin: 0; + padding: 0; + list-style: none; +} + +.invocation { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 13px; + align-items: start; + padding: 18px; + border-bottom: 1px solid var(--ink); +} + +.invocationMain { + min-width: 0; +} + +.invocationMain > div:first-child { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + gap: 6px 14px; +} + +.invocationMain strong { + font-size: 12px; +} + +.invocationMain > div:first-child span { + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + opacity: 0.58; +} + +.invocationMain p { + margin: 7px 0 0; + font-size: 12px; + line-height: 1.45; +} + +.invocationMain small { + display: block; + margin-top: 7px; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + line-height: 1.45; + opacity: 0.58; +} + +.successBadge { + border-color: #12633f; + padding: 6px; + color: #12633f; +} + +.outcomeTag { + display: flex; + flex-wrap: wrap; + gap: 5px 14px; + margin-top: 10px; + border: 1px solid var(--ink); + background: var(--yellow-soft); + padding: 8px 9px; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 650; + line-height: 1.4; +} + +.outcomeTag span:last-child { + overflow-wrap: anywhere; +} + +.approvalPanel { + border-top: 2px solid var(--ink); + background: var(--paper); + padding: 18px; +} + +.approvalPanel h3 { + margin: 6px 0 0; + font-size: 16px; + line-height: 1.2; +} + +.approvalGrid { + display: grid; + grid-template-columns: 1fr 1fr; + margin: 16px 0 0; + border-top: 1px solid var(--ink); + border-left: 1px solid var(--ink); +} + +.approvalGrid > div { + min-width: 0; + border-right: 1px solid var(--ink); + border-bottom: 1px solid var(--ink); + padding: 11px; +} + +.approvalGrid dt { + margin-bottom: 7px; + font-family: var(--font-geist-mono), monospace; + font-size: 8px; + font-weight: 750; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.approvalGrid dd { + margin: 0; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 620; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.approvalWarning { + margin: 12px 0 0; + font-size: 10px; + font-weight: 680; + line-height: 1.5; +} + +.nextAction { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + margin-top: auto; + background: var(--yellow-soft); + padding: 16px; +} + +.nextAction > div { + min-width: 0; +} + +.nextAction > div > p:last-child { + max-width: 470px; + margin: 5px 0 0; + font-size: 11px; + line-height: 1.4; +} + +.actionButton { + flex: 0 0 auto; + background: var(--yellow); + color: var(--ink); + font-family: inherit; + cursor: pointer; +} + +.closeGrid { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(340px, 0.9fr); + gap: 18px; + margin-top: 18px; +} + +.policyCard { + display: grid; + grid-template-columns: minmax(240px, 0.74fr) minmax(0, 1.26fr); + gap: 36px; + padding: 30px; +} + +.policyCard h3 { + margin: 11px 0 0; + font-size: clamp(1.8rem, 3vw, 2.8rem); + letter-spacing: -0.05em; + line-height: 1; +} + +.policyCard > div:first-child > p:last-child { + margin: 18px 0 0; + font-size: 13px; + line-height: 1.55; +} + +.policyFactors { + margin: 0; + border-top: 1px solid var(--ink); +} + +.policyFactors > div { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 16px; + align-items: center; + border-bottom: 1px solid var(--ink); + padding: 12px 0; +} + +.policyFactors dt { + font-size: 11px; + line-height: 1.4; +} + +.policyFactors dd { + display: grid; + grid-template-columns: 64px 32px; + gap: 8px; + align-items: center; + margin: 0; + font-family: var(--font-geist-mono), monospace; + font-size: 10px; + text-align: right; +} + +.policyFactors dd span { + opacity: 0.55; +} + +.policyFactors dd strong { + font-size: 12px; +} + +.policyFactors .policyTotal { + border-bottom-width: 2px; + background: var(--yellow); + padding: 13px 8px; + font-weight: 800; +} + +.policyFootnote { + grid-column: 1 / -1; + margin: 0; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.rewardReceipt { + min-width: 0; + background: var(--ink); + padding: 22px 24px 24px; + color: var(--green-bright); + font-family: var(--font-geist-mono), monospace; + text-shadow: 0 0 1.5px rgba(186, 255, 186, 0.28); +} + +.receiptTear { + overflow: hidden; + margin-bottom: 18px; + color: rgba(200, 255, 217, 0.48); + font-size: 9px; + white-space: nowrap; +} + +.receiptBrand { + margin: 0; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.2em; + text-align: center; + text-transform: uppercase; +} + +.rewardReceipt h3 { + margin: 11px 0 0; + font-size: 18px; + letter-spacing: 0.08em; + text-align: center; + text-transform: uppercase; +} + +.receiptDim { + margin: 8px 0 0; + color: rgba(200, 255, 217, 0.54); + font-size: 9px; + line-height: 1.45; + text-align: center; + text-transform: uppercase; +} + +.receiptRule { + margin: 17px 0; + border-top: 1px dashed rgba(200, 255, 217, 0.44); +} + +.receiptRows { + margin: 0; +} + +.receiptRows > div { + display: grid; + grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr); + gap: 18px; + margin-top: 7px; + font-size: 10px; + line-height: 1.4; +} + +.receiptRows dt { + color: rgba(200, 255, 217, 0.56); +} + +.receiptRows dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + text-align: right; +} + +.awardTotal { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 20px; + text-transform: uppercase; +} + +.awardTotal span { + font-size: 10px; + font-weight: 700; +} + +.awardTotal strong { + font-size: clamp(2rem, 4vw, 3.6rem); + letter-spacing: -0.08em; +} + +.receiptBarcode { + height: 38px; + margin-top: 22px; + background: repeating-linear-gradient( + 90deg, + var(--green-bright) 0 2px, + transparent 2px 5px, + var(--green-bright) 5px 6px, + transparent 6px 8px, + var(--green-bright) 8px 11px, + transparent 11px 15px, + var(--green-bright) 15px 16px, + transparent 16px 18px, + var(--green-bright) 18px 20px, + transparent 20px 25px + ); + opacity: 0.76; +} + +.sandboxComplete { + display: flex; + align-items: center; + justify-content: space-between; + gap: 36px; + margin-top: 18px; + border: 2px solid var(--ink); + background: var(--yellow); + padding: 22px; +} + +.sandboxComplete > div { + max-width: 760px; +} + +.sandboxComplete > div > p:last-child { + margin: 7px 0 0; + font-size: 14px; + line-height: 1.5; +} + +.sandboxComplete .primaryButton { + flex: 0 0 auto; + background: var(--ink); + color: var(--yellow); +} + +.sandboxComplete .primaryButton:hover { + background: var(--white); + color: var(--ink); +} + +.evidenceSection { + background: var(--ink); + color: var(--green-bright); +} + +.evidenceHeader { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(300px, 0.6fr); + gap: 48px; + align-items: end; +} + +.evidenceHeader .eyebrow { + color: var(--yellow); +} + +.evidenceGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 58px; + border-top: 1px dashed rgba(200, 255, 217, 0.45); + border-bottom: 1px dashed rgba(200, 255, 217, 0.45); +} + +.evidenceReceipt { + min-width: 0; + border-right: 1px dashed rgba(200, 255, 217, 0.35); + padding: 30px; + font-family: var(--font-geist-mono), monospace; +} + +.evidenceReceipt:last-child { + border-right: 0; +} + +.evidenceReceipt strong { + display: block; + font-size: clamp(3rem, 7vw, 6.3rem); + letter-spacing: -0.08em; + line-height: 0.9; +} + +.evidenceReceipt span { + display: block; + margin-top: 13px; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.evidenceReceipt p { + margin: 18px 0 0; + color: rgba(200, 255, 217, 0.58); + font-size: 10px; + line-height: 1.6; +} + +.evidenceActions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-top: 34px; +} + +.evidenceLink { + display: inline-flex; + min-height: 46px; + align-items: center; + gap: 20px; + border: 1px solid currentColor; + padding: 12px 14px; + color: inherit; + font-family: var(--font-geist-mono), monospace; + font-size: 10px; + font-weight: 750; + letter-spacing: 0.06em; + text-decoration: none; + text-transform: uppercase; +} + +.evidenceLink:hover { + background: var(--green-bright); + color: var(--ink); +} + +.evidenceCaveat { + margin-left: auto; + color: rgba(200, 255, 217, 0.58); + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + line-height: 1.5; + text-transform: uppercase; +} + +.pilotCta { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(350px, 0.85fr); + gap: clamp(50px, 9vw, 140px); + width: min(100%, 1440px); + margin: 0 auto; + padding: clamp(80px, 10vw, 150px) 48px; + background: var(--yellow); +} + +.pilotCta h2 { + margin-top: 16px; +} + +.pilotDetails { + align-self: end; +} + +.pilotDetails ul { + margin: 0 0 28px; + padding: 0; + border-top: 1px solid var(--ink); + list-style: none; +} + +.pilotDetails li { + border-bottom: 1px solid var(--ink); + padding: 11px 0; + font-size: 13px; + font-weight: 650; +} + +.pilotDetails li::before { + content: '✓'; + margin-right: 10px; +} + +.footer { + display: grid; + grid-template-columns: 1.2fr 0.8fr 0.8fr; + gap: 36px; + border-top: 1px solid var(--white); + background: var(--ink); + padding: 52px max(32px, calc((100vw - 1344px) / 2)); + color: var(--white); +} + +.footerBrand { + max-width: 430px; +} + +.footerBrand strong { + display: block; + font-size: 15px; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.footerBrand p { + margin: 12px 0 0; + color: rgba(255, 254, 249, 0.62); + font-size: 12px; + line-height: 1.55; +} + +.footerColumn { + display: flex; + flex-direction: column; + gap: 10px; + font-size: 12px; +} + +.footerColumn strong { + margin-bottom: 5px; + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +@media (max-width: 1100px) { + .hero { + grid-template-columns: 1fr; + min-height: auto; + } + + .heroCopy { + max-width: 900px; + } + + .snapshotWrap { + width: min(100%, 720px); + } + + .sandboxWorkspace, + .closeGrid { + grid-template-columns: 1fr; + } + + .registryBody { + display: grid; + grid-template-columns: minmax(260px, 0.7fr) minmax(0, 1.3fr); + gap: 24px; + } + + .skillFacts { + margin-top: 0; + } + + .integrityNote { + grid-column: 1 / -1; + } +} + +@media (max-width: 820px) { + .navInner { + grid-template-columns: 1fr auto; + padding: 0 20px; + } + + .navLinks { + display: none; + } + + .hero, + .section, + .rolesSection, + .evidenceSection, + .sandboxSection, + .pilotCta { + padding-right: 24px; + padding-left: 24px; + } + + .hero { + grid-template-columns: minmax(0, 1fr); + padding-top: 72px; + padding-bottom: 88px; + } + + .heroTitle { + font-size: clamp(3.2rem, 13vw, 6rem); + } + + .sectionHeading, + .rolesHeading, + .sandboxIntro, + .evidenceHeader, + .pilotCta { + grid-template-columns: 1fr; + align-items: start; + } + + .proofGrid, + .roleGrid, + .evidenceGrid { + grid-template-columns: 1fr; + } + + .proofCard, + .evidenceReceipt { + border-right: 0; + border-bottom: 1px solid var(--ink); + } + + .proofCard:last-child, + .evidenceReceipt:last-child { + border-bottom: 0; + } + + .proofArrow { + top: auto; + right: 50%; + bottom: -17px; + transform: translateX(50%) rotate(90deg); + } + + .proofCard > p { + min-height: auto; + } + + .stepper { + grid-template-columns: 1fr 1fr; + } + + .step:nth-child(even) { + border-right: 0; + } + + .step:nth-child(-n + 4) { + border-bottom: 1px solid var(--ink); + } + + .policyCard { + grid-template-columns: 1fr; + } + + .policyFootnote { + grid-column: auto; + } + + .sandboxComplete { + align-items: stretch; + flex-direction: column; + } + + .sandboxComplete .primaryButton { + align-self: flex-start; + } + + .evidenceReceipt { + border-bottom-style: dashed; + border-bottom-color: rgba(200, 255, 217, 0.35); + } + + .evidenceCaveat { + width: 100%; + margin: 10px 0 0; + } + + .footer { + grid-template-columns: 1fr 1fr; + padding: 46px 24px; + } + + .footerBrand { + grid-column: 1 / -1; + } +} + +@media (max-width: 600px) { + .navInner { + min-height: 60px; + } + + .brandText small { + display: none; + } + + .navCta { + padding: 9px 11px; + font-size: 9px; + } + + .researchStrip { + justify-content: flex-start; + overflow: hidden; + text-align: left; + } + + .researchStrip span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .hero, + .section, + .rolesSection, + .evidenceSection, + .sandboxSection, + .pilotCta { + padding-right: 16px; + padding-left: 16px; + } + + .hero { + gap: 58px; + padding-top: 54px; + padding-bottom: 72px; + } + + .heroTitle { + font-size: clamp(3rem, 16vw, 4.7rem); + } + + .heroLead { + font-size: 16px; + } + + .heroActions, + .heroActions a { + width: 100%; + } + + .snapshotWrap { + padding: 7px; + } + + .snapshot { + box-shadow: 8px 8px 0 var(--ink); + } + + .poolRow, + .snapshotHeader, + .snapshotFooter { + align-items: flex-start; + } + + .poolRow { + flex-direction: column; + } + + .poolMeta { + align-items: flex-start; + } + + .snapshotStats { + grid-template-columns: 1fr; + } + + .snapshotStats div { + display: flex; + align-items: center; + justify-content: space-between; + border-right: 0; + border-bottom: 1px solid var(--ink); + } + + .snapshotStats div:last-child { + border-bottom: 0; + } + + .snapshotStats dt { + min-height: 0; + } + + .snapshotStats dd { + margin: 0; + } + + .ledgerLine { + grid-template-columns: 30px minmax(0, 1fr); + } + + .ledgerLine > span:last-child { + grid-column: 2; + justify-self: start; + } + + .snapshotFooter { + flex-direction: column; + } + + .sectionHeading, + .rolesHeading, + .sandboxIntro, + .evidenceHeader { + gap: 24px; + } + + .sectionHeading h2, + .sandboxIntro h2, + .rolesHeading h2, + .evidenceHeader h2, + .pilotCta h2 { + font-size: clamp(2.4rem, 13vw, 4rem); + } + + .stepper { + grid-template-columns: 1fr; + } + + .step, + .step:nth-child(2) { + border-right: 0; + border-bottom: 1px solid var(--ink); + } + + .step:last-child { + border-bottom: 0; + } + + .sandboxStatus { + align-items: flex-start; + flex-wrap: wrap; + } + + .sandboxStatus h3 { + flex-basis: calc(100% - 24px); + } + + .resetButton { + margin-left: 21px; + } + + .registryBody { + display: block; + padding: 18px; + } + + .skillIdentity { + grid-template-columns: 50px minmax(0, 1fr); + gap: 13px; + } + + .skillMonogram { + width: 50px; + height: 50px; + } + + .skillFacts { + grid-template-columns: 1fr; + margin-top: 24px; + } + + .invocation { + grid-template-columns: auto minmax(0, 1fr); + } + + .invocation > .successBadge { + grid-column: 2; + justify-self: start; + } + + .approvalGrid { + grid-template-columns: 1fr; + } + + .nextAction { + align-items: stretch; + flex-direction: column; + } + + .actionButton { + width: 100%; + } + + .policyCard { + padding: 22px 18px; + } + + .rewardReceipt { + padding: 20px 17px; + } + + .receiptRows > div { + gap: 8px; + } + + .footer { + grid-template-columns: 1fr; + padding: 42px 16px; + } + + .footerBrand { + grid-column: auto; + } +} + +@media (prefers-reduced-motion: reduce) { + .page { + scroll-behavior: auto; + } +} diff --git a/site/app/layout.tsx b/site/app/layout.tsx index 4c87c95..0a570f7 100644 --- a/site/app/layout.tsx +++ b/site/app/layout.tsx @@ -14,15 +14,19 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { metadataBase: new URL("https://neverhandedover.com"), - title: "MANIFESTO", + title: { + default: "Skill Asset Protocol — Spending controls for AI Agents", + template: "%s · Skill Asset Protocol", + }, description: - "A manifesto for the sovereignty of authored work. Pay $0.25 in testnet USDC and the hosted skill runs for you — you get the output, never the skill.", + "Pre-release offline preview of a customer-hosted Wallet Kernel for policy-bound Agent spending, exact human approvals, and planned signed receipts.", openGraph: { - title: "“THE PROOF” — a manifesto for the sovereignty of authored work", + title: "Give AI Agents a wallet without giving them the keys", description: - "Ten principles. Real on-chain receipts. A live x402 endpoint: the artifact is never handed over.", + "Pre-release offline preview of a wallet-native Agent Spend Control Plane with budgets, seller policy, approvals, and planned signed receipts.", url: "https://neverhandedover.com", siteName: "SKILL ASSET PROTOCOL", + type: "website", }, alternates: { canonical: "https://neverhandedover.com" }, }; diff --git a/site/app/lib/x402-client.ts b/site/app/lib/x402-client.ts deleted file mode 100644 index fe0f711..0000000 --- a/site/app/lib/x402-client.ts +++ /dev/null @@ -1,313 +0,0 @@ -// x402-client.ts — the BUYER half of the x402 v1 protocol, in the browser. -// -// Mirrors the handshake of the working seller reference at -// spikes/pi-wielder/src/x402-seller.mjs: -// -// 1. fetch(url) → seller responds 402 with { x402Version: 1, accepts: [PaymentRequirements] } -// 2. Buyer takes accepts[0] (x402 v1 "exact" scheme on base-sepolia), builds an -// EIP-3009 TransferWithAuthorization EIP-712 payload against Base Sepolia USDC, -// and signs it with the connected wallet via eth_signTypedData_v4. -// Signing is an off-chain authorization — the buyer never broadcasts a tx and -// pays no gas; the facilitator settles transferWithAuthorization on-chain. -// 3. Buyer retries ONCE with X-PAYMENT: base64(JSON payment payload). -// 4. On success the seller sets X-PAYMENT-RESPONSE: base64({ success, transaction, ... }) -// — the settled txHash is the receipt. -// -// No wallet libraries, no deps — raw window.ethereum (EIP-1193) plus hand-rolled -// hex / base64 helpers. - -// --- x402 v1 / Base Sepolia constants (mirror x402-seller.mjs) --------------- -export const X402_VERSION = 1; -export const NETWORK = 'base-sepolia'; -export const CHAIN_ID = 84532; -export const CHAIN_ID_HEX = '0x14a34'; -// Circle's canonical USDC deployment on Base Sepolia (6 decimals). -export const USDC_ADDRESS = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; -// EIP-712 domain values USDC uses for EIP-3009 signatures. -export const USDC_EIP712 = { name: 'USDC', version: '2' }; -export const USDC_DECIMALS = 6; - -// --- wallet provider discovery (EIP-6963 + window.ethereum fallback) ---------- -export interface Eip1193Provider { - request(args: { method: string; params?: unknown[] }): Promise; -} - -declare global { - interface Window { - ethereum?: Eip1193Provider; - } -} - -// Multiple extensions race to own window.ethereum (Phantom, Coinbase, ...) and -// the winner may not implement the EVM methods this page needs. EIP-6963 lets -// every installed wallet announce itself instead; prefer MetaMask, then any -// announced wallet, then legacy window.ethereum. -interface AnnouncedProvider { - info?: { rdns?: string; name?: string }; - provider: Eip1193Provider; -} - -const announced: AnnouncedProvider[] = []; -if (typeof window !== 'undefined') { - window.addEventListener('eip6963:announceProvider', (event) => { - const detail = (event as CustomEvent).detail; - if (detail?.provider && !announced.some((p) => p.info?.rdns === detail.info?.rdns)) { - announced.push(detail); - } - }); - window.dispatchEvent(new Event('eip6963:requestProvider')); -} - -export const hasWallet = (): boolean => - typeof window !== 'undefined' && (announced.length > 0 || !!window.ethereum); - -// PaymentRequirements as emitted by the seller's 402 body (accepts[0]). -export interface PaymentRequirements { - scheme: string; // 'exact' - network: string; // 'base-sepolia' - maxAmountRequired: string; // atomic USDC (6 decimals), e.g. '250000' - resource: string; - description: string; - mimeType: string; - payTo: string; - maxTimeoutSeconds: number; - asset: string; // USDC contract address - extra?: { name?: string; version?: string }; -} - -export interface PaidReceipt { - amountUSDC: number; - txHash: string; -} - -// --- hand-rolled helpers (no deps) -------------------------------------------- -const bytesToHex = (bytes: Uint8Array): string => - '0x' + Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); - -const randomBytes32Hex = (): string => { - const bytes = new Uint8Array(32); - crypto.getRandomValues(bytes); - return bytesToHex(bytes); -}; - -// UTF-8-safe base64 (btoa alone chokes on multi-byte chars). -const jsonToB64 = (obj: unknown): string => { - const utf8 = new TextEncoder().encode(JSON.stringify(obj)); - let binary = ''; - for (let i = 0; i < utf8.length; i += 0x8000) { - binary += String.fromCharCode(...utf8.subarray(i, i + 0x8000)); - } - return btoa(binary); -}; - -const b64ToJson = (s: string): T => { - const binary = atob(s); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); - return JSON.parse(new TextDecoder().decode(bytes)) as T; -}; - -const getProvider = (): Eip1193Provider => { - const metamask = announced.find((p) => p.info?.rdns === 'io.metamask'); - const provider = - metamask?.provider ?? - announced[0]?.provider ?? - (typeof window !== 'undefined' ? window.ethereum : undefined); - if (!provider) { - throw new Error('No EIP-1193 wallet found — install MetaMask (or any injected wallet) to pay.'); - } - return provider; -}; - -// --- ensureBaseSepolia --------------------------------------------------------- -// Get the wallet onto Base Sepolia (0x14a34) without assuming any particular -// wallet: skip if already there, try wallet_switchEthereumChain, fall back to -// wallet_addEthereumChain (some wallets reject the switch method outright, not -// just with 4902), and if it still won't move, say exactly what to do by hand. -export async function ensureBaseSepolia(provider: Eip1193Provider): Promise { - const chainId = async (): Promise => { - try { - const id = await provider.request({ method: 'eth_chainId' }); - return typeof id === 'string' ? id.toLowerCase() : null; - } catch { - return null; - } - }; - - if ((await chainId()) === CHAIN_ID_HEX) return; - - let switchErr: unknown = null; - try { - await provider.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: CHAIN_ID_HEX }], - }); - } catch (err) { - switchErr = err; - } - if ((await chainId()) === CHAIN_ID_HEX) return; - - // 4001 = the user rejected the switch in the wallet UI; adding the chain - // would just pop a second dialog at someone who said no. - if ((switchErr as { code?: number })?.code === 4001) { - throw new Error('Network switch rejected in the wallet — approve it and click INVOKE again.'); - } - - try { - await provider.request({ - method: 'wallet_addEthereumChain', - params: [ - { - chainId: CHAIN_ID_HEX, - chainName: 'Base Sepolia', - nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, - rpcUrls: ['https://sepolia.base.org'], - blockExplorerUrls: ['https://sepolia.basescan.org'], - }, - ], - }); - } catch { - // Fall through to the final check and manual instruction. - } - if ((await chainId()) === CHAIN_ID_HEX) return; - - throw new Error( - 'Could not switch networks automatically. In your wallet, add or select "Base Sepolia" (chain 84532, RPC https://sepolia.base.org), then click INVOKE again.', - ); -} - -// --- connectWallet --------------------------------------------------------------- -// Connect FIRST, then do chain operations: several wallets refuse chain -// requests coming from a not-yet-connected site. -export interface WalletSession { - provider: Eip1193Provider; - account: string; -} - -export async function connectWallet(): Promise { - const provider = getProvider(); - const accounts = (await provider.request({ method: 'eth_requestAccounts' })) as string[]; - const account = accounts?.[0]; - if (!account) throw new Error('Wallet returned no accounts.'); - await ensureBaseSepolia(provider); - return { provider, account }; -} - -// --- EIP-3009 TransferWithAuthorization signing -------------------------------- -async function signPayment( - requirements: PaymentRequirements, - session?: WalletSession, -): Promise<{ header: string; from: string }> { - const { provider, account: from } = session ?? (await connectWallet()); - - const now = Math.floor(Date.now() / 1000); - const authorization = { - from, - to: requirements.payTo, - value: requirements.maxAmountRequired, // atomic USDC, uint256 as decimal string - validAfter: '0', - validBefore: String(now + (requirements.maxTimeoutSeconds ?? 60) + 60), - // Random 32-byte nonce — EIP-3009 nonces are single-use ON-CHAIN, which is - // the protocol's replay protection: a replayed authorization fails /settle. - nonce: randomBytes32Hex(), - }; - - // The seller publishes the EIP-712 domain values in requirements.extra. - const typedData = { - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - TransferWithAuthorization: [ - { name: 'from', type: 'address' }, - { name: 'to', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'validAfter', type: 'uint256' }, - { name: 'validBefore', type: 'uint256' }, - { name: 'nonce', type: 'bytes32' }, - ], - }, - primaryType: 'TransferWithAuthorization', - domain: { - name: requirements.extra?.name ?? USDC_EIP712.name, - version: requirements.extra?.version ?? USDC_EIP712.version, - chainId: CHAIN_ID, - verifyingContract: requirements.asset ?? USDC_ADDRESS, - }, - message: authorization, - }; - - const signature = (await provider.request({ - method: 'eth_signTypedData_v4', - params: [from, JSON.stringify(typedData)], - })) as string; - - // The X-PAYMENT envelope the seller (and the x402.org facilitator) expect: - // base64(JSON payment payload), x402 v1 'exact' scheme. - const paymentPayload = { - x402Version: X402_VERSION, - scheme: 'exact', - network: NETWORK, - payload: { signature, authorization }, - }; - - return { header: jsonToB64(paymentPayload), from }; -} - -// --- payAndFetch ---------------------------------------------------------------- -// fetch → if 402, parse accepts[0], sign EIP-3009 authorization, retry ONCE with -// the X-PAYMENT header. Returns the final response plus the settlement receipt -// (parsed from the standard X-PAYMENT-RESPONSE header) when a payment was made. -export async function payAndFetch( - url: string, - init?: RequestInit, - session?: WalletSession, -): Promise<{ response: Response; paid?: PaidReceipt }> { - const first = await fetch(url, init); - if (first.status !== 402) { - return { response: first }; - } - - // -- parse the 402 challenge -------------------------------------------------- - let challenge: { x402Version?: number; accepts?: PaymentRequirements[] }; - try { - challenge = await first.json(); - } catch { - throw new Error('Got 402 but the challenge body was not JSON.'); - } - const requirements = challenge.accepts?.[0]; - if (!requirements) throw new Error('402 challenge carried no accepts[] payment requirements.'); - if (requirements.scheme !== 'exact' || requirements.network !== NETWORK) { - throw new Error( - `Unsupported payment requirements: scheme=${requirements.scheme} network=${requirements.network}`, - ); - } - - // -- sign and retry once -------------------------------------------------------- - const { header } = await signPayment(requirements, session); - const headers = new Headers(init?.headers); - headers.set('X-PAYMENT', header); - const response = await fetch(url, { ...init, headers }); - - // -- read the settlement receipt ------------------------------------------------- - let paid: PaidReceipt | undefined; - const receiptHeader = response.headers.get('X-PAYMENT-RESPONSE'); - if (receiptHeader) { - try { - const receipt = b64ToJson<{ success?: boolean; transaction?: string }>(receiptHeader); - if (receipt.transaction) { - paid = { - amountUSDC: Number(requirements.maxAmountRequired) / 10 ** USDC_DECIMALS, - txHash: receipt.transaction, - }; - } - } catch { - // Malformed receipt header — the JSON body still carries `paid` on success. - } - } - - return { response, paid }; -} diff --git a/site/app/manifesto.tsx b/site/app/manifesto.tsx index 7c4b1f6..1c1e146 100644 --- a/site/app/manifesto.tsx +++ b/site/app/manifesto.tsx @@ -1,5 +1,3 @@ -'use client'; - // VARIANT A — "THE TEN" (industrial wall text). // // A single towering vertical scroll on a white gallery wall: hollow-outlined @@ -9,31 +7,13 @@ // shipping label / customs form with corner crop marks. The footer is a woven // garment label. -import { useState } from 'react'; +import Link from 'next/link'; + import type { Manifesto } from './content'; -import { useInvoke, type InvokeState } from './components/useInvoke'; import styles from './manifesto.module.css'; const YELLOW = '#FFD100'; -const STATES: { key: InvokeState; label: string }[] = [ - { key: 'idle', label: 'IDLE' }, - { key: 'connecting', label: 'CONNECTING' }, - { key: 'paying', label: 'PAYING' }, - { key: 'running', label: 'RUNNING' }, - { key: 'done', label: 'DONE' }, - { key: 'error', label: 'ERROR' }, -]; - -const STATUS_LINE: Record = { - idle: 'AWAITING INSTRUCTION. NOTHING SIGNED, NOTHING SENT.', - connecting: 'CONNECTING WALLET — SWITCHING CHAIN TO BASE SEPOLIA.', - paying: '402 RECEIVED — SIGN, DON’T SEND. THE FACILITATOR SETTLES.', - running: 'PAYMENT SETTLED — SKILL EXECUTING. OUTPUT INBOUND.', - done: 'DELIVERED. YOU GOT THE OUTPUT. THE SKILL STAYED HOME.', - error: 'STOPPED. SEE INCIDENT REPORT BELOW.', -}; - const shortTx = (tx: string) => `${tx.slice(0, 10)}…${tx.slice(-8)}`; /* Hazard-stripe divider between major sections. */ @@ -66,21 +46,24 @@ function Barcode() { } export default function VariantA({ manifesto }: { manifesto: Manifesto }) { - const { state, output, paid, error, account, connect, invoke, needsWallet } = useInvoke(); - const [input, setInput] = useState(''); - const busy = state === 'connecting' || state === 'paying' || state === 'running'; - return (
{/* ————— STICKY INDUSTRIAL MASTHEAD STRIP ————— */}
{manifesto.project} - “MANIFESTO” + ARCHIVED EXPERIMENT Nº 402
+
+

STATIC HISTORY — NOT THE CURRENT PRODUCT OR A LIVE PAYMENT ENDPOINT

+ + OPEN THE CURRENT WALLET KERNEL PREVIEW → + +
+ {/* ————— MASTHEAD: TITLE / SUBTITLE / PREAMBLE ————— */}

{manifesto.subtitle}

@@ -91,7 +74,7 @@ export default function VariantA({ manifesto }: { manifesto: Manifesto }) { {manifesto.preamble}

- SCROLL ↓ — 10 PRINCIPLES · 2 RECEIPTS · 1 METERED ENDPOINT + SCROLL ↓ — 10 HISTORICAL PRINCIPLES · 1 VERIFIED RECEIPT · NO LIVE ENDPOINT

@@ -128,10 +111,10 @@ export default function VariantA({ manifesto }: { manifesto: Manifesto }) { {/* ————— PROOF — THE RECEIPTS ————— */} -
+
SECTION — EVIDENCE - “RECEIPTS” + “ONE RECEIPT”

{manifesto.proof.heading} @@ -142,7 +125,7 @@ export default function VariantA({ manifesto }: { manifesto: Manifesto }) {

- “LEDGER” — EVERY CENT RECONCILED + HISTORICAL TRANSFER — ONE RECEIPT VERIFIED

{manifesto.proof.ledger} @@ -175,7 +158,7 @@ export default function VariantA({ manifesto }: { manifesto: Manifesto }) { {/* ————— DO IT YOURSELF — SHIPPING LABEL / CUSTOMS FORM ————— */} -

+
- {/* Monopoly-money customs declaration + faucet */} + {/* Static archive notice */} {/* The four steps — form fields */} @@ -232,123 +207,16 @@ export default function VariantA({ manifesto }: { manifesto: Manifesto }) {
- {/* Field 04 — the signatory (step 01 made pressable) */} -
-

- FIELD 04 — SIGNATORY (STEP 01: CONNECT A WALLET) +

+

+ RETIRED — NO WALLET CONNECTION OR PAYMENT IS AVAILABLE ON THIS + WEBSITE.

- {account ? ( -

- CONNECTED — {shortTx(account)} · BASE SEPOLIA -

- ) : ( - - )} -
- - {/* Field 05 — the prompt */} -
- -