Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions apps/web/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ export default function AboutPage() {
open SDK, an MCP server, and pay-per-job settlement over x402.
</p>
<p>
The current service provides live, managed NVIDIA L40S capacity. A provider
program for operator-owned infrastructure is in technical onboarding and is not yet
available for production leases.
The current service provides live NVIDIA capacity across several card classes, sourced
from vetted providers. Operator-owned hardware is not part of the service today, so every
machine a customer rents is brokered, and the trust class on each offer says so.
</p>
</InformationSection>

Expand All @@ -45,7 +45,38 @@ export default function AboutPage() {
<p>Workspaces are fresh containers, not confidential computing, so the infrastructure provider remains inside their trust boundary. Each vault item names the weakest workspace class it may be released into, and the default is above what the network can currently serve.</p>
</InformationSection>

<InformationSection index="03" title="Open-source infrastructure">
<InformationSection index="03" title="What PRISM does on the network">
<p>
PRISM is the network&apos;s access token, not its currency. Compute is quoted and paid
in USDG or USDC, so a renter never has to hold PRISM to buy a GPU. What PRISM buys is a
cheaper rate on part of the fleet.
</p>
<h3>Staking unlocks discounted capacity</h3>
<p>
Operators can reserve a machine for stakers. Those offers are marked{" "}
<code>staker_only</code> and priced below open capacity: today they run at 177 base
units per second against 222 elsewhere, about 20 percent lower, and a wallet that has
not staked is refused rather than charged the higher rate. Stake matures before it
counts and unwinds through a cooldown, both enforced by the contract.
</p>
<h3>Where to check it</h3>
<p>
The token is{" "}
<a href="https://robinhoodchain.blockscout.com/token/0x0A1e0Cc751f77C2C93760FC957CC8E4E779b2bC8">
0x0A1e0Cc7
</a>{" "}
and the staking contract is{" "}
<a href="https://robinhoodchain.blockscout.com/address/0x7c4060e0b1f6954a90ea92Ee81C14b3b70D1be7c">
0x7c4060e0
</a>
, both on Robinhood Chain. The staking contract is source-verified and has no owner, no
pause and no upgrade path, so the terms it was deployed with are the terms it keeps.
Live capacity and which offers are staker-only are published on the{" "}
<a href="https://api.prismnetwork.tech/v1/offers">offers endpoint</a>.
</p>
</InformationSection>

<InformationSection index="04" title="Open-source infrastructure">
<p>
Prism&apos;s protocol, smart contracts, service architecture, and application code are
developed in the open. Architecture and operational contracts are
Expand Down
71 changes: 64 additions & 7 deletions inference/gateway.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const TX_HASH = /^0x[0-9a-f]{64}$/i;
// pays first, so its key is the transaction hash. The exact scheme authorizes
// first, so its key is the payer and the authorization nonce, which is also
// what the token contract itself refuses to reuse.
const PAYMENT_KEY = /^(0x[0-9a-f]{64}|0x[0-9a-f]{40}:0x[0-9a-f]{64})$/i;
const PAYMENT_KEY = /^([a-z0-9:-]+\|)?(0x[0-9a-f]{64}|0x[0-9a-f]{40}:0x[0-9a-f]{64})$/i;

const ROUTES = {
single: {
Expand Down Expand Up @@ -285,6 +285,15 @@ export const USDC_BASE_DOMAIN = { name: "USD Coin", version: "2" };
/// on-chain DOMAIN_SEPARATOR. A client that signs against the wrong domain
/// produces a well-formed signature the token rejects.
export const USDG_ROBINHOOD_DOMAIN = { name: "Global Dollar", version: "1" };
/// USDC is Arc's gas token, exposed both as an 18-decimal native balance and as
/// a 6-decimal ERC-20 at this predeploy. x402 amounts are ERC-20 units, so the
/// payload is unchanged; anything reading a native balance for the same money is
/// off by 10^12.
export const USDC_ARC = "0x3600000000000000000000000000000000000000";
/// Read off the testnet predeploy on 2026-09-14, and it is not Base's domain: Arc's
/// `name()` is "USDC" where Base returns "USD Coin". Signing Base's domain here
/// produces a well-formed signature that recovers to nobody.
export const USDC_ARC_DOMAIN = { name: "USDC", version: "2" };

export function loadConsumed(file) {
const set = new Set();
Expand All @@ -302,6 +311,9 @@ export function createGateway({
models,
payTo,
basePayTo = null,
arcPayTo = null,
arcNetwork = "eip155:5042",
arcTestnet = false,
exact = null,
originUrl = "https://api.prismnetwork.tech/inference",
// Carried in every 402 so an agent that arrives cold learns the price and how
Expand Down Expand Up @@ -661,7 +673,8 @@ export function createGateway({
detail,
state: phase(),
retry_after_seconds: seconds,
retry: "nothing was charged; send the same payment header again.",
retry: "nothing was charged; send the same payment header again, or sign a "
+ "fresh one if its validBefore has passed while you waited.",
},
};
}
Expand All @@ -678,7 +691,8 @@ export function createGateway({
error,
detail,
state: phase(),
retry: "nothing was charged; retry with the same payment header",
retry: "nothing was charged; retry with the same payment header, or sign a "
+ "fresh one if its validBefore has passed while you waited",
},
};
}
Expand Down Expand Up @@ -742,6 +756,36 @@ export function createGateway({
extra: { ...USDC_BASE_DOMAIN, assetTransferMethod: "eip3009" },
});
}
if (arcPayTo) {
list.push({
scheme: "exact",
network: arcNetwork,
asset: USDC_ARC,
payTo: arcPayTo,
amount: amount.toString(),
resource: `${originUrl}${route.path}`,
description:
`${route.unit} ${route.where}, paid in USDC on Arc${arcTestnet ? " testnet" : ""}. ` +
"Sign an EIP-3009 transferWithAuthorization for the quoted amount and send it as the " +
"payment header. You need no gas: the authorization is broadcast for you." +
// The other rails in this array settle on mainnets. A quote that lists
// a test chain beside them, priced the same and labelled only by its
// id, invites a caller to pick the wrong one and spend real money.
(arcTestnet ? " TEST NETWORK: this rail settles in play money and buys nothing." : ""),
mimeType: "application/json",
maxTimeoutSeconds: 60,
...(shape ? { outputSchema: shape } : {}),
// v2 strips `description` from an accepts entry and keeps `extra`, so a
// warning that only lives in the prose never reaches a v2 client. This
// rail sits beside mainnet rails priced identically, and its chain id is
// the only other thing telling them apart.
extra: {
...USDC_ARC_DOMAIN,
assetTransferMethod: "eip3009",
...(arcTestnet ? { testnet: true } : {}),
},
});
}
list.push({
scheme: "exact",
network: "eip155:4663",
Expand Down Expand Up @@ -810,9 +854,6 @@ export function createGateway({
if (typeof from !== "string" || typeof nonce !== "string") {
return { ok: false, reason: "invalid_payload" };
}
const key = `${from}:${nonce}`.toLowerCase();
if (!PAYMENT_KEY.test(key)) return { ok: false, reason: "invalid_payload" };

// The requirement we quoted, not the one the client echoed back: a payer
// who rewrites the amount or the recipient must fail, and comparing their
// copy against itself would always pass.
Expand All @@ -823,10 +864,26 @@ export function createGateway({
);
if (!want) return { ok: false, reason: "invalid_network" };

// Namespaced by chain: a payer whose client derives nonces deterministically
// pays the same nonce on two rails, and an un-namespaced key refuses the
// second as a replay. The token's own authorizationState is per chain
// already, so this only guards the local set.
const key = `${want.network}|${from}:${nonce}`.toLowerCase();
if (!PAYMENT_KEY.test(key)) return { ok: false, reason: "invalid_payload" };

// Checked before the spent-payment cache is consulted. The authorization
// and its signature are broadcast on chain to settle, so a payer and nonce
// read off a block would otherwise redeem the answer they bought.
const verdict = await exact.verify(parsed, want);
// An unreachable chain is our fault, not the payer's, but 402 is the only
// answer that lets them retry or pick another rail. Letting it throw would
// take the process down and every warm lease with it.
let verdict;
try {
verdict = await exact.verify(parsed, want);
} catch (error) {
log(`verify failed on ${want.network}: ${describe(error)}`);
return { ok: false, reason: "facilitator_unavailable" };
}
if (!verdict.isValid) return { ok: false, reason: verdict.invalidReason };

if (!reservePayment(key)) {
Expand Down
101 changes: 100 additions & 1 deletion inference/gateway.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test } from "node:test";
import { recoverMessageAddress } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { boundMessage, hashRequest } from "@prismnetwork/x402/codec";
import { createGateway, DEFAULT_PRICING, priceFor, SERVED_TTL_MS } from "./gateway.mjs";
import { createGateway, DEFAULT_PRICING, priceFor, SERVED_TTL_MS, USDC_ARC } from "./gateway.mjs";

// What a lease costs and what it produces. The rate is the network's, read off
// /v1/offers and confirmed by settled receipts (900s billed 199,800 micros).
Expand Down Expand Up @@ -812,3 +812,102 @@ test("a spent payment answers the request it bought and no other", async () => {
assert.equal(resigned.body.error, "payment_reused");
assert.equal(deps.calls.generations.length, 2, "the second request must not be served from the first");
});

const ARC_PAY_TO = "0xe67a61f8e2aC4057aa22e64306107E7120078447";

test("Arc is offered only when it is configured, and never displaces the other rails", async () => {
const without = build(fakeDeps(), { basePayTo: BASE_PAY_TO, exact: fakeExact() });
const bare = (await without.handleInference({ model: "llama3.2:3b", prompt: "hi" }, undefined, 2)).body.accepts;
assert.deepEqual(bare.map((a) => a.network), ["eip155:8453", "eip155:4663"]);

const withArc = build(fakeDeps(), {
basePayTo: BASE_PAY_TO,
arcPayTo: ARC_PAY_TO,
arcNetwork: "eip155:5042",
exact: fakeExact(),
});
const offered = (await withArc.handleInference({ model: "llama3.2:3b", prompt: "hi" }, undefined, 2)).body.accepts;
// Base stays the headline: validators read accepts[0], and leading with a
// chain they do not index makes a payable endpoint look unsupported.
assert.deepEqual(offered.map((a) => a.network), ["eip155:8453", "eip155:5042", "eip155:4663"]);
assert.equal(offered[0].amount, offered[1].amount, "the same work costs the same on either chain");
});

test("the Arc quote carries Arc's own EIP-712 domain, not Base's", async () => {
const gateway = build(fakeDeps(), {
arcPayTo: ARC_PAY_TO,
arcNetwork: "eip155:5042",
exact: fakeExact(),
});
const arc = (await gateway.handleInference({ model: "llama3.2:3b", prompt: "hi" }, undefined, 2)).body.accepts.find((a) => a.network.startsWith("eip155:5042"));
assert.equal(arc.extra.name, "USDC");
assert.equal(arc.extra.version, "2");
assert.equal(arc.extra.assetTransferMethod, "eip3009");
assert.equal(arc.asset, USDC_ARC);
assert.equal(arc.payTo, ARC_PAY_TO);
});

test("the quoted Arc network is the one configured, so a testnet quote cannot read as mainnet", async () => {
const gateway = build(fakeDeps(), {
arcPayTo: ARC_PAY_TO,
arcNetwork: "eip155:5042002",
exact: fakeExact(),
});
const arc = (await gateway.handleInference({ model: "llama3.2:3b", prompt: "hi" }, undefined, 2)).body.accepts.find((a) => a.network.startsWith("eip155:5042"));
assert.equal(arc.network, "eip155:5042002");
});

test("a chain that cannot be reached answers 402 rather than taking the process down", async () => {
const unreachable = {
handles: () => true,
verify: async () => { throw new Error("HTTP request failed: 403"); },
settle: async () => { throw new Error("unreachable"); },
};
const gateway = build(fakeDeps(), {
arcPayTo: ARC_PAY_TO,
arcNetwork: "eip155:5042",
exact: unreachable,
});
const quote = (await gateway.handleInference({ model: "llama3.2:3b", prompt: "hi" }, undefined, 2)).body;
const arc = quote.accepts.find((a) => a.network === "eip155:5042");
const header = Buffer.from(JSON.stringify({
x402Version: 2,
accepted: { scheme: "exact", network: arc.network, asset: arc.asset, payTo: arc.payTo },
payload: { authorization: { from: PAYER, to: arc.payTo, value: arc.amount,
validAfter: "0", validBefore: String(Math.floor(Date.now() / 1000) + 3600), nonce: `0x${"11".repeat(32)}` },
signature: `0x${"22".repeat(65)}` },
}), "utf8").toString("base64");

const answer = await gateway.handleInference({ model: "llama3.2:3b", prompt: "hi" }, header, 2);
assert.equal(answer.status, 402, "an unreachable rail is a 402, not a crash");
assert.equal(answer.body.error, "facilitator_unavailable");
});


test("a test network says so in the quote, because it sits beside rails that spend real money", async () => {
const gateway = build(fakeDeps(), {
basePayTo: BASE_PAY_TO,
arcPayTo: ARC_PAY_TO,
arcNetwork: "eip155:5042002",
arcTestnet: true,
exact: fakeExact(),
});
const offered = (await gateway.handleInference({ model: "llama3.2:3b", prompt: "hi" }, undefined, 2)).body.accepts;
const arc = offered.find((a) => a.network === "eip155:5042002");
// v2 drops `description`, so the marker has to survive in `extra`.
assert.equal(arc.extra.testnet, true);
// The mainnet rails beside it must stay unmarked, or the warning means nothing.
for (const other of offered.filter((a) => a !== arc)) {
assert.equal(other.extra?.testnet, undefined, `${other.network} is not a test network`);
}
});

test("the mainnet Arc rail carries no test warning", async () => {
const gateway = build(fakeDeps(), {
arcPayTo: ARC_PAY_TO, arcNetwork: "eip155:5042", exact: fakeExact(),
});
const arc = (await gateway.handleInference({ model: "llama3.2:3b", prompt: "hi" }, undefined, 2))
.body.accepts.find((a) => a.network === "eip155:5042");
assert.equal(arc.extra.testnet, undefined);
assert.equal(arc.extra.name, "USDC");
});
Loading
Loading