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
10 changes: 10 additions & 0 deletions examples/svs-verified-action/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Placeholder values only. Do not commit real API keys or signing secrets.
SVS_SERVER_URL=https://app.svsprotocol.com
SVS_BOT_ID=replace-with-dashboard-bot-id
SVS_BOT_POLICY_ID=memo-human-approved-v1
SVS_BOT_API_KEY=replace-with-dashboard-issued-api-key
SVS_BOT_REQUEST_SIGNING_SECRET=replace-with-dashboard-issued-request-signing-secret
SVS_BOT_EXPECTED_INTEGRATION_CONTRACT_HASH=replace-with-current-contract-hash
SOLANA_RPC_URL=https://api.devnet.solana.com
SVS_SERIALIZED_TRANSACTION_BASE64=replace-with-prepared-serialized-transaction
SVS_RUN_LIVE_SUBMIT=false
95 changes: 95 additions & 0 deletions examples/svs-verified-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# SVS Verified Action Example For Vercel AI SDK

This directory is a PR-ready example package for the Vercel AI SDK ecosystem.
It is intentionally small so a framework maintainer can review it without the
private SVS implementation details.

## What The Example Shows

The example adds one Vercel AI SDK compatible tool:

```text
svsVerifyAndSubmitSolanaAction
```

The tool is created from the public SVS package export:

```js
import { createSvsVercelAiSdkTools } from "@svsprotocol/solana/vercel-ai";
```

It routes a prepared Solana action through SVS so the action is not trusted
until SVS checks signed bot identity, production readiness, human wallet
approval, registry proof, and portable verification.

## Proposed Upstream Placement

Suggested destination in a Vercel AI SDK docs or examples PR:

```text
examples/svs-verified-action/
```

The package is safe to copy because it uses only public dependencies and
placeholder environment values.

## Local Review

From this directory:

```sh
npm install
npm run validate
```

## Protocol Or Wallet Gate

Protocols and wallets can reject unverified automation by requiring the hosted
SVS registry before accepting an agent action:

```js
import { createHostedVerifiedAgentRegistryMiddleware } from "@svsprotocol/solana/protocol";

const requireVerifiedAgent = createHostedVerifiedAgentRegistryMiddleware({
registryUrl: "https://registry.svsprotocol.com/registry.json",
expectedRegistryHash: "PINNED_REGISTRY_HASH",
trustPolicy: "high-trust"
});

await requireVerifiedAgent({ agent: { botId: "svs-demo-devnet-agent" } });
```

## Public References

- Docs: https://svsprotocol.com/docs
- Registry: https://registry.svsprotocol.com/registry.json
- Verified Agent Standard: https://svsprotocol.com/docs#verified-agent-standard
- Verifier: https://svsprotocol.com/verify

## Run Modes

The default run is import validation only. It does not require SVS credentials,
read your config, or submit an action:

```sh
node ./svs-verified-action.mjs
```

Run `npm run validate` to check the package files and placeholder env template
before opening the upstream PR.

Live submission is opt-in:

```sh
SVS_RUN_LIVE_SUBMIT=true node ./svs-verified-action.mjs
```

Live mode requires a real SVS dashboard, bot API key, request-signing secret,
controller wallet, current integration-contract hash, and a prepared serialized
transaction. Do not commit those values.

## Boundary

This package must contain no API keys, request-signing secrets, wallet
keypairs, local `data/` evidence, private dashboard source, admin endpoints, or
private operator artifacts.
15 changes: 15 additions & 0 deletions examples/svs-verified-action/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "svs-vercel-ai-verified-action-example",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Reviewable Vercel AI SDK example that routes Solana tool calls through SVS human approval and proof verification.",
"scripts": {
"validate": "node ./validate.mjs"
},
"dependencies": {
"@svsprotocol/solana": "^0.2.0",
"ai": "^6.0.202",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples/svs-verified-action package declares "ai": "^6.0.202" (and "zod": "^4.4.3") as registry dependencies, so pnpm resolves ai@6 from npm instead of linking the in-repo workspace SDK (v7.0.9), meaning the example never exercises the SDK in this monorepo and pins an outdated major.

Fix on Vercel

"zod": "^4.4.3"
}
}
155 changes: 155 additions & 0 deletions examples/svs-verified-action/svs-verified-action.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { realpathSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { tool } from "ai";
import { z } from "zod";
import {
VERCEL_AI_SDK_ADAPTER_VERSION,
VERCEL_AI_SDK_TOOL_NAME,
createSvsVercelAiSdkTools
} from "@svsprotocol/solana/vercel-ai";

const REQUIRED_ENV = [
"SVS_SERVER_URL",
"SVS_BOT_ID",
"SVS_BOT_POLICY_ID",
"SVS_BOT_API_KEY",
"SVS_BOT_REQUEST_SIGNING_SECRET",
"SVS_BOT_EXPECTED_INTEGRATION_CONTRACT_HASH",
"SOLANA_RPC_URL"
];

const LIVE_SUBMIT_REQUIRED_ENV = [
...REQUIRED_ENV,
"SVS_SERIALIZED_TRANSACTION_BASE64"
];

export function createSvsVerifiedVercelAiSdkTools(env = process.env) {
assertRequiredEnv(env);

return createSvsVercelAiSdkTools({
tool,
z,
baseUrl: env.SVS_SERVER_URL,
apiKey: env.SVS_BOT_API_KEY,
requestSigningSecret: env.SVS_BOT_REQUEST_SIGNING_SECRET,
expectedIntegrationContractHash: env.SVS_BOT_EXPECTED_INTEGRATION_CONTRACT_HASH,
botId: env.SVS_BOT_ID,
policyId: env.SVS_BOT_POLICY_ID,
rpcUrl: env.SOLANA_RPC_URL,
waitForProof: env.SVS_WAIT_FOR_PROOF === "true",
fetchProof: true,
checkReceiptRegistryChain: true
});
}

export async function requireSvsVerifiedVercelAiSdkReady(env = process.env) {
const svs = createSvsVerifiedVercelAiSdkTools(env);

await svs.requireSvsProductionReady({
requireNoExpiredPreviousSigningSecrets: true
});

return {
ok: true,
toolName: VERCEL_AI_SDK_TOOL_NAME
};
}

export async function submitSvsVerifiedVercelAiSdkAction({
requestId,
intent,
serializedTransaction,
simulation,
metadata = {}
}, env = process.env) {
const svs = createSvsVerifiedVercelAiSdkTools(env);

await svs.requireSvsProductionReady({
requireNoExpiredPreviousSigningSecrets: true
});

return svs.verifyAndSubmitSolanaAction({
requestId,
idempotencyKey: requestId,
intent,
serializedTransaction,
simulation,
metadata,
source: {
agentFramework: "vercel-ai-sdk",
adapter: VERCEL_AI_SDK_ADAPTER_VERSION,
example: "svs-verified-action"
}
});
}

export function getSvsVercelAiSdkExampleInfo() {
return {
ok: true,
runtime: "Vercel AI SDK",
hostPackageImported: typeof tool === "function",
adapterVersion: VERCEL_AI_SDK_ADAPTER_VERSION,
toolName: VERCEL_AI_SDK_TOOL_NAME,
packageExport: "@svsprotocol/solana/vercel-ai",
liveSubmitOptIn: "SVS_RUN_LIVE_SUBMIT=true"
};
}

function assertRequiredEnv(env) {
const missing = REQUIRED_ENV.filter((name) => !env[name]);

if (missing.length > 0) {
throw new Error(`Missing required SVS env values: ${missing.join(", ")}`);
}
}

function assertLiveSubmitEnv(env) {
const missing = LIVE_SUBMIT_REQUIRED_ENV.filter((name) => !env[name]);

if (missing.length > 0) {
throw new Error(`Missing required SVS live-submit env values: ${missing.join(", ")}`);
}
}

function isDirectRun(metaUrl, argvPath = process.argv[1]) {
if (!argvPath) {
return false;
}

try {
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argvPath);
} catch {
return fileURLToPath(metaUrl) === argvPath;
}
}

if (isDirectRun(import.meta.url)) {
const info = getSvsVercelAiSdkExampleInfo();

if (process.env.SVS_RUN_LIVE_SUBMIT !== "true") {
console.log(JSON.stringify({
...info,
status: "dry_run",
nextAction: "Set SVS_RUN_LIVE_SUBMIT=true only after filling real SVS credentials and a prepared transaction."
}, null, 2));
process.exit(0);
}

assertLiveSubmitEnv(process.env);

const result = await submitSvsVerifiedVercelAiSdkAction({
requestId: `svs-vercel-ai-${Date.now()}`,
intent: {
botId: process.env.SVS_BOT_ID,
type: "memo",
summary: "Submit a Vercel AI SDK Solana tool call through SVS human approval."
},
serializedTransaction: process.env.SVS_SERIALIZED_TRANSACTION_BASE64,
simulation: {
ok: true,
source: "provided-by-agent"
}
});

console.log(JSON.stringify(result, null, 2));
}
119 changes: 119 additions & 0 deletions examples/svs-verified-action/validate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";

const files = {
readme: await readFile(new URL("./README.md", import.meta.url), "utf8"),
packageJson: JSON.parse(await readFile(new URL("./package.json", import.meta.url), "utf8")),
envExample: await readFile(new URL("./.env.example", import.meta.url), "utf8"),
example: await readFile(new URL("./svs-verified-action.mjs", import.meta.url), "utf8")
};

assert.equal(files.packageJson.private, true);
assert.equal(files.packageJson.type, "module");
assert.equal(files.packageJson.dependencies["@svsprotocol/solana"], "^0.2.0");
assert.equal(files.packageJson.dependencies.ai, "^6.0.202");
assert.equal(files.packageJson.dependencies.zod, "^4.4.3");
assert.equal(files.packageJson.scripts.validate, "node ./validate.mjs");

for (const required of [
"Vercel AI SDK",
"examples/svs-verified-action/",
"Protocol Or Wallet Gate",
"createHostedVerifiedAgentRegistryMiddleware",
"https://svsprotocol.com/docs",
"https://registry.svsprotocol.com/registry.json",
"https://svsprotocol.com/docs#verified-agent-standard",
"https://svsprotocol.com/verify",
"SVS_RUN_LIVE_SUBMIT=true",
"no API keys"
]) {
assert.match(files.readme, new RegExp(escapeRegExp(required)));
}

assert.match(files.readme, /The default run is import validation only/);
assert.doesNotMatch(files.readme, /import\/config validation only/);
assert.doesNotMatch(files.readme, /private SVS operator repository/);
assert.doesNotMatch(files.readme, /From the SVS repository/);

for (const required of [
"SVS_SERVER_URL",
"SVS_BOT_ID",
"SVS_BOT_POLICY_ID",
"SVS_BOT_API_KEY",
"SVS_BOT_REQUEST_SIGNING_SECRET",
"SVS_BOT_EXPECTED_INTEGRATION_CONTRACT_HASH",
"SOLANA_RPC_URL",
"SVS_SERIALIZED_TRANSACTION_BASE64",
"SVS_RUN_LIVE_SUBMIT=false"
]) {
assert.match(files.envExample, new RegExp(escapeRegExp(required)));
}

for (const required of [
"ai",
"zod",
"@svsprotocol/solana/vercel-ai",
"VERCEL_AI_SDK_ADAPTER_VERSION",
"VERCEL_AI_SDK_TOOL_NAME",
"createSvsVercelAiSdkTools",
"requireSvsProductionReady",
"verifyAndSubmitSolanaAction",
"assertLiveSubmitEnv",
"isDirectRun",
"fileURLToPath",
"SVS_SERIALIZED_TRANSACTION_BASE64",
"SVS_RUN_LIVE_SUBMIT"
]) {
assert.match(files.example, new RegExp(escapeRegExp(required)));
}

for (const [name, text] of Object.entries({
readme: files.readme,
envExample: files.envExample,
example: files.example
})) {
assertNoCommittedSecret(name, text);
}

const dryRun = JSON.parse(execFileSync(process.execPath, [
fileURLToPath(new URL("./svs-verified-action.mjs", import.meta.url))
], {
encoding: "utf8"
}));

assert.equal(dryRun.ok, true);
assert.equal(dryRun.status, "dry_run");
assert.equal(dryRun.packageExport, "@svsprotocol/solana/vercel-ai");
assert.equal(dryRun.toolName, "svsVerifyAndSubmitSolanaAction");

console.log(JSON.stringify({
ok: true,
package: files.packageJson.name,
target: "vercel-ai-sdk",
packageExport: "@svsprotocol/solana/vercel-ai",
toolName: "svsVerifyAndSubmitSolanaAction",
dryRun: {
status: dryRun.status,
packageExport: dryRun.packageExport,
toolName: dryRun.toolName
}
}, null, 2));

function assertNoCommittedSecret(name, text) {
const secretPatterns = [
/svs_live_[A-Za-z0-9_-]{8,}/,
/svs_req_live_[A-Za-z0-9_-]{8,}/,
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
/\[[\d,\s]{80,}\]/
];

for (const pattern of secretPatterns) {
assert.doesNotMatch(text, pattern, `${name} includes secret-looking material`);
}
}

function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}