From d9f355153b673b5e81258e41a796360139f83f20 Mon Sep 17 00:00:00 2001 From: winterstacks Date: Sat, 26 Sep 2026 21:17:00 +0200 Subject: [PATCH 1/5] say what the network serves today: brokered NVIDIA capacity, early release, settled receipts Signed-off-by: winterstacks --- .gitignore | 4 + README.md | 55 +++--- apps/web/app/compute/opengraph-image.tsx | 2 +- apps/web/app/compute/twitter-image.tsx | 2 +- .../app/learn/how-a-lease-settles/page.tsx | 3 +- apps/web/app/pricing/opengraph-image.tsx | 2 +- apps/web/app/pricing/twitter-image.tsx | 2 +- apps/web/app/roadmap/page.tsx | 158 ++++++++++++++---- apps/web/components/developer-docs.tsx | 8 +- apps/web/components/landing-page.tsx | 21 +-- apps/web/components/public-footer.tsx | 2 +- apps/web/lib/faq.ts | 4 +- apps/web/proxy.ts | 2 +- python-sdk/prismnetwork/__init__.py | 2 +- python-sdk/pyproject.toml | 2 +- sdk/package.json | 2 +- x402/package.json | 2 +- 17 files changed, 190 insertions(+), 83 deletions(-) diff --git a/.gitignore b/.gitignore index f03d0f0..1ab34fa 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,10 @@ deploy/lightsail/secrets/ deploy/ec2/.env deploy/ec2/secrets/ +# Internal planning. Carries supplier cost prices and margins, so it never +# ships with the public tree. +docs/ROADMAP.md + mcp/package-lock.json x402/package-lock.json sdk/package-lock.json diff --git a/README.md b/README.md index bd024cb..debf781 100644 --- a/README.md +++ b/README.md @@ -17,33 +17,37 @@ Prism has two execution paths: - **Independent nodes:** Ubuntu 24.04 x86-64 hosts run public OCI images in Kata VM-backed containers with exclusive NVIDIA VFIO passthrough. Access uses short-lived SSH or Jupyter credentials through an outbound-only mTLS tunnel. -- **Vast broker:** a bonded broker provisions disposable L40S instances and - exposes direct SSH. This path relies on provider-reported readiness and - evidence; it does not provide Kata/VFIO isolation, the Prism gateway, or - Jupyter access. +- **Vast broker:** a bonded broker provisions disposable instances across several + NVIDIA classes and exposes direct SSH. This path relies on provider-reported + readiness and evidence; it does not provide Kata/VFIO isolation, the Prism + gateway, or Jupyter access. -Interactive raw GPU leases are what the network serves today. A lease can also -carry a single command instead of a session, which runs on an independent node -and reports back what it printed, but no independent node has completed the -hardware canary yet, so nothing has run that path in production. Managed -inference is not implemented. +All capacity served today comes from the broker path. `prismd` is not running on +any host and no independent node is enrolled, so the isolation the independent +path describes is written and tested but never reaches a customer. + +Interactive raw GPU leases are what the network serves today, and a renter can +end one early to stop the meter. A lease can also carry commands instead of a +session, which the broker path runs and reports back. Managed inference runs as +a separate pay-per-call endpoint. ## Current state -Verified on 2026-07-20: +Verified on 2026-09-07: | Area | Status | | --- | --- | -| Public web and API | Live at [prismnetwork.tech](https://prismnetwork.tech), with one Vast-backed L40S offer visible | -| Robinhood Chain contracts | Deployed on mainnet; the lease escrow is live | -| Vast execution | Implemented and locally lifecycle-tested; a funded mainnet canary has not been completed | -| Independent Kata nodes | Daemon, gateway, certificates, commands, tunnel and workspace lifecycle are implemented and integration-tested without physical GPU hardware | -| Settlement and proof | Workers and local end-to-end flows are implemented; no public mainnet settlement receipt exists yet | -| Batch commands | Implemented on the independent-node path; never executed on physical hardware | -| Managed inference | Planned, not implemented | - -This is an unaudited pre-production system, so do not put production traffic or -serious money on it yet. +| Public web and API | Live at [prismnetwork.tech](https://prismnetwork.tech); online offers fluctuate between one and three | +| Robinhood Chain contracts | Deployed on mainnet; the lease escrow is live and settling | +| Vast execution | Live on mainnet, funded, with settled leases and published receipts | +| Independent Kata nodes | Daemon, gateway, certificates, commands, tunnel and workspace lifecycle are implemented and integration-tested without physical GPU hardware; no node is enrolled | +| Settlement and proof | Live; settlement receipts are published at [prismnetwork.tech/proof](https://prismnetwork.tech/proof) | +| Early lease release | Live; `POST /v1/leases/{id}/release` stops the meter and returns unused escrow | +| Batch commands | Live on brokered capacity through the reproducible-run path; the independent-node command channel has never run on physical hardware | +| Managed inference | Live; two models on the open tier and nine on the confidential tier, paid per call over x402 | + +The contracts have not received an independent audit and the escrow caps are set +low deliberately. Size any deposit accordingly. What a supplier protects is stated per offer rather than as one blanket warning. Every offer, quote, lease and receipt carries a trust class (`open`, @@ -222,16 +226,17 @@ Docker with Compose and ripgrep. ## Remaining release gates -- Keep the escrow paused until a capped, funded mainnet canary completes from - deposit through refund or settlement. +Funded mainnet leases now settle end to end and their receipts are public, so +the deposit-through-settlement gate and the first-receipt gate are closed. What +is still open: + - Validate CUDA readiness, Kata isolation, VFIO assignment, egress controls and - teardown on physical NVIDIA hardware. + teardown on physical NVIDIA hardware, which no enrolled node has done. - Complete live KMS signing and failure-recovery evidence for lifecycle and settlement workers. - Exercise real Privy signup, external and embedded wallets, SSH access and Jupyter access against the release deployment. -- Publish the first confirmed proof receipt and test the independent daily X - digest outbox. +- Test the independent daily digest outbox. - Run applied-host backup/restore, load, failover and incident-response drills. - Obtain independent smart-contract and infrastructure security review before raising contract caps. diff --git a/apps/web/app/compute/opengraph-image.tsx b/apps/web/app/compute/opengraph-image.tsx index d59ffe5..7a8d8a5 100644 --- a/apps/web/app/compute/opengraph-image.tsx +++ b/apps/web/app/compute/opengraph-image.tsx @@ -8,6 +8,6 @@ export default function Image() { return renderOgImage({ eyebrow: "GPU compute", title: "Launch a workspace in minutes.", - tag: "NVIDIA L40S · Temporary SSH", + tag: "NVIDIA GPU · Temporary SSH", }); } diff --git a/apps/web/app/compute/twitter-image.tsx b/apps/web/app/compute/twitter-image.tsx index d59ffe5..7a8d8a5 100644 --- a/apps/web/app/compute/twitter-image.tsx +++ b/apps/web/app/compute/twitter-image.tsx @@ -8,6 +8,6 @@ export default function Image() { return renderOgImage({ eyebrow: "GPU compute", title: "Launch a workspace in minutes.", - tag: "NVIDIA L40S · Temporary SSH", + tag: "NVIDIA GPU · Temporary SSH", }); } diff --git a/apps/web/app/learn/how-a-lease-settles/page.tsx b/apps/web/app/learn/how-a-lease-settles/page.tsx index 00551f8..23606ad 100644 --- a/apps/web/app/learn/how-a-lease-settles/page.tsx +++ b/apps/web/app/learn/how-a-lease-settles/page.tsx @@ -56,7 +56,8 @@ export default function LeaseSettlementPage() {

Usage accrues per second for as long as the workspace is live, to a ceiling of six funded - hours. Current L40S capacity runs at 0.7992 USDG an hour, displayed as $0.80. + hours. Capacity runs at 0.7992 USDG an hour, displayed as $0.80, at the same rate for + every card class on offer.

diff --git a/apps/web/app/pricing/opengraph-image.tsx b/apps/web/app/pricing/opengraph-image.tsx index 156ccad..e4db4b1 100644 --- a/apps/web/app/pricing/opengraph-image.tsx +++ b/apps/web/app/pricing/opengraph-image.tsx @@ -7,7 +7,7 @@ export const alt = "Prism pricing"; export default function Image() { return renderOgImage({ eyebrow: "Pricing", - title: "L40S compute at \$0.80 per GPU hour.", + title: "GPU compute at \$0.80 per GPU hour.", tag: "Per-second billing · USDG", }); } diff --git a/apps/web/app/pricing/twitter-image.tsx b/apps/web/app/pricing/twitter-image.tsx index 156ccad..e4db4b1 100644 --- a/apps/web/app/pricing/twitter-image.tsx +++ b/apps/web/app/pricing/twitter-image.tsx @@ -7,7 +7,7 @@ export const alt = "Prism pricing"; export default function Image() { return renderOgImage({ eyebrow: "Pricing", - title: "L40S compute at \$0.80 per GPU hour.", + title: "GPU compute at \$0.80 per GPU hour.", tag: "Per-second billing · USDG", }); } diff --git a/apps/web/app/roadmap/page.tsx b/apps/web/app/roadmap/page.tsx index 96aba82..768a790 100644 --- a/apps/web/app/roadmap/page.tsx +++ b/apps/web/app/roadmap/page.tsx @@ -36,6 +36,22 @@ export default function RoadmapPage() { Any MCP client, Claude included, can list capacity, rent a GPU, and run a command through Prism. Listed in the official MCP registry.

+

Prism inside the agent frameworks people already use

+

+ An agent running Hermes gets three skills, rent a GPU, reproduce a CUDA run and read + the receipts, from{" "} + hermes plugins install prismnetwork-tech/hermes-plugin-prism. The published + Python package is a version behind and carries the terminal backend without those + skills, so the plugin install is the one to use. The Coinbase AgentKit action provider + is published and installable, so an agent built on AgentKit, LangGraph, or the Vercel AI + SDK reaches the same capacity with no custom code. +

+

Spend caps an agent cannot cross

+

+ Every paid call states the most it can cost before the agent pays it, and the SDK keeps a + running ledger against a session budget. An agent left to run unattended has a ceiling, + and the receipt afterwards separates what was charged from what came back. +

Onchain lease escrow and settlement

Funding, metered billing, and refunds execute through deployed contracts, bounded by a @@ -43,6 +59,13 @@ export default function RoadmapPage() { those contracts sits behind an enforced 48-hour timelock, so a change to who signs settlements or where fees go is scheduled in public two days before it can execute.

+

Ending a lease early

+

+ A renter closes the session the moment the work is done and the meter stops there. + Settlement charges the time the machine was actually open and returns the rest of the + escrow. One recent lease held a workstation GPU for twenty-eight seconds, charged 0.006216 + USDG and returned the remaining 0.126984 on the same receipt. +

Verifiable receipts

Every finalized lease publishes a settlement receipt that anyone can confirm on a block @@ -50,16 +73,24 @@ export default function RoadmapPage() {

Concurrent multi-class GPU capacity

- Supply sourced from vetted providers across several NVIDIA classes, including L40S, RTX - A6000, RTX 6000 Ada and RTX 5880 Ada, and matched to a lease on demand. The network - carries up to eighteen leases at once rather than one at a time. + Supply is sourced from vetted providers across several NVIDIA classes, including L40S, + RTX A6000, RTX 6000 Ada and RTX 5880 Ada, and matched to a lease on demand. Leases run + alongside each other rather than one at a time. How many machines are on offer at any + moment is a small number, usually one to three. Raising it is the first item below.

Managed and confidential inference

Text generation paid per call over x402, in USDG on Robinhood Chain or USDC on Base, with - no account or API key. The confidential tier runs the model inside a hardware enclave - with an attested GPU: the client checks the enclave's attestation before the prompt - is sent, so the prompt is readable only inside the enclave. + no account or API key. Two models on the open tier and nine on the confidential tier, + which runs the model inside a hardware enclave with an attested GPU. The client checks the + enclave's attestation before the prompt is sent, so the prompt is readable only + inside the enclave. +

+

Findable by an agent that already pays over x402

+

+ Pay-per-job GPU execution and the inference endpoint are both indexed in Coinbase's + x402 Bazaar and on agentic.market. An agent that speaks the payment protocol can find the + endpoint, read the price, and pay it without anyone integrating Prism first.

A pinned session key per lease

@@ -89,64 +120,129 @@ export default function RoadmapPage() {

- + +

+ The constraint on Prism right now is how much capacity is on offer at once, and what a + lease costs to run. That is where the work is. +

+

More machines on offer

+

+ The filter that decides which rented machines qualify asks for more memory than most of + the market has. On 7 September, nine machines across the supplier marketplace passed it. + Lowering the floor to 24 GB and accepting more card models takes that pool to about + sixty. You will see it as ten or more distinct offers on the public offers endpoint and + on this site, held for a week, with at least one settled receipt from a 24 GB card in the + public feed. Confidential inference keeps its current hardware floor, because the enclave + needs hardware consumer cards do not have. +

+

A quote that resolves to a machine

+

+ A funded lease reaches a shell inside four minutes when a host is there to take it. The + work now is confirming a rentable host at quote time, so a lease is only opened against + capacity that has already been checked. Escrow is still funded before anything is + provisioned, and that does not change. +

+

A LangChain package

- Work underway to widen where Prism reaches and to make provisioning dependable under real - demand. + The LangChain integration is finished and its last dependency is now on PyPI. It ships as + a standalone open source package, installable with pip. The name{" "} + langchain-prism on PyPI belongs to an unrelated project, so Prism's + package publishes under a different one and this page will carry it the day it is up. + LangChain stopped taking integrations into its own project in March, so a standalone + package is the only shape this can take.

-

Coinbase AgentKit provider

+

Prism in the tool catalogs

- The Prism action provider is published and installable today, so an agent built on - AgentKit, LangGraph, or the Vercel AI SDK can rent a GPU with no custom integration. - Contribution of the provider to AgentKit itself is the remaining step. + A listing on skills.sh, a listing on ClawHub, and an entry in the community Hermes plugin + index. One package reaches Hermes, Claude Code, Cursor, Codex, Copilot, Windsurf, + Gemini, Cline and about twenty more clients through the same install path.

-

Dependable on-demand provisioning

+
+ + +

Twenty offers, held for two weeks

+

+ Twenty or more distinct online offers on the public offers endpoint, continuously, for + fourteen days, with at least 95% of advertised capacity provisioning successfully when + leased. Advertised capacity counts only if it is there when a lease arrives for it. +

+

Cheaper settlement per lease

- Matching a lease to live capacity across a volatile spot market, so a request placed at any - moment resolves to a running machine. A funded lease now reaches a shell in about ninety - seconds. The remaining work is holding that under load and when a provider hands back a - host that will not boot. + Robinhood Chain fees rose ninefold in eleven days while the lease price stayed fixed, + which makes short leases expensive to settle. Two options are on the table: settle on + Base, or batch the writes on Robinhood Chain. The choice and its date go in the + documentation, and the target is under five cents of chain cost per settled lease, held + for thirty days. Receipts stay on Robinhood Chain either way, so the public trail does + not move.

-

Independent node operators

+

Spend controls, written down

- The relay a self-hosted machine reaches renters through now runs, and a self-hosted node - publishes the host key it generates so its leases verify end to end. What remains is the - hardware validation that lets such a node advertise a stronger trust class than brokered - capacity, and an operator fleet to carry it. + A public page stating the per-call and per-session caps an agent can set, what happens + when one is hit, and how to export receipts for accounting.

- +

Directional commitments that widen what the network can be trusted to run. Each ships only when it meets the same onchain, verifiable standard as the rest of Prism.

+

A second supplier

+

+ One broker supplies all of Prism's capacity today. The goal is offers sourced from + two independent brokers and a published drill showing at least five offers still served + with the first supplier switched off. The reason is continuity: capacity a customer can + rent should not depend on one company staying available. +

+

Receipts a stranger can check without us

+

+ A published verifier that takes a receipt, checks the settlement records on chain and the + enclave attestation behind them, and fails on a mismatch. It states which parts of a + receipt it verified and which parts the supplier reported, so the difference is visible + to whoever is reading it. +

+

Own hardware, once usage earns it

+

+ Independent nodes with exclusive GPU passthrough are the only route to the isolation that + rented capacity cannot promise. They are also a fixed cost that does not care whether + anything is leased, so they start when the network books 400 leased GPU hours a month for + two consecutive months. Until then all capacity is brokered and this site says so. +

Attested and confidential workspaces

The two classes above what rented workspaces can verify today: a launch measurement checked against vendor roots, then hardware-backed trusted execution with encrypted GPU memory, so a workload and its data stay private from the infrastructure provider. - Confidential inference already runs this way; extending it to a workspace a renter - controls needs hardware the network does not have yet, not a software change. + Confidential inference already runs this way. Extending it to a workspace a renter + controls waits on the hardware above.

Durable workspaces

Workspace storage that survives across leases. The vault carries credentials and small artifacts today; a full working disk that a new lease can resume is not built.

+
+ + +

More framework integrations

+

+ Six surfaces already reach Prism. Another wrapper does not change what a customer can do, + so a seventh is not scheduled. +

Stronger service-level remedies

The live commitment is not being charged for an interrupted machine. Performance - guarantees, and remedies beyond withholding payment, are not built. + guarantees, and remedies beyond withholding payment, are not built and are not scheduled.

-

Wider settlement and framework reach

+

Interruptible leases in 2026

- More agent frameworks and payment rails, so capacity is reachable from wherever an agent - already runs. + Preemptible capacity is roughly a third of the price and it breaks the duration a lease + commits to. It needs its own product class before it can be sold, and that is a 2027 + conversation.

- +

Verifiable over asserted

Settlement is recorded onchain, so a claim about what the network did is checkable rather diff --git a/apps/web/components/developer-docs.tsx b/apps/web/components/developer-docs.tsx index b96742f..70eefe3 100644 --- a/apps/web/components/developer-docs.tsx +++ b/apps/web/components/developer-docs.tsx @@ -166,7 +166,7 @@ export function DeveloperDocs() { Read the architecture ↓

-
Execution
L40S cloud
+
Execution
Brokered NVIDIA cloud
Settlement
Robinhood Chain + USDG
Access
Temporary, key-only SSH
Billing unit
Confirmed runtime second
@@ -191,7 +191,7 @@ export function DeveloperDocs() {
-

Managed cloud leases receive a temporary direct SSH endpoint. Operator-owned infrastructure uses revocable gateway grants over outbound mTLS tunnels.

+

Managed cloud leases receive a temporary direct SSH endpoint. Operator-owned infrastructure would use revocable gateway grants over outbound mTLS tunnels, and no host is enrolled on that path today.

PostgreSQL is the system of record for accounts, quotes, provider instances, lease transitions, settlement transactions, and proof publication.

@@ -318,7 +318,7 @@ export function DeveloperDocs() { - +

Operators can also serve open-class leases from machines they already own, with the @@ -424,7 +424,7 @@ export function DeveloperDocs() {

Provider launches reconcile by a unique lease label. Chain submissions persist signed bytes before broadcast. Workers retry from persisted state and reject conflicting final-state transitions.

-

Prism publishes an L40S offer only when available capacity satisfies model, VRAM, reliability, and pricing requirements.

+

Prism publishes an offer only when available capacity satisfies the model, VRAM, reliability and pricing requirements.

Provision failures close or refund rather than starting billing. Destruction is retried before final settlement. Emergency pause blocks new leases without blocking existing refunds.

diff --git a/apps/web/components/landing-page.tsx b/apps/web/components/landing-page.tsx index d0462dd..fc9f363 100644 --- a/apps/web/components/landing-page.tsx +++ b/apps/web/components/landing-page.tsx @@ -12,7 +12,7 @@ const agentSteps = [ const executionSteps = [ ["01", "Quote", "Review the per-second rate, funded duration, and maximum USDG commitment."], ["02", "Fund", "Approve the quoted amount and deposit it into the lease escrow contract."], - ["03", "Provision", "Prism assigns available L40S capacity and prepares an ephemeral workspace."], + ["03", "Provision", "Prism assigns an available NVIDIA card and prepares an ephemeral workspace."], ["04", "Meter", "Charges begin after GPU and access-readiness checks complete."], ["05", "Settle", "Confirmed usage is settled onchain and unused escrow is returned."], ] as const; @@ -50,7 +50,7 @@ export function LandingPage() {

Agent-native GPU compute

GPU compute your agents
can rent themselves.

- An agent authenticates with its wallet, leases an NVIDIA L40S, and pays per + An agent authenticates with its wallet, leases an NVIDIA GPU, and pays per second in USDG. No console, no API keys. Every lease is metered and settled onchain.

@@ -73,17 +73,18 @@ export function LandingPage() {

01 / Network

Compute and settlement
in one service.

- Managed L40S capacity is live. Operator-owned infrastructure is in technical - onboarding and is not yet available for production leases. + Every machine on the network today is rented from a vetted provider and matched to a + lease on demand. Operator-owned hardware is not part of the service, and the trust + class on each offer says what the host can and cannot see.

R / 01
-

L40S compute

-

46 GB of GPU memory from $0.80 per hour.

-

Run digest-pinned OCI images in ephemeral workspaces with temporary SSH access. Offers reflect real-time capacity.

+

Rent a GPU

+

48 GB NVIDIA cards at $0.80 per hour.

+

Run digest-pinned OCI images in ephemeral workspaces with temporary SSH access. Offers reflect real-time capacity, and a quote names the exact card before you fund anything.

Explore compute ↗
@@ -92,7 +93,7 @@ export function LandingPage() {

Provider program

Operate GPU capacity and earn from settled usage.

-

Review the requirements for operator-owned NVIDIA infrastructure. Approved providers receive 90% of confirmed usage charges.

+

The hardware, bonding and isolation requirements for operator-owned NVIDIA hosts are published. No independent host is enrolled today. The settlement terms pay a provider 90% of the confirmed charge on a finalized lease.

Review requirements ↗ @@ -144,7 +145,7 @@ export function LandingPage() { CONTAINER
GPU - L40S + NVIDIA
@@ -185,7 +186,7 @@ export function LandingPage() {
STATUS
SETTLED ONCHAIN
-
GPU MODEL
NVIDIA L40S
+
GPU MODEL
NVIDIA RTX 6000 ADA
BILLING
PER SECOND, USDG
NETWORK
ROBINHOOD CHAIN
FEED
/PROOF
diff --git a/apps/web/components/public-footer.tsx b/apps/web/components/public-footer.tsx index dce13a3..1139960 100644 --- a/apps/web/components/public-footer.tsx +++ b/apps/web/components/public-footer.tsx @@ -78,7 +78,7 @@ export function PublicFooter() { prism.

Metered GPU compute for autonomous agents, rented with a wallet.

- Live · Robinhood Chain · L40S + Live · Robinhood Chain · NVIDIA capacity {columns.map((column) => ( diff --git a/apps/web/lib/faq.ts b/apps/web/lib/faq.ts index 035e908..146fee0 100644 --- a/apps/web/lib/faq.ts +++ b/apps/web/lib/faq.ts @@ -16,7 +16,7 @@ export const faq = [ { question: "What does GPU compute cost?", answer: - "L40S capacity is 0.7992 USDG per hour, displayed as $0.80, metered per second. Billing starts only after GPU, pricing and access-readiness checks pass. A lease can run for at most six funded hours and escrow is capped at 50 USDG, with unused escrow returned when settlement finalizes.", + "Capacity is 0.7992 USDG per hour, displayed as $0.80, metered per second, at the same rate for every card class on offer. Billing starts only after GPU, pricing and access-readiness checks pass. A lease can run for at most six funded hours and escrow is capped at 50 USDG, with unused escrow returned when settlement finalizes.", }, { question: "Which GPUs can I rent?", @@ -51,7 +51,7 @@ export const faq = [ { question: "How do I supply GPUs and what do I earn?", answer: - "Providers receive 90% of the confirmed charge on a finalized lease, with 10% going to Prism as the service fee. Capacity has to be bonded and device-signed before it can be matched, and the provider program for operator-owned infrastructure is in technical onboarding.", + "Providers receive 90% of the confirmed charge on a finalized lease, with 10% going to Prism as the service fee. Capacity has to be bonded and device-signed before it can be matched. Every machine on the network today is rented from a vetted provider; no operator-owned host is enrolled yet, and the requirements for enrolling one are published on the provider page.", }, { question: "Is Prism audited?", diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index eaf323b..756d2d6 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -30,7 +30,7 @@ export function publicPageRewrite(hostname: string, pathname: string) { /// if they drift, because a stale hash silently drops the markup. export const STRUCTURED_DATA_HASHES = [ "sha256-X0tJmqBWy2c3p35NmEyLzHOmoQf1DZ1oE1C+rD0rFSs=", - "sha256-dHbYlUj0AgSutpV8NARzDW7bvn32OViL3zzave8JVco=", + "sha256-yU8NGougXIOk1yRPYOiHy2v7jE8G482SnTavRj10HBM=", ] as const; export function contentSecurityPolicy(nonce: string, development: boolean) { diff --git a/python-sdk/prismnetwork/__init__.py b/python-sdk/prismnetwork/__init__.py index 870117d..df10a7e 100644 --- a/python-sdk/prismnetwork/__init__.py +++ b/python-sdk/prismnetwork/__init__.py @@ -108,4 +108,4 @@ "keyset_digest", "report_data", ] -__version__ = "0.4.3" +__version__ = "0.4.4" diff --git a/python-sdk/pyproject.toml b/python-sdk/pyproject.toml index 7055142..9145b65 100644 --- a/python-sdk/pyproject.toml +++ b/python-sdk/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "prismnetwork" -version = "0.4.3" +version = "0.4.4" description = "Headless GPU leasing and metered confidential inference on Prism Network for autonomous agents. Wallet-signature auth, on-chain USDG payment, SSH." readme = "README.md" requires-python = ">=3.10" diff --git a/sdk/package.json b/sdk/package.json index 6f1356c..06d1e01 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@prismnetwork/agent-sdk", - "version": "0.7.11", + "version": "0.7.12", "description": "Headless GPU leasing and renter-encrypted storage on Prism Network for wallet-holding agents.", "type": "module", "main": "prism.mjs", diff --git a/x402/package.json b/x402/package.json index 0da87d1..4bfa3d1 100644 --- a/x402/package.json +++ b/x402/package.json @@ -1,6 +1,6 @@ { "name": "@prismnetwork/x402", - "version": "0.3.1", + "version": "0.3.2", "description": "Pay-per-job GPU compute on Prism Network over HTTP 402, payable in USDC on Base.", "type": "module", "bin": { From 060b42ed631f451feb4fa269af4f5457e69aafc6 Mon Sep 17 00:00:00 2001 From: winterstacks Date: Sat, 26 Sep 2026 21:22:27 +0200 Subject: [PATCH 2/5] add prism chain to the sdk: the chain for viem, bridge contracts, deposit and withdraw Signed-off-by: winterstacks --- sdk/README.md | 17 +++++ sdk/chain.d.mts | 32 +++++++++ sdk/chain.mjs | 163 +++++++++++++++++++++++++++++++++++++++++++++ sdk/chain.test.mjs | 58 ++++++++++++++++ sdk/package.json | 8 ++- sdk/prism.d.mts | 1 + sdk/prism.mjs | 1 + 7 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 sdk/chain.d.mts create mode 100644 sdk/chain.mjs create mode 100644 sdk/chain.test.mjs diff --git a/sdk/README.md b/sdk/README.md index 4005b85..5e32a7a 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -191,10 +191,27 @@ with `host_key_unpublished`. `lease()` (and the lower-level `fund()`) reproduce the escrow's quote binding: `clientReference = keccak256(quote_id)`, `approve(escrow, maximum_escrow)`, then `createLease(...)`, waiting 12 confirmations. +Pass `decision` and `policy` to `lease()` to authorise the spend first. The decision is checked against the policy before a quote is taken, and its hash is bound into the deposit: `clientReference = keccak256(keccak256(quote_id) ‖ decision_hash)`. Anyone holding the decision can recompute the reference and see it existed before the money moved. It does not show the decision was right. The helpers live in `@prismnetwork/agent-sdk/decision`. + ## Funding The wallet needs two balances on Robinhood Chain (id 4663): USDG (`0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`, 6 decimals) for the lease deposit, and native ETH for gas. Bridge from L1 to fund a fresh wallet. `authenticate()`, `offers()`, and `quote()` need neither, so the read paths work before you fund anything. +## Prism Chain + +Prism Chain (id 77476) is the Layer 3 for GPU compute, settled on Robinhood Chain. `@prismnetwork/agent-sdk/chain` exports the chain for viem and the bridge helpers: + +```js +import { depositToken, prismChain, withdraw } from "@prismnetwork/agent-sdk/chain"; + +// Robinhood Chain → Prism Chain, arrives in about a minute. +await depositToken(robinhoodWallet, USDG, 5_000_000n); +// Prism Chain → Robinhood Chain, claimable there after about a day. +await withdraw(prismWallet, 1_000_000n, { token: USDG }); +``` + +`depositEth` moves ETH the same way. Claim a finished withdrawal at [bridge.prismnetwork.tech](https://bridge.prismnetwork.tech). Leases still fund and settle on Robinhood Chain. + ## Requirements Node >= 20, `viem` ^2 (peer), and `ssh`, `ssh-keygen` and `ssh-keyscan` on PATH diff --git a/sdk/chain.d.mts b/sdk/chain.d.mts new file mode 100644 index 0000000..e4e518a --- /dev/null +++ b/sdk/chain.d.mts @@ -0,0 +1,32 @@ +import type { Address, Chain, Hash, WalletClient } from "viem"; + +export declare const prismChain: Chain; +export declare const PRISM_CHAIN_BRIDGE_URL: string; +export declare const prismChainContracts: { + rollup: Address; + inbox: Address; + outbox: Address; + parentRouter: Address; + parentStandardGateway: Address; + childRouter: Address; + arbSys: Address; +}; + +export interface TokenDepositFees { + maxGas: bigint; + gasPriceBid: bigint; + maxSubmissionCost: bigint; + /** Wei of ETH sent with the deposit; the unused part is refunded on Prism Chain. */ + value: bigint; +} + +interface RpcOptions { + parentRpcUrl?: string; + childRpcUrl?: string; +} + +export declare function prismChainToken(token: Address, options?: { rpcUrl?: string }): Promise
; +export declare function tokenDepositFees(token: Address, options?: RpcOptions): Promise; +export declare function depositEth(wallet: WalletClient, amount: bigint): Promise; +export declare function depositToken(wallet: WalletClient, token: Address, amount: bigint, options?: RpcOptions): Promise; +export declare function withdraw(wallet: WalletClient, amount: bigint, options?: { token?: Address | null }): Promise; diff --git a/sdk/chain.mjs b/sdk/chain.mjs new file mode 100644 index 0000000..2f02e08 --- /dev/null +++ b/sdk/chain.mjs @@ -0,0 +1,163 @@ +/// Prism Chain: the Layer 3 for GPU compute, settled on Robinhood Chain. +/// +/// Assets reach it through the canonical Arbitrum token bridge. A deposit lands +/// on Prism Chain in about a minute. A withdrawal starts on Prism Chain, +/// finalizes on Robinhood Chain after the rollup's confirmation window (about a +/// day), and is then claimed there; https://bridge.prismnetwork.tech does the +/// claim. Leases still settle on Robinhood Chain today. +import { createPublicClient, defineChain, encodeAbiParameters, erc20Abi, http, parseAbi } from "viem"; + +// Kept here rather than imported from prism.mjs, which pulls in Node modules; +// this file stays usable in a browser. +const robinhoodChain = defineChain({ + id: 4663, + name: "Robinhood Chain", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } }, +}); + +export const prismChain = defineChain({ + id: 77476, + name: "Prism Chain", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { + default: { http: ["https://rpc.prismnetwork.tech"], webSocket: ["wss://ws.prismnetwork.tech"] }, + }, + blockExplorers: { default: { name: "Prism Chain explorer", url: "https://explorer.prismnetwork.tech" } }, + sourceId: robinhoodChain.id, +}); + +export const PRISM_CHAIN_BRIDGE_URL = "https://bridge.prismnetwork.tech"; + +/// The rollup on Robinhood Chain and both halves of the token bridge. +export const prismChainContracts = { + rollup: "0x8232Ce617E7F1031F5B7bdcD72678dbdF2fDafeB", + inbox: "0x2FbD2fF90f56C58A8789Fc56e8b31F26e660438f", + outbox: "0x8cC14bF09cE7a1bf2415C70E5E4B9f50cD4a889d", + parentRouter: "0x52f5aF48C1DF13E0e1ABbe04F99622aabF6B2CD2", + parentStandardGateway: "0x1274b56aE5566014bB73dfE0A2247ab5BbC96470", + childRouter: "0x12d1c6c0b4b97278A7E2961eF16105Ba2dd2e859", + arbSys: "0x0000000000000000000000000000000000000064", +}; + +const inboxAbi = parseAbi([ + "function depositEth() payable returns (uint256)", + "function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) view returns (uint256)", +]); +const parentRouterAbi = parseAbi([ + "function getGateway(address token) view returns (address)", + "function outboundTransferCustomRefund(address token, address refundTo, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) payable returns (bytes)", +]); +const childRouterAbi = parseAbi([ + "function calculateL2TokenAddress(address l1Token) view returns (address)", + "function outboundTransfer(address l1Token, address to, uint256 amount, bytes data) payable returns (bytes)", +]); +const arbSysAbi = parseAbi(["function withdrawEth(address destination) payable returns (uint256)"]); + +const parent = (rpcUrl) => createPublicClient({ chain: robinhoodChain, transport: http(rpcUrl) }); +const child = (rpcUrl) => createPublicClient({ chain: prismChain, transport: http(rpcUrl) }); + +/// Where a Robinhood Chain token lives on Prism Chain. Its contract exists once +/// the first deposit of that token has landed. +export function prismChainToken(token, { rpcUrl } = {}) { + return child(rpcUrl).readContract({ + address: prismChainContracts.childRouter, + abi: childRouterAbi, + functionName: "calculateL2TokenAddress", + args: [token], + }); +} + +/// What a token deposit prepays for its call on Prism Chain, in wei of ETH on +/// Robinhood Chain. The first deposit of a token also deploys its contract +/// there, which needs more gas. Whatever goes unused is refunded to the sender +/// on Prism Chain, so this is a ceiling, not a price. +export async function tokenDepositFees(token, { parentRpcUrl, childRpcUrl } = {}) { + const [block, childGasPrice, deployed] = await Promise.all([ + parent(parentRpcUrl).getBlock(), + child(childRpcUrl).getGasPrice(), + prismChainToken(token, { rpcUrl: childRpcUrl }).then((address) => child(childRpcUrl).getCode({ address })), + ]); + const maxGas = deployed && deployed !== "0x" ? 300_000n : 1_200_000n; + const gasPriceBid = childGasPrice * 2n; + const submission = await parent(parentRpcUrl).readContract({ + address: prismChainContracts.inbox, + abi: inboxAbi, + functionName: "calculateRetryableSubmissionFee", + args: [2_000n, block.baseFeePerGas ?? 0n], + }); + const maxSubmissionCost = submission * 4n; + return { maxGas, gasPriceBid, maxSubmissionCost, value: maxSubmissionCost + maxGas * gasPriceBid }; +} + +/// Move ETH from Robinhood Chain to the same address on Prism Chain. `wallet` +/// is a viem wallet client on Robinhood Chain. Returns the Robinhood Chain +/// transaction hash. +export function depositEth(wallet, amount) { + return wallet.writeContract({ + chain: robinhoodChain, + address: prismChainContracts.inbox, + abi: inboxAbi, + functionName: "depositEth", + value: amount, + }); +} + +/// Move an ERC-20 such as USDG from Robinhood Chain to the same address on +/// Prism Chain. Approves the token's gateway first when the allowance is short. +export async function depositToken(wallet, token, amount, { parentRpcUrl, childRpcUrl } = {}) { + const account = wallet.account.address; + const reader = parent(parentRpcUrl); + const gateway = await reader.readContract({ + address: prismChainContracts.parentRouter, + abi: parentRouterAbi, + functionName: "getGateway", + args: [token], + }); + const allowance = await reader.readContract({ address: token, abi: erc20Abi, functionName: "allowance", args: [account, gateway] }); + if (allowance < amount) { + const approval = await wallet.writeContract({ + chain: robinhoodChain, + address: token, + abi: erc20Abi, + functionName: "approve", + args: [gateway, amount], + }); + await reader.waitForTransactionReceipt({ hash: approval }); + } + const fees = await tokenDepositFees(token, { parentRpcUrl, childRpcUrl }); + const data = encodeAbiParameters([{ type: "uint256" }, { type: "bytes" }], [fees.maxSubmissionCost, "0x"]); + return wallet.writeContract({ + chain: robinhoodChain, + address: prismChainContracts.parentRouter, + abi: parentRouterAbi, + functionName: "outboundTransferCustomRefund", + args: [token, account, account, amount, fees.maxGas, fees.gasPriceBid, data], + value: fees.value, + }); +} + +/// Start moving ETH, or a bridged token given by its Robinhood Chain address, +/// back to Robinhood Chain. `wallet` is a viem wallet client on Prism Chain. +/// The withdrawal is claimable on Robinhood Chain after about a day, at +/// PRISM_CHAIN_BRIDGE_URL. Returns the Prism Chain transaction hash. +export function withdraw(wallet, amount, { token = null } = {}) { + const account = wallet.account.address; + if (!token) { + return wallet.writeContract({ + chain: prismChain, + address: prismChainContracts.arbSys, + abi: arbSysAbi, + functionName: "withdrawEth", + args: [account], + value: amount, + }); + } + return wallet.writeContract({ + chain: prismChain, + address: prismChainContracts.childRouter, + abi: childRouterAbi, + functionName: "outboundTransfer", + args: [token, account, amount, "0x"], + }); +} diff --git a/sdk/chain.test.mjs b/sdk/chain.test.mjs new file mode 100644 index 0000000..6a21a08 --- /dev/null +++ b/sdk/chain.test.mjs @@ -0,0 +1,58 @@ +import { strict as assert } from "node:assert"; +import { test } from "node:test"; + +import { depositEth, prismChain, prismChainContracts, withdraw } from "./chain.mjs"; +import { prismChain as fromIndex } from "./prism.mjs"; + +const ME = "0x9F15121215A22f2D330501CD65f1c37086190dCB"; +const USDG = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"; + +// Records what would be signed instead of signing it. +function recorder() { + const calls = []; + return { + calls, + account: { address: ME }, + async writeContract(request) { + calls.push(request); + return "0xhash"; + }, + }; +} + +test("Prism Chain is chain 77476, settled on Robinhood Chain, and the index re-exports it", () => { + assert.equal(prismChain.id, 77476); + assert.equal(prismChain.sourceId, 4663); + assert.equal(prismChain.rpcUrls.default.http[0], "https://rpc.prismnetwork.tech"); + assert.equal(fromIndex, prismChain); +}); + +test("an ETH deposit goes to the inbox on Robinhood Chain with the amount as value", async () => { + const wallet = recorder(); + await depositEth(wallet, 5n); + const [call] = wallet.calls; + assert.equal(call.chain.id, 4663); + assert.equal(call.address, prismChainContracts.inbox); + assert.equal(call.functionName, "depositEth"); + assert.equal(call.value, 5n); +}); + +test("an ETH withdrawal starts on Prism Chain and returns to the sender", async () => { + const wallet = recorder(); + await withdraw(wallet, 7n); + const [call] = wallet.calls; + assert.equal(call.chain.id, 77476); + assert.equal(call.address, prismChainContracts.arbSys); + assert.deepEqual(call.args, [ME]); + assert.equal(call.value, 7n); +}); + +test("a token withdrawal names the Robinhood Chain token and sends no ETH", async () => { + const wallet = recorder(); + await withdraw(wallet, 500_000n, { token: USDG }); + const [call] = wallet.calls; + assert.equal(call.address, prismChainContracts.childRouter); + assert.equal(call.functionName, "outboundTransfer"); + assert.deepEqual(call.args, [USDG, ME, 500_000n, "0x"]); + assert.equal(call.value, undefined); +}); diff --git a/sdk/package.json b/sdk/package.json index 06d1e01..36abf7a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@prismnetwork/agent-sdk", - "version": "0.7.12", + "version": "0.8.0", "description": "Headless GPU leasing and renter-encrypted storage on Prism Network for wallet-holding agents.", "type": "module", "main": "prism.mjs", @@ -9,6 +9,10 @@ "types": "./prism.d.mts", "default": "./prism.mjs" }, + "./chain": { + "types": "./chain.d.mts", + "default": "./chain.mjs" + }, "./decision": { "default": "./decision.mjs" }, @@ -40,6 +44,8 @@ "files": [ "prism.mjs", "prism.d.mts", + "chain.mjs", + "chain.d.mts", "attest.mjs", "attest.d.mts", "decision.mjs", diff --git a/sdk/prism.d.mts b/sdk/prism.d.mts index 56b2ad5..9f604ae 100644 --- a/sdk/prism.d.mts +++ b/sdk/prism.d.mts @@ -1,6 +1,7 @@ import type { AttestationResult, VerifyConfidentialOptions, WorkloadPin } from "./attest.d.mts"; export declare const robinhoodChain: unknown; +export { PRISM_CHAIN_BRIDGE_URL, prismChain, prismChainContracts } from "./chain.d.mts"; export declare const USDG: string; export declare const DEFAULT_IMAGE: string; export declare const TRUST_CLASSES: readonly ["open", "isolated", "attested", "confidential"]; diff --git a/sdk/prism.mjs b/sdk/prism.mjs index 4c4576c..75998e4 100644 --- a/sdk/prism.mjs +++ b/sdk/prism.mjs @@ -15,6 +15,7 @@ import { stringToBytes, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; +export { PRISM_CHAIN_BRIDGE_URL, prismChain, prismChainContracts } from "./chain.mjs"; import { appraiseWorkload, DEFAULT_CONFIDENTIAL_BASE, EXPECTED_WORKLOAD, verifyConfidential } from "./attest.mjs"; import { decryptResponse, encryptChatRequest } from "./e2ee.mjs"; import { hostKeyArgs, HostKeyError } from "./hostkey.mjs"; From f67c16bab448c90afc418fd8b17239b8c37bf16d Mon Sep 17 00:00:00 2001 From: winterstacks Date: Sat, 26 Sep 2026 21:22:27 +0200 Subject: [PATCH 3/5] let the operator set a spend policy the mcp enforces before any lease is quoted Signed-off-by: winterstacks --- mcp/README.md | 26 ++++++++++ mcp/package.json | 5 +- mcp/policy.mjs | 117 ++++++++++++++++++++++++++++++++++++++++++++ mcp/policy.test.mjs | 85 ++++++++++++++++++++++++++++++++ mcp/server.json | 10 +++- mcp/server.mjs | 65 +++++++++++++++++++++++- 6 files changed, 302 insertions(+), 6 deletions(-) create mode 100644 mcp/policy.mjs create mode 100644 mcp/policy.test.mjs diff --git a/mcp/README.md b/mcp/README.md index 14c0549..4dd3074 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -106,6 +106,32 @@ share the ledger file. None of this is the real limit. Fund a dedicated wallet with what you are willing to lose: that balance is what survives a bug in everything above. +## Spend policy + +The limits above cap how much an agent spends. A spend policy decides whether a +given lease has a reason you accept. Set `PRISM_SPEND_POLICY` to the policy as +JSON, or to the path of a JSON file: + +```json +{ + "policy_id": "gpu-v1", + "allow": ["fine_tune", "benchmark"], + "require": ["needs_gpu"], + "minimums": { "needs_gpu": 0.8 } +} +``` + +Every lease tool then takes a `decision`: the action, what made the call, and +typed answers with a confidence from 0 to 1. A decision that misses the policy +is refused with every reason before anything is quoted, funded or counted +against the budget. One that passes is hashed into the escrow deposit, so the +lease on chain carries a reference only that decision reproduces. `prism_budget` +shows the policy in force. Without `PRISM_SPEND_POLICY` a decision is optional +and still binds when given. + +The binding shows a decision existed before the money moved and met your rules. +It does not show the decision was sound. + Tools that spend are annotated `destructiveHint` and carry `anthropic/requiresUserInteraction`, so Claude Code asks before every one of them even in modes that otherwise approve tools automatically. diff --git a/mcp/package.json b/mcp/package.json index 52d645b..d336eb0 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@prismnetwork/mcp", - "version": "0.9.4", + "version": "0.10.0", "description": "MCP server for leasing and running on Prism Network GPUs.", "mcpName": "io.github.prismnetwork-tech/mcp", "type": "module", @@ -9,6 +9,7 @@ }, "files": [ "budget.mjs", + "policy.mjs", "server.mjs", "README.md" ], @@ -18,7 +19,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "@phala/dcap-qvl": "^0.6.1", - "@prismnetwork/agent-sdk": "^0.7.8", + "@prismnetwork/agent-sdk": "^0.7.12", "jose": "^6", "viem": "^2" }, diff --git a/mcp/policy.mjs b/mcp/policy.mjs new file mode 100644 index 0000000..30b94ce --- /dev/null +++ b/mcp/policy.mjs @@ -0,0 +1,117 @@ +// The operator's spending rules for leases. The budget caps how much an agent +// may spend; the policy decides whether a given spend has a reason the operator +// accepts. The agent states its reason as a decision, the SDK checks it against +// this policy before anything is quoted, and only the decision's hash leaves +// the machine, bound into the escrow deposit. +import { readFileSync } from "node:fs"; +import { answer, decision as makeDecision, decisionDigest, refusals } from "@prismnetwork/agent-sdk/decision"; + +export class PolicyError extends Error { + constructor(message) { + super(message); + this.name = "PolicyError"; + } +} + +const strings = (value, field) => { + if (value === undefined) return []; + if (!Array.isArray(value) || value.some((v) => typeof v !== "string" || !v.trim())) { + throw new PolicyError(`PRISM_SPEND_POLICY: ${field} must be a list of names`); + } + return value; +}; + +/// PRISM_SPEND_POLICY holds the policy as JSON, or the path to a JSON file. +/// Unset means no policy: a decision is optional and, when given, still binds. +export function readPolicy(raw = process.env.PRISM_SPEND_POLICY) { + if (raw === undefined || raw.trim() === "") return null; + let text = raw.trim(); + if (!text.startsWith("{")) { + try { + text = readFileSync(text, "utf8"); + } catch (err) { + throw new PolicyError(`PRISM_SPEND_POLICY: cannot read ${raw}: ${err.code ?? err.message}`); + } + } + let parsed; + try { + parsed = JSON.parse(text); + } catch { + throw new PolicyError("PRISM_SPEND_POLICY is not valid JSON"); + } + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new PolicyError("PRISM_SPEND_POLICY must be a JSON object"); + } + const policyId = parsed.policy_id; + if (typeof policyId !== "string" || !policyId.trim()) { + throw new PolicyError("PRISM_SPEND_POLICY needs a policy_id, so every decision records which rules admitted it"); + } + const minimums = parsed.minimums ?? {}; + if (typeof minimums !== "object" || Array.isArray(minimums)) { + throw new PolicyError("PRISM_SPEND_POLICY: minimums must map an answer name to a confidence floor"); + } + for (const [name, floor] of Object.entries(minimums)) { + if (typeof floor !== "number" || !(floor >= 0 && floor <= 1)) { + throw new PolicyError(`PRISM_SPEND_POLICY: the floor for ${name} must be a number from 0 to 1`); + } + } + return { + policyId, + allow: strings(parsed.allow, "allow"), + require: strings(parsed.require, "require"), + minimums, + }; +} + +/// What prism_budget shows, so an agent can shape its decision before it asks. +export function describePolicy(policy) { + if (!policy) return { spend_policy: "none: leases need no stated reason" }; + return { + spend_policy: { + policy_id: policy.policyId, + allowed_actions: policy.allow.length ? policy.allow : "any", + required_answers: policy.require, + confidence_floors: policy.minimums, + }, + }; +} + +/// The decision a tool call carries, in the SDK's form. Malformed input is the +/// caller's mistake and says which field; a missing decision under a policy is +/// refused with what the policy needs. +export function decisionFrom(input, policy) { + if (input === undefined || input === null) { + if (!policy) return null; + const needs = [ + policy.allow.length ? `action one of ${policy.allow.join(", ")}` : "an action", + ...policy.require.map((n) => `an answer for ${n}`), + ...Object.entries(policy.minimums).map(([n, f]) => `${n} with confidence at least ${f}`), + ]; + throw new PolicyError( + `the operator's spend policy ${policy.policyId} requires a decision with this lease: ${needs.join("; ")}. Nothing was quoted or funded.`, + ); + } + if (typeof input !== "object") throw new PolicyError("decision must be an object with action, source and answers"); + const answers = (input.answers ?? []).map((a) => { + if (!a || typeof a.name !== "string") throw new PolicyError("each decision answer needs a name"); + return answer(a.name, a.value ?? null, a.confidence ?? null); + }); + return makeDecision({ + action: input.action, + source: input.source, + answers, + policyId: input.policy_id ?? policy?.policyId ?? null, + }); +} + +/// Checked here, before the spend is booked against the budget, so a refused +/// decision neither costs anything nor holds capacity. +export function authorised(policy, d) { + if (!policy) return; + const reasons = refusals(policy, d); + if (reasons.length) { + throw new PolicyError(`the spend policy ${policy.policyId} refused this lease: ${reasons.join("; ")}. Nothing was quoted or funded.`); + } +} + +export { decisionDigest }; diff --git a/mcp/policy.test.mjs b/mcp/policy.test.mjs new file mode 100644 index 0000000..81370a9 --- /dev/null +++ b/mcp/policy.test.mjs @@ -0,0 +1,85 @@ +import { strict as assert } from "node:assert"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; + +import { PolicyError, authorised, decisionDigest, decisionFrom, describePolicy, readPolicy } from "./policy.mjs"; + +const POLICY = JSON.stringify({ + policy_id: "gpu-v1", + allow: ["fine_tune", "benchmark"], + require: ["needs_gpu"], + minimums: { needs_gpu: 0.8 }, +}); + +test("no policy: a decision is optional and an absent one is fine", () => { + assert.equal(readPolicy(undefined), null); + assert.equal(readPolicy(" "), null); + assert.equal(decisionFrom(undefined, null), null); + assert.deepEqual(describePolicy(null), { spend_policy: "none: leases need no stated reason" }); +}); + +test("reads the policy from JSON or from a file path", () => { + const inline = readPolicy(POLICY); + assert.equal(inline.policyId, "gpu-v1"); + assert.deepEqual(inline.allow, ["fine_tune", "benchmark"]); + const dir = mkdtempSync(join(tmpdir(), "prism-policy-")); + const path = join(dir, "policy.json"); + writeFileSync(path, POLICY); + assert.deepEqual(readPolicy(path), inline); +}); + +test("a malformed policy is an error, never a silent pass", () => { + assert.throws(() => readPolicy("{not json"), PolicyError); + assert.throws(() => readPolicy("{}"), /policy_id/); + assert.throws(() => readPolicy('{"policy_id":"x","minimums":{"a":2}}'), /from 0 to 1/); + assert.throws(() => readPolicy('{"policy_id":"x","allow":"fine_tune"}'), /list of names/); + assert.throws(() => readPolicy("/nonexistent/policy.json"), /cannot read/); +}); + +test("under a policy, a lease with no decision is refused and says what the policy needs", () => { + const policy = readPolicy(POLICY); + assert.throws(() => decisionFrom(undefined, policy), (err) => { + assert.match(err.message, /gpu-v1/); + assert.match(err.message, /fine_tune, benchmark/); + assert.match(err.message, /needs_gpu with confidence at least 0.8/); + assert.match(err.message, /Nothing was quoted or funded/); + return true; + }); +}); + +test("a decision that meets the policy passes and carries the policy id", () => { + const policy = readPolicy(POLICY); + const d = decisionFrom( + { action: "fine_tune", source: "rule:test", answers: [{ name: "needs_gpu", value: true, confidence: 0.9 }] }, + policy, + ); + assert.equal(d.policyId, "gpu-v1"); + authorised(policy, d); + assert.match(decisionDigest(d), /^0x[0-9a-f]{64}$/); +}); + +test("a decision under the floor or outside the allowed actions is refused with every reason", () => { + const policy = readPolicy(POLICY); + const d = decisionFrom( + { action: "mine_crypto", source: "model", answers: [{ name: "needs_gpu", value: true, confidence: 0.5 }] }, + policy, + ); + assert.throws(() => authorised(policy, d), (err) => { + assert.match(err.message, /mine_crypto/); + assert.match(err.message, /under the 0.8 floor/); + return true; + }); +}); + +test("builds the same decision the SDK would, so the lease reference is the SDK's", async () => { + const sdk = await import("@prismnetwork/agent-sdk/decision"); + const input = { action: "fine_tune", source: "rule:test", answers: [{ name: "needs_gpu", value: true, confidence: 0.9 }] }; + const ours = decisionFrom(input, null); + const theirs = sdk.decision({ ...input, answers: [sdk.answer("needs_gpu", true, 0.9)] }); + assert.equal(decisionDigest(ours), sdk.decisionDigest(theirs)); + const quote = "3f2b1a44-9c7e-4d21-8b55-0a1c2d3e4f56"; + assert.equal(sdk.leaseReference(quote, ours), sdk.leaseReference(quote, theirs)); + assert.notEqual(sdk.leaseReference(quote, ours), sdk.leaseReference(quote, null)); +}); diff --git a/mcp/server.json b/mcp/server.json index 0fd53bf..c8176db 100644 --- a/mcp/server.json +++ b/mcp/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.prismnetwork-tech/mcp", "description": "Rent real NVIDIA GPUs from your agent. Browse with no wallet; pay per second onchain.", - "version": "0.9.4", + "version": "0.10.0", "websiteUrl": "https://prismnetwork.tech", "repository": { "url": "https://github.com/prismnetwork-tech/prism", @@ -14,7 +14,7 @@ "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "@prismnetwork/mcp", - "version": "0.9.4", + "version": "0.10.0", "transport": { "type": "stdio" }, @@ -42,6 +42,12 @@ "description": "Most any single lease or generation may cost. Defaults to 1. A max_usdg argument can lower it and cannot raise it", "isRequired": false, "default": "1" + }, + { + "name": "PRISM_SPEND_POLICY", + "description": "Optional spend policy as JSON or a path to a JSON file: policy_id, allowed actions, required answers and confidence floors. When set, every lease must carry a decision that meets it", + "isRequired": false, + "isSecret": false } ] } diff --git a/mcp/server.mjs b/mcp/server.mjs index 8fdea63..044e207 100644 --- a/mcp/server.mjs +++ b/mcp/server.mjs @@ -15,6 +15,7 @@ import { verifyConfidential, } from "@prismnetwork/agent-sdk"; import { BudgetError, SpendLedger, callCeiling, readBudget, recordSpend, stripUnexpanded } from "./budget.mjs"; +import { authorised, decisionDigest, decisionFrom, describePolicy, readPolicy } from "./policy.mjs"; stripUnexpanded(process.env); @@ -64,6 +65,26 @@ try { console.error(`prism mcp: ${budgetProblem}`); } +// Same rule as the budget: a policy the operator got wrong stops leasing rather +// than letting every lease through unchecked. +let policy = null; +let policyProblem = null; +try { + policy = readPolicy(); +} catch (err) { + policyProblem = err?.message ?? String(err); + console.error(`prism mcp: ${policyProblem}`); +} + +/// The decision this lease carries, checked against the operator's policy +/// before anything is booked, quoted or funded. +function leaseDecision(tool, args) { + if (policyProblem) throw new Error(`${tool} is disabled until the spend policy is fixed: ${policyProblem}`); + const d = decisionFrom(args.decision, policy); + if (d) authorised(policy, d); + return d; +} + function requireWallet(tool, reason = "spends money") { if (!agent) { throw new Error( @@ -211,6 +232,33 @@ const spends = { _meta: { "anthropic/requiresUserInteraction": true }, }; +// Why a lease is being funded. Only its hash leaves the machine, bound into the +// escrow deposit, so whoever holds the decision can prove it came first. +const DECISION_SCHEMA = { + type: "object", + description: + "Why this lease is being funded. Required when the operator set a spend policy (see prism_budget), and the lease is refused before anything is quoted if the decision does not meet it. Only a hash of it leaves this machine, recorded with the escrow deposit.", + properties: { + action: { type: "string", description: "What the spend is for, e.g. 'fine_tune' or 'benchmark'." }, + source: { type: "string", description: "What made the call: a model name, a rule, or 'operator'." }, + answers: { + type: "array", + description: "Typed answers behind the decision. confidence (0 to 1) is what a policy floor reads.", + items: { + type: "object", + properties: { + name: { type: "string", description: "Lowercase identifier, e.g. 'needs_gpu'." }, + value: { description: "The answer: an option, a score, or a probability." }, + confidence: { type: "number", minimum: 0, maximum: 1 }, + }, + required: ["name", "value"], + }, + }, + policy_id: { type: "string", description: "The policy this decision was made under; defaults to the operator's." }, + }, + required: ["action", "source"], +}; + const TOOLS = [ { name: "prism_budget", @@ -272,6 +320,7 @@ const TOOLS = [ duration_seconds: { type: "integer", description: "Paid window in seconds (default 900, max 21600). A command still running at the end is killed and reported exit 124." }, min_vram_mib: { type: "integer", description: "Minimum GPU memory in MiB (default 16000)." }, max_usdg: { type: "number", description: "Cost ceiling for this lease in USDG. It lowers the operator's PRISM_MAX_USDG and cannot raise it; omitted, that ceiling applies. See prism_budget." }, + decision: DECISION_SCHEMA, }, required: ["command"], }, @@ -371,6 +420,7 @@ const TOOLS = [ description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.", }, max_usdg: { type: "number", description: "Cost ceiling for this lease in USDG. It lowers the operator's PRISM_MAX_USDG and cannot raise it; omitted, that ceiling applies. See prism_budget." }, + decision: DECISION_SCHEMA, }, required: ["command"], }, @@ -391,6 +441,7 @@ const TOOLS = [ description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.", }, max_usdg: { type: "number", description: "Cost ceiling for this lease in USDG. It lowers the operator's PRISM_MAX_USDG and cannot raise it; omitted, that ceiling applies. See prism_budget." }, + decision: DECISION_SCHEMA, }, }, ...spends, @@ -483,7 +534,9 @@ const TOOLS = [ ]; async function handle(name, args) { - if (name === "prism_budget") return requireLedger(name).status(); + if (name === "prism_budget") { + return { ...requireLedger(name).status(), ...(policyProblem ? { spend_policy_error: policyProblem } : describePolicy(policy)) }; + } if (name === "prism_wallet") { const b = await requireWallet("prism_wallet").balances(); return { address: b.address, usdg: usdg(b.usdg), eth_wei: b.eth }; @@ -574,6 +627,7 @@ async function handle(name, args) { if (name === "prism_batch_run") { requireCommand(args.command); requireWallet(name); + const decision = leaseDecision(name, args); const cap = maxDeposit(name, args); return spending(name, cap, async () => { const batch = await agent.lease({ @@ -582,6 +636,8 @@ async function handle(name, args) { minVramMib: args.min_vram_mib ?? 16000, maxDeposit: cap, command: args.command, + decision, + policy, }); return { reference: batch.fundingHash, @@ -589,6 +645,7 @@ async function handle(name, args) { value: { lease_id: batch.leaseId, funding_tx: batch.fundingHash, + ...(decision ? { decision_hash: decisionDigest(decision) } : {}), exit_code: batch.result?.exit_code, stdout: batch.result?.stdout, stderr: batch.result?.stderr, @@ -731,6 +788,7 @@ async function handle(name, args) { if (name === "prism_lease_and_run" || name === "prism_lease") { if (name === "prism_lease_and_run") requireCommand(args.command); requireWallet(name); + const decision = leaseDecision(name, args); const cap = maxDeposit(name, args); sweepExpiredLeases(); const lease = await spending(name, cap, async () => { @@ -740,6 +798,8 @@ async function handle(name, args) { minVramMib: args.min_vram_mib ?? 16000, maxDeposit: cap, minTrustClass: args.min_trust_class ?? "open", + decision, + policy, }); return { value: funded, reference: funded.fundingHash, settledMicros: escrowed(funded.quote) }; }); @@ -747,6 +807,7 @@ async function handle(name, args) { const summary = { lease_id: lease.leaseId, funding_tx: lease.fundingHash, + ...(decision ? { decision_hash: decisionDigest(decision) } : {}), // `prism_run` checks this itself. It is in the summary because the // caller is being handed an address they may connect to by hand, and an // address with no key to check is an invitation to accept whatever @@ -850,7 +911,7 @@ async function handleVault(name, args) { throw new Error(`unknown tool ${name}`); } -const server = new Server({ name: "prism", version: "0.9.0" }, { capabilities: { tools: {} } }); +const server = new Server({ name: "prism", version: "0.10.0" }, { capabilities: { tools: {} } }); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { From 506abbd7b142cd648a5053fc81587d79ca26a884 Mon Sep 17 00:00:00 2001 From: winterstacks Date: Sat, 26 Sep 2026 21:23:48 +0200 Subject: [PATCH 4/5] put prism chain and spend authorisation on the site: roadmap, docs, faq, footer Signed-off-by: winterstacks --- apps/web/app/roadmap/page.tsx | 29 ++++++++++++++++++++------ apps/web/components/developer-docs.tsx | 21 +++++++++++++++++++ apps/web/components/public-footer.tsx | 2 ++ apps/web/lib/faq.ts | 5 +++++ apps/web/proxy.ts | 2 +- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/apps/web/app/roadmap/page.tsx b/apps/web/app/roadmap/page.tsx index 768a790..9693b53 100644 --- a/apps/web/app/roadmap/page.tsx +++ b/apps/web/app/roadmap/page.tsx @@ -52,6 +52,23 @@ export default function RoadmapPage() { running ledger against a session budget. An agent left to run unattended has a ceiling, and the receipt afterwards separates what was charged from what came back.

+

Prism Chain

+

+ A Layer 3 built for GPU compute and settled on Robinhood Chain, with its own{" "} + explorer and{" "} + bridge. ETH, USDG and PRISM move between + the two chains through the canonical Arbitrum token bridge. Deposits arrive in about a + minute; withdrawals finalize in about a day. The TypeScript SDK exports the chain and the + bridge calls. Leases still settle on Robinhood Chain, and moving them is the next step + below. +

+

A stated reason behind every spend

+

+ An operator writes a policy: which actions an agent may fund and how sure it has to be. + The agent's decision is checked before anything is quoted, and its hash is recorded with + the escrow deposit, so the lease onchain points back to the reason it was funded. Both + SDKs and the MCP server support it. +

Onchain lease escrow and settlement

Funding, metered billing, and refunds execute through deployed contracts, bounded by a @@ -166,14 +183,14 @@ export default function RoadmapPage() { fourteen days, with at least 95% of advertised capacity provisioning successfully when leased. Advertised capacity counts only if it is there when a lease arrives for it.

-

Cheaper settlement per lease

+

Leases settle on Prism Chain

Robinhood Chain fees rose ninefold in eleven days while the lease price stayed fixed, - which makes short leases expensive to settle. Two options are on the table: settle on - Base, or batch the writes on Robinhood Chain. The choice and its date go in the - documentation, and the target is under five cents of chain cost per settled lease, held - for thirty days. Receipts stay on Robinhood Chain either way, so the public trail does - not move. + which makes short leases expensive to settle. The escrow and settlement contracts move to + Prism Chain, funded with USDG bridged from Robinhood Chain, where a transaction costs a + fraction of a cent. The target is under one cent of chain cost per settled lease, held + for thirty days. Prism Chain posts its data to Robinhood Chain, so the public trail stays + there.

Spend controls, written down

diff --git a/apps/web/components/developer-docs.tsx b/apps/web/components/developer-docs.tsx index 70eefe3..748c7a0 100644 --- a/apps/web/components/developer-docs.tsx +++ b/apps/web/components/developer-docs.tsx @@ -361,6 +361,27 @@ export function DeveloperDocs() { independent production audit. Verify contract addresses, bytecode, constructor inputs, and current pause state directly before building a financial dependency. + Prism Chain +

+ Prism Chain is the Layer 3 for GPU compute, settled on Robinhood Chain. ETH, USDG and + PRISM reach it through the canonical Arbitrum token bridge at{" "} + bridge.prismnetwork.tech. A deposit + lands in about a minute. A withdrawal finalizes on Robinhood Chain after about a day + and is then claimed there. Leases are funded and settled on Robinhood Chain today. +

+
+
Chain ID
77476
+
RPC
rpc.prismnetwork.tech
+
WebSocket
ws.prismnetwork.tech
+ +
Gas token
ETH
+
Settles on
Robinhood Chain (4663)
+
+

+ The TypeScript SDK exports the chain for viem and the bridge calls from{" "} + @prismnetwork/agent-sdk/chain: depositEth,{" "} + depositToken and withdraw. +

diff --git a/apps/web/components/public-footer.tsx b/apps/web/components/public-footer.tsx index 1139960..74f77c3 100644 --- a/apps/web/components/public-footer.tsx +++ b/apps/web/components/public-footer.tsx @@ -24,6 +24,8 @@ const columns = [ ["Activity", new URL("/activity", siteUrl).href, false], ["Receipts", new URL("/proof", siteUrl).href, false], ["Network", new URL("/network", siteUrl).href, false], + ["Prism Chain explorer", "https://explorer.prismnetwork.tech", true], + ["Bridge", "https://bridge.prismnetwork.tech", true], ], }, { diff --git a/apps/web/lib/faq.ts b/apps/web/lib/faq.ts index 146fee0..e501466 100644 --- a/apps/web/lib/faq.ts +++ b/apps/web/lib/faq.ts @@ -38,6 +38,11 @@ export const faq = [ answer: "Items are sealed on your machine with a key derived from a wallet signature that is never transmitted, so Prism stores ciphertext and holds no means of reading it. Each item names the weakest class of workspace it may be released into, and a release into anything below that floor is refused.", }, + { + question: "What is Prism Chain?", + answer: + "A Layer 3 built for GPU compute and settled on Robinhood Chain, chain id 77476. ETH, USDG and PRISM move to it and back at bridge.prismnetwork.tech, and its activity is public at explorer.prismnetwork.tech. Leases are funded and settled on Robinhood Chain today; moving them to Prism Chain is on the roadmap.", + }, { question: "Do I need an account to rent a GPU?", answer: diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index 756d2d6..b30e214 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -30,7 +30,7 @@ export function publicPageRewrite(hostname: string, pathname: string) { /// if they drift, because a stale hash silently drops the markup. export const STRUCTURED_DATA_HASHES = [ "sha256-X0tJmqBWy2c3p35NmEyLzHOmoQf1DZ1oE1C+rD0rFSs=", - "sha256-yU8NGougXIOk1yRPYOiHy2v7jE8G482SnTavRj10HBM=", + "sha256-zD2vBJoxa/5Og6XXxH1m+Pgfx9IuJy4icR1mZvw01RM=", ] as const; export function contentSecurityPolicy(nonce: string, development: boolean) { From bba8d664969918dc113d1c9e3a5d67ee51a17b1c Mon Sep 17 00:00:00 2001 From: winterstacks Date: Sat, 26 Sep 2026 21:26:42 +0200 Subject: [PATCH 5/5] pin policy.mjs in the mcp archive check Signed-off-by: winterstacks --- scripts/validate-mcp-package.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/validate-mcp-package.mjs b/scripts/validate-mcp-package.mjs index f37b3b9..92aedd2 100644 --- a/scripts/validate-mcp-package.mjs +++ b/scripts/validate-mcp-package.mjs @@ -37,9 +37,10 @@ assert.equal(packed[0].version, packageJson.version); // Every file the server imports has to be in the archive. `budget.mjs` carries // the spend ledger, so shipping without it leaves a package that installs and // then throws on first import, which is the failure this check exists to catch. +// `policy.mjs` is imported the same way, so it is pinned for the same reason. assert.deepEqual( packed[0].files.map(({ path }) => path).sort(), - ["README.md", "budget.mjs", "package.json", "server.mjs"], + ["README.md", "budget.mjs", "package.json", "policy.mjs", "server.mjs"], "npm archive contents changed", );