Introduction
NoVault is a non-custodial coordination protocol for autonomous agent workflows. It provides a single layer that handles capability discovery, provider selection, payment settlement, and execution verification — so agents can call paid services without holding keys or trusting an operator.
This documentation covers the full developer surface: the TypeScript and Python SDKs, the REST API, the provider schema format, the x402 payment protocol, on-chain escrow contracts, and the ZK verification roadmap.
Architecture at a glance
Every NoVault interaction passes through four cooperating layers. Each layer has a narrow responsibility and communicates through machine-readable schemas.
Agent declares what it needs in a structured intent object. No provider coupling, no SDK lock-in.
Schema registry matches the intent to eligible providers and returns a ranked candidate list.
x402 escrow opens, the request executes, and the escrow collapses atomically. No custody.
A signed receipt attests to every outcome. Disputes trigger deterministic re-execution or ZK proof.
Quick Start
Install the SDK, configure your API key, and execute your first paid agent request in a few lines of code.
Install the SDK
# npm
npm install @novault/sdk
# pnpm
pnpm add @novault/sdk
# yarn
yarn add @novault/sdkConfigure the client
import { NoVault } from "@novault/sdk";
export const nv = new NoVault({
apiKey: process.env.NOVAULT_API_KEY!,
// Optional: pin to testnet during development
environment: "sandbox",
});Execute your first intent
import { nv } from "./novault";
const result = await nv.intent.execute({
capability: "image/upscale",
input: {
url: "https://example.com/photo.png",
scale: 4,
},
budget: {
maxUsd: "0.05",
},
});
console.log(result.output.url); // → upscaled image URL
console.log(result.receipt.hash); // → cryptographic execution receiptintent.execute() call.Verify the receipt
Every response includes a signed receipt. You can verify it independently of the NoVault API at any time.
import { verifyReceipt } from "@novault/sdk";
const isValid = await verifyReceipt(result.receipt);
// → trueAuthentication
All API requests authenticate with a bearer token. Agent-side signing uses ed25519 keypairs for intent co-signing; the API key governs dashboard and management access.
API keys
API keys are scoped to a single environment (sandbox or production). Prefix nv_live_ indicates production; nv_test_ indicates sandbox. Never commit keys to source control.
curl https://api.novault.xyz/v1/intents \
-H "Authorization: Bearer nv_live_sk_…" \
-H "Content-Type: application/json"Agent signing keys
Intent co-signing uses a separate ed25519 keypair generated locally by the SDK. The private key never leaves the agent runtime — the API sees only the signed intent payload and the agent's public key DID.
import { generateAgentIdentity } from "@novault/sdk";
// Run once; persist the identity securely
const identity = await generateAgentIdentity();
// { did: "did:novault:0x…", privateKey: "…", publicKey: "…" }
const nv = new NoVault({
apiKey: process.env.NOVAULT_API_KEY!,
agentIdentity: identity,
});Scoped API keys
Fine-grained API keys can be restricted by IP CIDR, capability class, maximum per-request spend, and TTL. Create them via the dashboard or the Management API.
| Parameter | Type | Required | Description |
|---|---|---|---|
| capabilities | string[] | optional | Restrict to specific capability namespaces, e.g. ["image/*", "data/price"] |
| maxRequestUsd | string | optional | Hard cap per single request. Requests exceeding this are rejected before escrow opens. |
| ipCidrs | string[] | optional | Allowlist of IPv4/IPv6 CIDR blocks. Empty = unrestricted. |
| expiresAt | ISO 8601 | optional | Key expiry timestamp. After expiry all requests with this key return 401. |
Environments
NoVault runs two isolated environments. Sandbox uses simulated settlement with no real funds; production settles on-chain.
| Sandbox | Production | |
|---|---|---|
| Base URL | https://api.sandbox.novault.xyz/v1 | https://api.novault.xyz/v1 |
| Key prefix | nv_test_ | nv_live_ |
| Settlement | Simulated | On-chain (USDC) |
| Slashing | No | Yes |
| Rate limit | 100 req/min | 1 000 req/min |
| SLA | Best-effort | 99.9% uptime |
Intent Declaration
An intent is the fundamental unit of interaction with NoVault. It is a signed, structured object that describes what an agent needs — not which provider to call or how the request should be routed. This separation is the core design principle of the protocol.
Intent anatomy
{
"capability": "image/upscale", // namespaced capability identifier
"version": "^1.0", // semver range accepted
"input": { // capability-specific input
"url": "https://…/photo.png",
"scale": 4
},
"budget": {
"maxUsd": "0.10", // hard maximum for this request
"preferLowestPrice": true // resolution hint
},
"policy": {
"ttlMs": 8000, // abort if not settled within 8 s
"retries": 2, // retry on provider failure
"requireVerification": true // reject unverified receipts
},
"agentDid": "did:novault:0xabc…", // agent's public identity
"nonce": "f3c9…", // prevents replay
"signature": "ed25519:0x7e3f…" // agent co-signature over intent hash
}Capability namespaces
Capabilities follow a two-part namespace: category/action. Wildcard matching is used in key scoping but not in intent declarations — every intent must name an exact capability.
image/upscale— Image upscalingimage/generate— Text-to-image generationdata/price— Real-time asset price feeddata/risk-score— On-chain risk assessmentllm/complete— LLM text completionllm/embed— Text embeddingcompute/render— GPU render job
x- prefix is reserved for experimental or private capabilities.Capability Resolution
When an intent arrives, the Resolution Layer queries the Schema Registry for providers that satisfy the capability, version range, and input contract. It ranks candidates using a scoring function and returns an ordered list.
Scoring function
Candidates are scored by a weighted combination of four observable signals. Weights can be overridden per-intent via the budget hints object.
Lower price scores higher. Normalised across all candidates in the same capability class.
Rolling 30-day p99 latency vs declared SLA. Providers missing their SLA by >20% are excluded.
Rolling 7-day availability. Providers below 95% uptime are excluded regardless of other scores.
Semantic match between the intent input schema and the provider capability schema.
Resolution hints
Add hints to the budget object to override default weighting for a specific request.
budget: {
maxUsd: "0.10",
preferLowestPrice: true, // weight price 80%
requireSlaCompliant: true, // exclude any provider with SLA miss in last 7 days
pinProvider: "did:novault:0x…" // bypass resolution, pin to specific provider
}Request Settlement
NoVault settles every service call atomically at the request level. There are no monthly invoices, no batched reconciliation, and no credit risk. The full payment lifecycle for a single request is:
Intent lock
Agent co-signs an intent object. No funds move. The lock establishes the agreed provider, maximum price, and TTL.
Escrow open
An on-chain escrow contract receives the agreed payment. Neither the agent nor the provider controls it unilaterally.
Execution
Provider executes the request and returns output alongside a signed execution receipt.
Receipt validation
The validation layer checks the receipt's signature, output hash, and latency.
Settlement
If valid: escrow releases payment to provider. If invalid or timed out: agent is refunded; provider stake is slashed proportionally.
x402 payment header
Settlement uses the x402 open standard. The escrow authorization travels as an HTTP header with the request — no separate payment round-trip is needed.
X-Payment: {
"version": "1",
"scheme": "exact",
"network": "base-mainnet",
"maxAmountRequired": "40000", // in USDC microunits (6 decimals)
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xEscrowContract…",
"extra": {
"intentHash": "sha256:3f4a…",
"agentSignature": "ed25519:0x…"
}
}Execution Verification
Every execution receipt is a cryptographically signed attestation that binds the provider identity, request hash, output hash, latency, and timestamp into a single unforgeable object.
{
"receiptVersion": "1",
"intentHash": "sha256:3f4a…",
"outputHash": "sha256:9c1b…",
"provider": "did:novault:0xabcd…",
"capabilityId": "image/upscale",
"executedAt": 1753996800,
"settledAt": 1753996802,
"latencyMs": 1240,
"amountPaidUsd": "0.0040",
"verificationMethod": "deterministic-reexecution",
"verifierNode": "did:novault:0xverif…",
"signature": "ed25519:0x7e3f…"
}Verification methods
- Deterministic re-execution — for deterministic capabilities (data feeds, transforms, validations): a verifier node reruns the request and compares output hashes.
- Merkle proof — for capabilities with large outputs: a Merkle root of output chunks replaces the full output hash, verified by spot-checking.
- ZK proof (roadmap) — for non-deterministic capabilities (LLM inference, generative models): ZK proof of correct execution attached to the receipt.
@novault/sdk verifyReceipt() function or directly against the smart contract.Receipts & Evidence
Receipts are the accountability primitive of the NoVault protocol. They are returned synchronously with every successful response and stored immutably.
Accessing receipts in the SDK
const result = await nv.intent.execute({ … });
// Receipt is always present on success
const { receipt } = result;
console.log(receipt.hash); // content-addressed receipt ID
console.log(receipt.amountPaidUsd); // exact amount settled
console.log(receipt.latencyMs); // provider execution latency
console.log(receipt.verifierNode); // DID of verifier that checked the output
// Independently verify the receipt at any time
const valid = await nv.receipts.verify(receipt.hash);Receipt retention
Receipts are permanently stored on IPFS and indexed by the NoVault API for 90 days with fast retrieval. After 90 days, retrieval falls back to IPFS CID resolution. On-chain settlement events are permanently accessible on-chain.
Querying receipts
// List receipts for an agent
const receipts = await nv.receipts.list({
agentDid: "did:novault:0xabc…",
capability: "image/upscale",
from: "2026-01-01",
to: "2026-08-01",
limit: 50,
});
// Fetch a single receipt
const receipt = await nv.receipts.get("sha256:9c1b…");Making Your First Request
This guide walks through a complete end-to-end request using the TypeScript SDK, from client setup to receipt verification.
import { NoVault, generateAgentIdentity, verifyReceipt } from "@novault/sdk";
// 1. Generate a persistent agent identity (do this once, store it securely)
const identity = await generateAgentIdentity();
// 2. Initialise the client
const nv = new NoVault({
apiKey: process.env.NOVAULT_API_KEY!,
agentIdentity: identity,
environment: "sandbox", // switch to "production" when ready
});
// 3. Execute an intent
const result = await nv.intent.execute({
capability: "data/price",
input: {
asset: "ETH",
currency: "USD",
sources: ["chainlink", "pyth"],
aggregation: "median",
},
budget: {
maxUsd: "0.002",
},
policy: {
ttlMs: 5000,
requireVerification: true,
},
});
// 4. Use the output
console.log("ETH price:", result.output.price, result.output.currency);
// 5. Verify the receipt
const isValid = await verifyReceipt(result.receipt);
console.log("Receipt valid:", isValid);
console.log("Paid:", result.receipt.amountPaidUsd, "USD");environment: "sandbox" during development. Sandbox requests use simulated settlement — no real funds are moved.Budget Enforcement
Budget limits are enforced at the protocol level — before escrow opens. An agent cannot overspend; it can only under-spend. Limits compose: per-request, per-session, and capability-class limits all apply simultaneously.
Per-request budget
budget: {
maxUsd: "0.10", // hard cap — request rejected if all providers cost more
preferLowestPrice: true, // resolution hint: weight price heavily
}Session-level budget
const session = await nv.sessions.create({
budget: {
totalMaxUsd: "5.00", // session hard cap
capabilityLimits: {
"image/*": "2.00", // wildcard cap for all image capabilities
"llm/complete": "1.50",
},
},
ttlSeconds: 3600, // session expires after 1 hour
});
// All intents executed within this session share the budget pool
const result = await nv.intent.execute(
{ capability: "image/upscale", … },
{ sessionId: session.id }
);Account-level policy
Account-level policies are set in the dashboard and apply to all API keys in the account. They cannot be exceeded by per-request or session budgets.
| Parameter | Type | Required | Description |
|---|---|---|---|
| dailyMaxUsd | string | optional | Maximum total spend per calendar day (UTC). Resets at 00:00 UTC. |
| monthlyMaxUsd | string | optional | Maximum total spend per calendar month. |
| maxRequestUsd | string | optional | Hard ceiling on any single request, regardless of intent budget. |
| allowedCapabilities | string[] | optional | Capability namespaces this account may access. Wildcard matching supported. |
| blockedProviders | string[] | optional | Provider DIDs that will never be selected for this account. |
Registering as a Provider
Any entity can register as a NoVault provider by completing three steps: identity creation, collateral bonding, and schema publication.
Step 1 — Create a provider identity
import { createProviderIdentity } from "@novault/sdk/provider";
const provider = await createProviderIdentity({
name: "Acme Upscaling Inc.",
contact: "ops@acme.example",
website: "https://acme.example",
});
// → { did: "did:novault:0x…", privateKey: "…", publicKey: "…" }Step 2 — Bond collateral
Providers must bond USDC collateral proportional to their declared throughput. Collateral is slashed on failed receipts.
await nv.provider.bondCollateral({
providerDid: provider.did,
amountUsd: "500.00", // minimum: $100 for <100 req/min; $1 000 for <1 000 req/min
});Step 3 — Publish your schema
See Publishing a Schema for the full schema format.
Publishing a Schema
A capability schema is the machine-readable contract that agents use to decide whether a provider can satisfy their intent. It must be accurate — discrepancies between the declared schema and actual behavior are grounds for slashing.
{
"capability": "image/upscale",
"version": "1.3.0",
"provider": "did:novault:0xabcd…",
"input": {
"type": "object",
"properties": {
"url": { "type": "string", "format": "uri" },
"scale": { "type": "integer", "enum": [2, 4, 8] }
},
"required": ["url", "scale"]
},
"output": {
"type": "object",
"properties": {
"url": { "type": "string", "format": "uri" },
"width": { "type": "integer" },
"height": { "type": "integer" }
}
},
"pricing": {
"model": "per-request",
"unit": "megapixel",
"rate": "0.0004",
"currency": "USDC"
},
"sla": {
"p99LatencyMs": 3000,
"uptimePct": 99.5,
"maxConcurrentRequests": 200
},
"settlement": {
"method": "x402",
"payTo": "0xYourWalletAddress…",
"network": "base-mainnet"
}
}Publishing via SDK
import schema from "./capability-schema.json";
await nv.provider.publishSchema({
providerDid: provider.did,
schema,
// SDK validates the schema locally before submitting
});
// → { schemaId: "sha256:…", status: "pending_review" }
// Schemas enter the registry after a 1-hour automated validation pass.Multi-provider Fallback
Configure automatic fallback so that if the top-ranked provider fails, the intent is retried with the next eligible candidate — without any code changes on your side.
const result = await nv.intent.execute({
capability: "llm/complete",
input: { prompt: "Summarise this contract: …", maxTokens: 512 },
budget: { maxUsd: "0.05" },
policy: {
ttlMs: 12000,
retries: 3, // try up to 3 additional providers on failure
retryOnCodes: [ // only retry on these provider error codes
"PROVIDER_TIMEOUT",
"PROVIDER_RATE_LIMITED",
"PROVIDER_UNAVAILABLE",
],
},
});
// The receipt tells you which provider ultimately settled the request
console.log(result.receipt.provider);
console.log(result.metadata.providerAttempts); // → 2 (if first failed)Webhooks & Events
Subscribe to settlement and lifecycle events via HTTPS webhooks. Events fire for both synchronous and asynchronous intents.
Registering a webhook
await nv.webhooks.create({
url: "https://your-server.example/novault/events",
events: [
"intent.settled",
"intent.failed",
"receipt.verified",
"receipt.disputed",
"provider.slashed",
],
secret: process.env.WEBHOOK_SECRET!,
});Verifying webhook signatures
import { verifyWebhookSignature } from "@novault/sdk";
// In your express handler (or equivalent):
app.post("/novault/events", (req, res) => {
const isValid = verifyWebhookSignature({
payload: req.rawBody, // must be the raw buffer, not parsed JSON
signature: req.headers["x-novault-signature"] as string,
secret: process.env.WEBHOOK_SECRET!,
});
if (!isValid) return res.status(401).send("Invalid signature");
const event = req.body;
if (event.type === "intent.settled") {
console.log("Settled:", event.data.intentHash, event.data.amountPaidUsd);
}
res.status(200).send("ok");
});| Parameter | Type | Required | Description |
|---|---|---|---|
| intent.settled | event | optional | Fires when an intent's escrow releases successfully to the provider. |
| intent.failed | event | optional | Fires when all retry attempts for an intent are exhausted. |
| intent.refunded | event | optional | Fires when a receipt fails validation and the agent is refunded. |
| receipt.verified | event | optional | Fires when a verifier node confirms a receipt is valid. |
| receipt.disputed | event | optional | Fires when a receipt verification fails and a dispute is opened. |
| provider.slashed | event | optional | Fires when a provider's collateral is slashed due to a failed dispute. |
Intents API
/v1/intentsExecute an intentResolve a capability, open escrow, execute the request, and return settled output and a receipt.
| Parameter | Type | Required | Description |
|---|---|---|---|
| capability | string | required | Namespaced capability identifier, e.g. image/upscale |
| version | string | optional | Semver range of acceptable capability schema versions. Defaults to latest stable. |
| input | object | required | Capability-specific input payload. Shape validated against the resolved provider's schema. |
| budget | object | optional | maxUsd, preferLowestPrice, requireSlaCompliant, pinProvider. |
| policy | object | optional | ttlMs, retries, retryOnCodes, requireVerification. |
| sessionId | string | optional | Associate this request with a session budget pool. |
| agentDid | string | optional | Agent DID for co-signing. Required if agentIdentity is not set on the client. |
| nonce | string | optional | Idempotency nonce. Duplicate nonces within 24 h return the original result without re-execution. |
/v1/intents/:intentHashGet intent statusReturns current lifecycle status for an intent. Useful for async intents that execute in the background.
/v1/intentsList intents| Parameter | Type | Required | Description |
|---|---|---|---|
| capability | string | optional | Filter by capability. |
| status | string | optional | pending | executing | settled | failed | refunded |
| from | ISO 8601 | optional | Start of date range (inclusive). |
| to | ISO 8601 | optional | End of date range (inclusive). |
| limit | integer | optional | Max records per page. Default 20, max 100. |
| cursor | string | optional | Pagination cursor from previous response. |
Registry API
The Schema Registry is a permissionless index of provider capability schemas.
/v1/registry/capabilitiesList all registered capabilitiescurl "https://api.novault.xyz/v1/registry/capabilities?category=image" \
-H "Authorization: Bearer nv_live_…"/v1/registry/capabilities/:capability/providersList providers for a capabilityReturns ranked provider list for a given capability. Useful for inspection without executing an intent.
/v1/registry/schemasPublish a new capability schema| Parameter | Type | Required | Description |
|---|---|---|---|
| providerDid | string | required | DID of the publishing provider. Must match the signing key. |
| schema | object | required | Full capability schema object. See Publishing a Schema. |
| replaces | string | optional | Schema ID this version supersedes. Old version is deprecated, not deleted. |
/v1/registry/schemas/:schemaIdDeprecate a schemaMarks a schema as deprecated. In-flight intents using the schema complete normally; new resolutions exclude it.
Settlement API
/v1/settlement/escrows/:intentHashGet escrow stateReturns on-chain escrow state for an intent. Useful for debugging failed settlements.
{
"intentHash": "sha256:3f4a…",
"state": "released", // pending | open | released | refunded | slashed
"amount": "40000", // USDC microunits
"openedAt": 1753996798,
"settledAt": 1753996802,
"provider": "did:novault:0xabcd…",
"txHash": "0xchain…"
}/v1/settlement/disputeOpen a disputeOpens a formal dispute against a receipt. Triggers a verifier node to re-execute the request. Disputes must be filed within 24 hours of settlement.
| Parameter | Type | Required | Description |
|---|---|---|---|
| intentHash | string | required | Intent hash of the disputed settlement. |
| reason | string | required | output_mismatch | latency_exceeded | provider_unavailable | other |
| evidence | object | optional | Optional supporting evidence (output diff, timing logs). |
Receipts API
/v1/receipts/:receiptHashFetch a receipt/v1/receiptsList receipts| Parameter | Type | Required | Description |
|---|---|---|---|
| agentDid | string | optional | Filter by agent DID. |
| provider | string | optional | Filter by provider DID. |
| capability | string | optional | Filter by capability. |
| verificationMethod | string | optional | deterministic-reexecution | merkle-proof | zk-proof |
| from / to | ISO 8601 | optional | Date range filter. |
| limit / cursor | integer / string | optional | Pagination. |
/v1/receipts/:receiptHash/verifyTrigger re-verificationManually request a verifier node to re-verify an existing receipt. Returns the verification result and the verifier's signature.
Errors & Status Codes
All errors follow a consistent envelope. The code field is stable across versions; the message field is human-readable and may change.
{
"error": {
"code": "INTENT_BUDGET_EXCEEDED",
"message": "No eligible provider found within the declared maxUsd budget of $0.01.",
"intentHash": "sha256:3f4a…",
"docs": "https://docs.novault.xyz/errors/INTENT_BUDGET_EXCEEDED"
}
}| Code | HTTP | Description |
|---|---|---|
| AUTH_INVALID_KEY | 401 | API key not found or revoked. |
| AUTH_KEY_EXPIRED | 401 | API key has passed its expiresAt date. |
| INTENT_INVALID_SCHEMA | 422 | Input payload failed schema validation against the resolved provider. |
| INTENT_BUDGET_EXCEEDED | 422 | No eligible provider exists within the budget constraint. |
| INTENT_TTL_EXPIRED | 408 | Execution did not complete within the declared ttlMs. |
| INTENT_DUPLICATE_NONCE | 200 | Nonce was already used within 24 h. Original result returned. |
| PROVIDER_UNAVAILABLE | 503 | Resolved provider returned an error or timed out. Retry with fallback. |
| SETTLEMENT_ESCROW_FAILED | 502 | On-chain escrow transaction reverted. Funds not moved. |
| RECEIPT_VERIFICATION_FAILED | 200 | Receipt signature invalid. Refund initiated; provider dispute opened. |
| RATE_LIMITED | 429 | Request rate exceeds your plan limit. Check Retry-After header. |
TypeScript / JavaScript SDK
The official TypeScript SDK is a zero-dependency library that provides a fully typed wrapper over the NoVault REST API, with built-in intent signing, receipt verification, and streaming support.
Installation
npm install @novault/sdk
# peer dep for streaming intents (optional)
npm install eventsourceClient options
| Parameter | Type | Required | Description |
|---|---|---|---|
| apiKey | string | required | Your NoVault API key. |
| agentIdentity | AgentIdentity | optional | ed25519 keypair for intent co-signing. Generate with generateAgentIdentity(). |
| environment | "sandbox" | "production" | optional | Defaults to production. |
| timeout | number | optional | Request timeout in milliseconds. Default 30 000. |
| maxRetries | number | optional | Client-level retry count for network errors (not provider failures). Default 2. |
| fetch | function | optional | Custom fetch implementation. Defaults to global fetch. |
Streaming intents
For capabilities that support streaming output (e.g. llm/complete), use the streaming API:
const stream = await nv.intent.stream({
capability: "llm/complete",
input: { prompt: "Explain non-custodial settlement in one paragraph.", maxTokens: 200 },
budget: { maxUsd: "0.02" },
});
for await (const chunk of stream) {
process.stdout.write(chunk.delta);
}
const receipt = await stream.receipt(); // available after stream ends
console.log("Paid:", receipt.amountPaidUsd);Python SDK
pip install novaultBasic usage
import asyncio
from novault import NoVault, generate_agent_identity
async def main():
identity = await generate_agent_identity()
nv = NoVault(
api_key="nv_live_sk_…",
agent_identity=identity,
environment="sandbox",
)
result = await nv.intent.execute(
capability="data/price",
input={"asset": "BTC", "currency": "USD", "aggregation": "median"},
budget={"max_usd": "0.001"},
)
print(f"BTC price: {result.output['price']} {result.output['currency']}")
print(f"Receipt: {result.receipt.hash}")
print(f"Paid: {result.receipt.amount_paid_usd} USD")
asyncio.run(main())Sync client
from novault.sync import NoVaultSync
nv = NoVaultSync(api_key="nv_live_sk_…")
result = nv.intent.execute(capability="data/price", input={…}, budget={…})REST (cURL)
The REST API is the canonical interface. All SDKs are thin wrappers over it. Use cURL or any HTTP client for languages without an official SDK.
Execute an intent
curl -X POST https://api.novault.xyz/v1/intents \
-H "Authorization: Bearer nv_live_sk_…" \
-H "Content-Type: application/json" \
-d '{
"capability": "data/price",
"input": { "asset": "ETH", "currency": "USD", "aggregation": "median" },
"budget": { "maxUsd": "0.001" },
"policy": { "ttlMs": 5000, "requireVerification": true }
}'Response
{
"intentHash": "sha256:3f4a…",
"status": "settled",
"output": {
"price": "3421.87",
"currency": "USD",
"timestamp": 1753996800
},
"receipt": {
"hash": "sha256:9c1b…",
"provider": "did:novault:0xabcd…",
"amountPaidUsd": "0.0004",
"latencyMs": 312,
"verificationMethod": "deterministic-reexecution",
"signature": "ed25519:0x7e3f…"
}
}x402 Payment Standard
x402 is an open HTTP payment standard that embeds payment authorization directly in the request/response cycle. NoVault uses x402 for all settlement operations.
The standard defines two new HTTP headers: X-Payment (client → server, carries payment authorization) and X-Payment-Response (server → client, carries settlement receipt).
Flow
HTTP/1.1 402 Payment Required
X-Payment-Required: {
"version": "1",
"scheme": "exact",
"network": "base-mainnet",
"maxAmountRequired": "40000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xEscrowContract…",
"extra": { "capability": "image/upscale" }
}POST /execute HTTP/1.1
X-Payment: {
"version": "1",
"scheme": "exact",
"network": "base-mainnet",
"payload": "0x…", // signed EIP-712 authorization
"extra": {
"intentHash": "sha256:3f4a…",
"agentSignature": "ed25519:0x…"
}
}HTTP/1.1 200 OK
X-Payment-Response: {
"success": true,
"txHash": "0xchain…",
"receipt": { … }
}Escrow Contracts
NoVault's escrow contracts are deployed on Base mainnet (and Base Sepolia for sandbox). They are immutable, audited, and governed entirely by protocol rules — no operator can drain them unilaterally.
Contract addresses
| Contract | Network | Address |
|---|---|---|
| IntentEscrow | Base Mainnet | 0x1a2b3c4d… |
| ProviderRegistry | Base Mainnet | 0x5e6f7a8b… |
| SlashingController | Base Mainnet | 0x9c0d1e2f… |
| IntentEscrow | Base Sepolia | 0xaabbccdd… |
Security properties
- Funds cannot be withdrawn until a valid execution receipt is verified or the TTL expires.
- The agent co-signature is required to open escrow — the API key alone is insufficient.
- Re-entrancy protection and integer-overflow checks throughout.
- Audited by Trail of Bits and Spearbit (reports linked in the GitHub repo).
Slashing & Disputes
Slashing is the mechanism that makes provider misbehavior economically costly. Collateral is seized when a provider's execution receipt fails verification.
Slashing schedule
| Offense | Slash amount | Suspension |
|---|---|---|
| Output hash mismatch | 5× request value | None (1st offense) |
| Output hash mismatch (repeated) | 20× request value | 24 h suspension |
| SLA latency exceeded (>2×) | 1× request value | None |
| SLA latency exceeded (>5×) | 5× request value | 1 h suspension |
| Schema mismatch (input rejected) | 1× request value | None |
| Malicious output (manual review) | 100% collateral | Permanent ban |
Dispute resolution timeline
- T+0 — Receipt fails verification. Refund is initiated immediately.
- T+1 h — Verifier node completes re-execution. Dispute outcome recorded on-chain.
- T+24 h — Provider may challenge the outcome via governance vote (requires 5% of bonded collateral as challenge stake).
- T+7 d — Governance challenge period ends. Slashing executed if no successful challenge.
ZK Verification Roadmap
Deterministic re-execution works well for data feeds, transforms, and structured queries. It cannot verify non-deterministic capabilities like LLM inference or generative image models, where two correct executions of the same input may produce different (but equally valid) outputs.
The NoVault receipt format is forward-compatible with ZK attestation. The verificationMethod field is designed to accept a "zk-proof" value, and the receipt schema includes reserved fields for the proof and verification key.
Roadmap phases
Deterministic re-execution
Live on mainnet. Covers all deterministic capability classes.
Merkle-proof verification
Large-output capabilities (compute/render, data/bulk). Q3 2026.
ZK proof of LLM inference
Partnered with Boundless (RISC Zero) for SP1 proof generation. Mainnet target Q1 2027.
ZK proof of generative models
Active research. Requires hardware-accelerated ZK proving for GPU workloads.
Simple Agent Request
A complete example: an autonomous agent that fetches an ETH price, upscales a chart image, and logs both receipts.
import { NoVault, generateAgentIdentity } from "@novault/sdk";
const identity = await generateAgentIdentity();
const nv = new NoVault({ apiKey: process.env.NOVAULT_API_KEY!, agentIdentity: identity });
// ── Step 1: fetch ETH price ────────────────────────────────────────────────
const priceResult = await nv.intent.execute({
capability: "data/price",
input: { asset: "ETH", currency: "USD", aggregation: "median" },
budget: { maxUsd: "0.001" },
});
const ethPrice = priceResult.output.price;
console.log("ETH/USD:", ethPrice);
// ── Step 2: upscale a chart image ─────────────────────────────────────────
const imageResult = await nv.intent.execute({
capability: "image/upscale",
input: { url: `https://charts.example.com/eth?price=${ethPrice}`, scale: 4 },
budget: { maxUsd: "0.01" },
});
console.log("Upscaled chart:", imageResult.output.url);
// ── Step 3: log receipts ──────────────────────────────────────────────────
for (const result of [priceResult, imageResult]) {
console.log({
capability: result.receipt.capabilityId,
provider: result.receipt.provider,
paid: result.receipt.amountPaidUsd,
latency: result.receipt.latencyMs + "ms",
});
}Multi-step Workflow
Run multiple intents concurrently within a shared session budget, with automatic fallback on provider failure.
import { NoVault, generateAgentIdentity } from "@novault/sdk";
const nv = new NoVault({ apiKey: process.env.NOVAULT_API_KEY!, agentIdentity: await generateAgentIdentity() });
// Create a session with a $1 total budget
const session = await nv.sessions.create({
budget: { totalMaxUsd: "1.00" },
ttlSeconds: 300,
});
const opts = { sessionId: session.id, policy: { retries: 2, ttlMs: 10_000 } };
// ── Concurrent data fetch ─────────────────────────────────────────────────
const [eth, btc, sol] = await Promise.all([
nv.intent.execute({ capability: "data/price", input: { asset: "ETH", currency: "USD", aggregation: "median" }, budget: { maxUsd: "0.001" }, ...opts }),
nv.intent.execute({ capability: "data/price", input: { asset: "BTC", currency: "USD", aggregation: "median" }, budget: { maxUsd: "0.001" }, ...opts }),
nv.intent.execute({ capability: "data/price", input: { asset: "SOL", currency: "USD", aggregation: "median" }, budget: { maxUsd: "0.001" }, ...opts }),
]);
console.log("Prices:", eth.output.price, btc.output.price, sol.output.price);
// ── Sequential analysis (depends on prices) ───────────────────────────────
const risk = await nv.intent.execute({
capability: "data/risk-score",
input: { portfolio: { ETH: 0.6, BTC: 0.3, SOL: 0.1 }, prices: { ETH: eth.output.price, BTC: btc.output.price, SOL: sol.output.price } },
budget: { maxUsd: "0.02" },
...opts,
});
console.log("Portfolio risk score:", risk.output.score, "/100");
console.log("Session total spent:", (await nv.sessions.get(session.id)).totalSpentUsd);Full Provider Setup
End-to-end provider registration: identity, collateral, schema, and a minimal execution server.
import { createProviderIdentity, NoVault } from "@novault/sdk/provider";
import { serve } from "@hono/node-server";
import { Hono } from "hono";
// 1. Create provider identity (run once, store securely)
const identity = await createProviderIdentity({ name: "My Upscale Service" });
console.log("Provider DID:", identity.did);
// 2. Bond collateral ($200 covers up to 200 req/min throughput)
const nv = new NoVault({ apiKey: process.env.NOVAULT_PROVIDER_KEY!, providerIdentity: identity });
await nv.provider.bondCollateral({ amountUsd: "200.00" });
// 3. Publish schema
await nv.provider.publishSchema({
providerDid: identity.did,
schema: {
capability: "image/upscale",
version: "1.0.0",
input: { type: "object", properties: { url: { type: "string" }, scale: { type: "integer", enum: [2, 4] } }, required: ["url", "scale"] },
output: { type: "object", properties: { url: { type: "string" } } },
pricing: { model: "per-request", unit: "request", rate: "0.005", currency: "USDC" },
sla: { p99LatencyMs: 5000, uptimePct: 99.0 },
settlement: { method: "x402", payTo: "0xYourWallet…", network: "base-mainnet" },
},
});
// 4. Execution server (receives requests from NoVault after escrow opens)
const app = new Hono();
app.post("/execute", async (c) => {
const { input, intentHash } = await c.req.json();
// Verify the x402 payment header before doing any work
const isAuthorized = await nv.provider.verifyPaymentHeader(c.req.header("X-Payment")!, { intentHash });
if (!isAuthorized) return c.json({ error: "Unauthorized" }, 401);
// Do the actual work
const upscaled = await upscaleImage(input.url, input.scale);
// Return output + issue a signed receipt
const receipt = await nv.provider.issueReceipt({ intentHash, outputHash: await sha256(upscaled.url) });
return c.json({ output: upscaled, receipt }, 200, { "X-Payment-Response": JSON.stringify(receipt) });
});
serve({ fetch: app.fetch, port: 3000 });
console.log("Provider server running on :3000");