Skip to content
Open
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
65 changes: 47 additions & 18 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,72 @@ NETWORK=testnet

# Required for MPP servers: binds payment challenges to their contents so the
# server can verify incoming credentials match challenges it issued.
# MAINNET: the server refuses to boot on this placeholder, the dev default, or
# any secret shorter than 24 chars — set a long random string (e.g. openssl rand -hex 32).
MPP_SECRET_KEY=change-me-to-a-long-random-string

# Wallet that RECEIVES stablecoin payments (your earnings address on Tempo).
# Testnet: funded with pathUSD via faucet. Mainnet: funded with real USDC.e.
# Testnet: funded with pathUSD via faucet. Mainnet: receives real USDC.e.
# MAINNET: the server refuses to boot on the built-in demo address — set your own.
TEMPO_RECIPIENT=0x…

# Optional: the PAYEE's private key (address MUST equal TEMPO_RECIPIENT). When set, the
# server cooperatively CLOSES the channel on-chain as the payee (clean open+close ≈ 2 txs,
# paying the fee from its own balance) instead of letting the payer's deposit reclaim on
# channel timeout. Unset → close is best-effort. Must be a wallet whose key you hold.
# Optional: the PAYEE's private key (address MUST equal TEMPO_RECIPIENT — mismatch is
# fatal on mainnet). When set, the server cooperatively CLOSES the channel on-chain as
# the payee (clean open+close ≈ 2 txs, paying the fee from its own balance) instead of
# letting the payer's deposit reclaim on channel timeout. Unset → close is best-effort
# and unsettled revenue is lost on restart (state is in-memory). Must be a wallet whose
# key you hold. Tempo has NO native gas token — fees are paid in stablecoin, so on
# mainnet this account needs a small USDC.e balance for close-tx fees.
TEMPO_RECIPIENT_PRIVATE_KEY=

# The real Phala Intel TDX enclave (the private upstream). Unset → stub mode: the
# server runs an offline demo, /health reports ready:false, and agents (correctly)
# refuse to pay. Required for any deployment that should actually earn.
TEE_ENDPOINT=

# Optional strict-pin: reject the enclave quote unless mrtd/rtmr equals this value.
# Unset → soft-pin (compare-to-advertised + display only). See DESIGN §1.
EXPECTED_MEASUREMENT=

# Pricing (decimal token units; TIP-20 stablecoins use 6 decimals).
PRICE_PER_CALL=0.01 # one-time `charge` per non-stream request
PRICE_PER_UNIT=0.0002 # `session` per-response-chunk price for streaming

# ── Upstream LLM (OpenAI-compatible). Leave UPSTREAM_URL empty to use the
# built-in MOCK streamer so the server runs fully offline for a demo. ───────
# Point this at SolRouter, OpenAI, OpenRouter, a local model, etc.
UPSTREAM_URL=
UPSTREAM_API_KEY=
# How many SSE chunks to slice a response into — each chunk = one MPP voucher tick.
# 1 = one charge per inference; >1 = metered in up to N ticks (short responses
# produce fewer chunks; billing never exceeds N — ADR-0003).
CHUNK_COUNT=1

# Default model id the blind relay forwards to the enclave when a client omits one.
UPSTREAM_MODEL=gpt-oss:20b

# Request-body cap in bytes (encrypted prompt only; management POSTs are header-only).
MAX_BODY_BYTES=1000000

PORT=8402
LOG_LEVEL=info

# ── Client/agent side (src/agent.ts) ─────────────────────────────────────────
# Funded Tempo wallet key used by the paying agent.
# ── Client/agent side (src/agent.ts, sdk, cli, mcp) ──────────────────────────
# Funded Tempo wallet key used by the paying agent. The client's NETWORK must match
# the server's (chain mismatch → vouchers reject with CHAIN_MISMATCH, ADR-0002).
# Testnet: get test pathUSD from the faucet at https://explore.testnet.tempo.xyz
# Mainnet: fund with real USDC.e via your Tempo wallet
AGENT_PRIVATE_KEY=
SERVER_URL=http://localhost:8402
# Read by BOTH sides, in stablecoin units (real dollars on mainnet):
# - Server: suggestedDeposit in every 402 challenge — compliant agents may escrow
# this much per session, so keep it small.
# - Client (agent/sdk/cli/mcp): spend cap per session.
MAX_DEPOSIT=1

# ── Mainnet notes ───────────────────────────────────────────────────────────
# To run on mainnet:
# 1. Set NETWORK=mainnet
# 2. Set TEMPO_RECIPIENT to your mainnet earnings address
# 3. Set TEMPO_RECIPIENT_PRIVATE_KEY (mainnet key for cooperative close)
# 4. Set AGENT_PRIVATE_KEY to a mainnet wallet funded with USDC.e
# 5. Currency automatically switches from pathUSD → USDC.e (chain 4217)
# 6. RPC automatically switches to https://rpc.tempo.xyz
# 2. Set TEMPO_RECIPIENT to your mainnet earnings address (demo default = boot failure)
# 3. Set MPP_SECRET_KEY to a long random string (dev default = boot failure)
# 4. Set TEE_ENDPOINT to the real enclave (stub mode earns nothing)
# 5. Set TEMPO_RECIPIENT_PRIVATE_KEY (same address as TEMPO_RECIPIENT; fund it with a
# little USDC.e — Tempo gas is paid in stablecoin) for cooperative close
# 6. Clients: NETWORK=mainnet + AGENT_PRIVATE_KEY funded with real USDC.e
# 7. Currency auto-switches pathUSD → USDC.e (chain 4217); RPC → https://rpc.tempo.xyz
# 8. Voucher state is in-memory (Store.memory) — settle before restarts/redeploys or
# unsettled revenue is lost. Persistent store is a known follow-up.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ Environment variables (see [`.env.example`](.env.example)):
|---|---|---|---|
| `TEE_ENDPOINT` | No | — | Phala TDX base URL (`…/tee`). Unset → stub mode. |
| `NETWORK` | No | `testnet` | `testnet` (Moderato 42431) or `mainnet` (Allegro 4217). |
| `MPP_SECRET_KEY` | Yes | — | HMAC secret for challenge binding (not a chain key). |
| `TEMPO_RECIPIENT` | Yes | — | Wallet address receiving stablecoin payments. |
| `MPP_SECRET_KEY` | Yes | — | HMAC secret for challenge binding (not a chain key). Mainnet: ≥24 chars, placeholders refused at boot. |
| `TEMPO_RECIPIENT` | Yes | — | Wallet address receiving stablecoin payments. Mainnet: must be a valid, non-demo address (boot guard). |
| `TEMPO_RECIPIENT_PRIVATE_KEY` | No | — | Payee key for cooperative channel close. |
| `PRICE_PER_UNIT` | No | `0.0002` | Stablecoin per response-chunk. |
| `CHUNK_COUNT` | No | `1` | SSE chunks per inference (metering granularity). |
Expand Down Expand Up @@ -229,8 +229,19 @@ decrypt to plaintext answer.`
- ✅ Multi-unit streaming supported (`CHUNK_COUNT > 1`); prod bills one charge per inference
- ✅ Cooperative `manager.close()` settles on-chain as the payee

**🛡️ Mainnet readiness VERIFIED on the hardening branch (2026-07-14, PR #3):**

- ✅ `NETWORK=mainnet` boot serves Allegro (chain 4217, USDC.e) consistently on `/`, `/llms.txt`, `/SKILL.md`, `/openapi.json`; testnet behavior unchanged
- ✅ Fail-closed boot guards: demo recipient, placeholder/short `MPP_SECRET_KEY`, payee-key mismatch, invalid recipient address — all refuse to boot on mainnet; unknown `NETWORK` values crash instead of silently running testnet
- ✅ Chain facts verified against live RPC + official tokenlist (chainId `4217`, USDC.e `0x20C0…8b50`, 6 dp)
- ✅ Staging E2E on the branch: real enclave (`tdx-live`), paid run, **first on-chain cooperative close** (see [evidence.md](evidence.md), staging-run section)
- ⏳ Cutover pending: mainnet env on the deployed service + explicit deploy (the service has never auto-deployed on push — deploy explicitly) + pricing decision below

## TODO

- [ ] **Fix session economics before mainnet** — close fee (~0.0006) > per-session revenue (0.0002 × 1 chunk): raise `PRICE_PER_UNIT`, meter more chunks, or batch settlement (mppx `settlementSchedule`)
- [ ] **Persistent voucher store** — `Store.memory()` loses unsettled vouchers on restart (real revenue on mainnet); wire an mppx redis/upstash store
- [ ] **Mainnet cutover** — set mainnet env on the deployed service (see `.env.example` Mainnet notes) + explicit deploy + small real-money E2E
- [ ] **List on MPPScan** — register at https://www.mppscan.com/register (instant, ~2 min)
- [ ] **List on mpp.dev/services** — open PR to `quiknode-labs/mpp-dev-official-docs` (see [DISCOVERY.md](docs/DISCOVERY.md))
- [ ] **Add GitHub topics** — `mpp`, `ai-inference`, `tee`, `intel-tdx`, `privacy`, `stablecoins`, `agents`
Expand Down
12 changes: 6 additions & 6 deletions cli/temprouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import { parseArgs } from 'node:util'
import process from 'node:process'
import { TempRouter, detectSensitive, formatReport, AttestationError } from '../sdk/src/index.js'
import { config } from '../src/config.js'
import { config, tempoChain, isMainnet } from '../src/config.js'

const USAGE = `temprouter — attestation-gated, MPP-paid confidential inference on Tempo

usage:
temprouter infer "<prompt>" [--model <m>] [--server <url>] [--max-deposit <n>] [--json]
verify the enclave, pay per response-chunk in pathUSD, decrypt locally.
verify the enclave, pay per response-chunk in the network's stablecoin, decrypt locally.
temprouter verify [--server <url>]
run the pre-pay attestation gate only. Never pays. Exit 0 if it passes.
temprouter detect "<text>"
Expand All @@ -23,7 +23,7 @@ usage:
flags:
--model <m> model id (default from SDK, e.g. nosana:gpt-oss:20b)
--server <url> tempRouter base URL (default: $SERVER_URL or config)
--max-deposit <n> pathUSD deposit headroom (default: config.maxDeposit)
--max-deposit <n> stablecoin deposit headroom (default: config.maxDeposit)
--json machine-readable output for 'infer' (no decorative lines)
-h, --help show this help
`
Expand All @@ -43,7 +43,7 @@ async function cmdInfer(prompt: string, flags: Record<string, unknown>) {
}

if (!config.agentPrivateKey) {
console.error('AGENT_PRIVATE_KEY not set (fund a Tempo testnet wallet)')
console.error(`AGENT_PRIVATE_KEY not set (fund a Tempo ${isMainnet ? 'MAINNET wallet with real USDC.e' : 'testnet wallet'})`)
process.exit(2)
}

Expand All @@ -62,7 +62,7 @@ async function cmdInfer(prompt: string, flags: Record<string, unknown>) {
if (!json) console.log('\n── pre-pay attestation gate ──\n' + formatReport(r))
},
onUnit: (n, paid) => {
if (!json) process.stdout.write(`\r 💸 [units paid: ${n} | ${paid} pathUSD]`)
if (!json) process.stdout.write(`\r 💸 [units paid: ${n} | ${paid} ${tempoChain.currencyName}]`)
},
})

Expand All @@ -87,7 +87,7 @@ async function cmdInfer(prompt: string, flags: Record<string, unknown>) {
console.log('\n── post-pay receipt verification ──\n' + formatReport(res.attestation.postPay))
}
console.log('\n🔓 decrypted answer (plaintext only ever seen by you + the attested enclave):\n' + res.answer)
console.log(`\n(${res.units} units · ${res.paid} pathUSD)`)
console.log(`\n(${res.units} units · ${res.paid} ${tempoChain.currencyName})`)
} catch (e) {
if (e instanceof AttestationError) {
console.error('⛔ ' + e.message + '\n' + formatReport(e.report))
Expand Down
36 changes: 36 additions & 0 deletions evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ content.
returns exactly one event — the OPEN — and no close. The production server runs close **best-effort**
(no payee settlement key set), so the channel deposit reclaims on **timeout** rather than settling
on-chain. Vouchers are off-chain, so this is the expected MPP shape, not a failure.
> **Update 2026-07-14:** cooperative close HAS now been demonstrated on-chain — see the
> staging run section below. It remains disabled in prod until `TEMPO_RECIPIENT_PRIVATE_KEY` is set.

## 6. Payer state (read-only, public RPC)

Expand Down Expand Up @@ -210,3 +212,37 @@ verification, paid 0.0002 pathUSD per chunk, and decrypted successfully.
| 3 | `hex-private-key` | Wallet key exposure → security review | [`0x23d1…dee2`](https://explore.testnet.tempo.xyz/tx/0x23d1cabe4dfb85956fcc050d3533d3be1fa2a70850723c2a5bf7217ef8b7dee2) |

All three: 1 unit · 0.0002 pathUSD · total 0.0006 pathUSD on Tempo Moderato testnet.

## Staging run — mainnet-hardening branch (2026-07-14)

Full paid E2E on Tempo Moderato testnet, run against `feat/mainnet-hardening` (PR #3)
with the real Phala enclave (`mode=tdx-live`, `ready:true`) and — for the first time —
a **payee settlement key configured**.

```
detect → pre-pay DCAP verify ✓ → encrypt → 402 → MPP session →
1 chunk streamed & charged (0.0002 pathUSD) → post-pay receipt ✓ → decrypt ✓
```

**First on-chain cooperative close.** With `TEMPO_RECIPIENT_PRIVATE_KEY` set, the server
settled the channel as the payee instead of leaving the deposit to timeout-reclaim:

```
payee (throwaway) 0x8e790205001a263df321d84c4b1c80daa1e32dab
nonce 0 → 1 (exactly one outgoing tx: the close)
pathUSD balanceOf 1,000,000 → 999,999.999611
= +0.0002 settled revenue − ~0.000589 close-tx fee
```

Anyone can verify: the address above has a single outgoing transaction on
https://explore.testnet.tempo.xyz — the cooperative close.

> ⚠️ **Economics finding:** at `PRICE_PER_UNIT=0.0002` and `CHUNK_COUNT=1`, the close fee
> (~0.0006) exceeds per-session revenue — every 1-chunk session settles at a net LOSS
> (≈ −0.0004). Before mainnet: raise the price, meter more chunks per session, or batch
> settlement (mppx `settlementSchedule`). Tracked in the README TODO.

Also verified in the same session (see PR #3 for the full matrix): mainnet boot guards
fail closed on demo recipient / placeholder or short `MPP_SECRET_KEY` / payee-key mismatch;
unknown `NETWORK` values crash the boot instead of silently running testnet; `NETWORK=mainnet`
serves chainId 4217 / USDC.e consistently on `/`, `/llms.txt`, `/SKILL.md`, `/openapi.json`.
10 changes: 5 additions & 5 deletions mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
// • private_inference(prompt, model?) — verify → encrypt → pay-per-chunk → decrypt
//
// Wire into an MCP client (e.g. Claude) as a stdio server, with AGENT_PRIVATE_KEY (a
// funded Tempo testnet wallet) in the environment:
// funded Tempo wallet on the server's network) in the environment:
// { "command": "npx", "args": ["tsx", "/abs/path/tempRouter/mcp/server.ts"] }

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import { TempRouter, detectSensitive, formatReport, AttestationError } from '../sdk/src/index.js'
import { config } from '../src/config.js'
import { config, tempoChain, isMainnet } from '../src/config.js'

const ok = (t: string) => ({ content: [{ type: 'text' as const, text: t }] })
const err = (t: string) => ({ content: [{ type: 'text' as const, text: t }], isError: true })
Expand All @@ -36,7 +36,7 @@ function makeClient(serverUrl?: string) {
})
}

const NO_WALLET = 'AGENT_PRIVATE_KEY not set — fund a Tempo testnet wallet and set it in this MCP server\'s environment.'
const NO_WALLET = `AGENT_PRIVATE_KEY not set — fund a Tempo ${isMainnet ? 'mainnet wallet (real USDC.e)' : 'testnet wallet'} and set it in this MCP server's environment.`

const server = new McpServer({ name: 'temprouter', version: '0.1.0' })

Expand Down Expand Up @@ -77,7 +77,7 @@ server.registerTool(
{
title: 'Confidential inference (verify → encrypt → pay → decrypt)',
description:
'Run a prompt through tempRouter\'s confidential lane: verify a real Phala Intel TDX enclave with Intel DCAP, encrypt the prompt to the enclave key, pay per response-chunk in pathUSD on Tempo, and decrypt the answer locally. A failed attestation pays NOTHING. Use for any prompt containing secrets, credentials, or PII. The enclave runs an OSS model (gpt-oss:20b) — use it for confidential payloads, not as a frontier-model replacement.',
'Run a prompt through tempRouter\'s confidential lane: verify a real Phala Intel TDX enclave with Intel DCAP, encrypt the prompt to the enclave key, pay per response-chunk in the network stablecoin (pathUSD testnet / USDC.e mainnet) on Tempo, and decrypt the answer locally. A failed attestation pays NOTHING. Use for any prompt containing secrets, credentials, or PII. The enclave runs an OSS model (gpt-oss:20b) — use it for confidential payloads, not as a frontier-model replacement.',
inputSchema: {
prompt: z.string().describe('The (possibly sensitive) prompt to run privately.'),
model: z.string().optional().describe('Model id (default nosana:gpt-oss:20b).'),
Expand All @@ -88,7 +88,7 @@ server.registerTool(
if (!config.agentPrivateKey) return err(NO_WALLET)
try {
const res = await makeClient(server).infer(prompt, { model })
const footer = `\n\n— verified TDX enclave · ${res.units} chunk(s) · ${res.paid} pathUSD${res.attestation.postPay?.ok ? ' · receipt ✓' : ''}`
const footer = `\n\n— verified TDX enclave · ${res.units} chunk(s) · ${res.paid} ${tempoChain.currencyName}${res.attestation.postPay?.ok ? ' · receipt ✓' : ''}`
return ok(res.answer + footer)
} catch (e) {
if (e instanceof AttestationError)
Expand Down
Loading
Loading