@aivo-x402/solana
v1.0.2
Published
AIVO Solana layer on top of the official x402 v2 protocol — auto-DID wallet + self-hosted facilitator with x402.org fallback. Re-exports @x402/* (core, svm, hono, fetch, extensions).
Readme
@aivo-x402/solana
AIVO Solana layer on top of the official x402 v2 protocol.
One package, three jobs:
- 🔁 Re-exports the official
@x402/*packages (core,svm,hono,fetch,extensions) so you import one name. - 🪪 Auto-DID wallet flow —
did:sol:<pubkey>per W3C DID spec. No signup, no API key. - 🌐 Self-hosted facilitator with
x402.orgfallback — try self-host, fall back if it dies.
Built for the AIVO 3-repo stack: this package is the shared protocol layer between aivo-swarm (server) and rover (client). See AGENTS.md v1.2.2 for the full spec.
Table of contents
- Why this package exists
- Install
- Quick start
- What this package re-exports from
@x402/* - What AIVO adds on top
- Failure semantics
- Publishing & releases
- Architecture diagram
- License
Why this package exists
The AIVO stack has three repos that need to talk to each other over x402:
| Repo | Role | Talks x402 as… |
|---|---|---|
| aivo-swarm | Pool intel API | Server (paywall routes) |
| rover | LP agent | Client (auto-pays Swarm) |
| x402-solana (this) | Shared protocol | Both + AIVO helpers |
Two pieces are unique to AIVO and not in the upstream @x402/* packages:
- Auto-DID wallet flow — wallet pubkey = identity, no registration. Swarm upserts on first contact.
- Self-hosted facilitator with fallback — run our own facilitator, fall back to
x402.orgif it dies.
Both come from AGENTS.md v1.2.2. This package is the implementation.
Install
npm install @aivo-x402/solanaRequirements: Node.js ≥ 22.12, TypeScript ≥ 5.0, ESM-only.
Quick start
1. Auto-DID wallet (AGENTS.md v1.2.2 a–d)
Wallet = identity. did:sol:<pubkey> per W3C DID spec for Solana. No signup, no API key.
import { createDID, verifyWallet, walletFromDID } from "@aivo-x402/solana";
import nacl from "tweetnacl";
// ── Server (Swarm) ──────────────────────────────────────────
// Derive a DID from any wallet that pings you. Upsert on first contact.
const did = createDID("AGwiTnRnXCgf9AemWdvTwWDz18YUevTGDv2TokT97n1Q");
// → { id: "did:sol:AGwiT...", controller: "AGwiT...", created: "2026-06-02T..." }
// Extract the pubkey back out of a DID string
const wallet = walletFromDID("did:sol:AGwiTnRnXCgf9AemWdvTwWDz18YUevTGDv2TokT97n1Q");
// → "AGwiTnRnXCgf9AemWdvTwWDz18YUevTGDv2TokT97n1Q"
// ── Client (Rover) ──────────────────────────────────────────
// Prove wallet ownership by signing a payload (e.g. the request body)
const keypair = nacl.sign.keyPair.fromSeed(seed);
const payload = new TextEncoder().encode(JSON.stringify({ pool: "X" }));
const signature = nacl.sign.detached(payload, keypair.secretKey);
const isValid = verifyWallet({
payload,
signature,
wallet: keypair.publicKey.toBase58(),
});
// → true / falseWhy this is nice: one wallet works across Swarm + Rover + (future) @aivo-x402/solana consumers. No separate registration, no API keys, no friction.
2. Self-hosted facilitator with x402.org fallback (AGENTS.md v1.2.2 e)
import {
createFacilitator,
x402ResourceServer,
ExactSvmScheme,
X402FacilitatorUnavailableError,
} from "@aivo-x402/solana";
const facilitator = createFacilitator({
selfHostUrl: process.env.X402_FACILITATOR_PRIMARY, // e.g. https://x402.aivo.sh
fallbackUrl: "https://x402.org",
timeoutMs: 15_000,
onFallback: (reason) => console.warn(`[x402] falling back: ${reason}`),
});
const server = new x402ResourceServer(
new ExactSvmScheme(/* signer config */),
);
// Use the facilitator to verify/settle payments
try {
const result = await facilitator.verify({ paymentPayload, accepted });
if (!result.isValid) throw new Error("payment rejected");
} catch (err) {
if (err instanceof X402FacilitatorUnavailableError) {
// Both self-host AND x402.org are down — fail closed
throw err;
}
throw err;
}The order of resolution is:
1. selfHostUrl (your own facilitator — lowest latency, no per-tx fee)
↓ if unavailable / 5xx / timeout / network error
2. fallbackUrl (x402.org — Coinbase + Cloudflare, public)
↓ if still failing
3. throw X402FacilitatorUnavailableErrorSee Failure semantics for details.
3. Hono server (paywall route)
import { Hono } from "hono";
import {
paymentMiddleware,
RoutesConfig,
ExactSvmScheme,
x402ResourceServer,
} from "@aivo-x402/solana";
const app = new Hono();
const routes: RoutesConfig = {
"GET /v1/pools/top": {
accepts: {
scheme: "exact",
network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
payTo: "AGwiTnRnXCgf9AemWdvTwWDz18YUevTGDv2TokT97n1Q",
price: "$0.001",
},
description: "Top N pools by composite score",
},
};
const server = new x402ResourceServer(new ExactSvmScheme(/* signer */));
app.use(
paymentMiddleware(
routes,
server, // x402ResourceServer (handles facilitator internally)
),
);
app.get("/v1/pools/top", (c) => c.json({ pools: [] }));4. Fetch client (Rover)
import {
wrapFetchWithPayment,
x402Client,
ExactSvmScheme,
} from "@aivo-x402/solana";
const client = new x402Client().register(
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
new ExactSvmScheme({ /* signer config */ }),
);
const paidFetch = wrapFetchWithPayment(fetch, client);
const res = await paidFetch("https://swarm.example.com/v1/pools/top");
// → automatically retries 402 with payment, then returns the dataWhat this package re-exports from @x402/*
You can import everything from @aivo-x402/solana instead of juggling scoped names.
| From | What you get |
|---|---|
| @x402/core/types | PaymentPayload, PaymentRequired, PaymentRequirements, ResourceInfo, Network, VerifyResponse, SettleResponse |
| @x402/core/server | x402ResourceServer, x402HTTPResourceServer, FacilitatorClient, RoutesConfig, RouteConfig |
| @x402/core/http | encodePaymentRequiredHeader, decodePaymentSignatureHeader (base64 wire helpers) |
| @x402/fetch | x402Client, x402HTTPClient, wrapFetchWithPayment |
| @x402/hono | paymentMiddleware, paymentMiddlewareFromConfig |
| @x402/svm | ExactSvmScheme, ClientSvmConfig, FacilitatorRpcConfig |
| @x402/extensions | Bazaar, sign-in-with-x, etc. (re-exported as a pass-through) |
For the full API surface, see the upstream docs at coinbase/x402.
What AIVO adds on top
| Export | Purpose |
|---|---|
| createDID(wallet) | Derive did:sol:<pubkey> from a Solana pubkey (W3C DID) |
| getDID(wallet) | Alias of createDID for symmetry with "get" semantics |
| verifyWallet({ payload, signature, wallet }) | Ed25519 signature verification via tweetnacl |
| walletFromDID(did) | Extract the pubkey back from a did:sol: string |
| createFacilitator({ selfHostUrl?, fallbackUrl?, timeoutMs?, onFallback? }) | Self-host → fallback chain facilitator client |
| X402FacilitatorUnavailableError | Thrown when both facilitators fail |
| AivoFacilitator (type) | Shape of the returned facilitator client |
The DID helpers do not persist anything — they're pure derivation functions. Storage of the DID ↔ wallet mapping is the caller's responsibility (Swarm stores in Postgres, Rover keeps it in memory per session).
Failure semantics
The facilitator wrapper is opinionated about what counts as a "real" failure:
| Outcome | Action |
|---|---|
| 2xx from self-host | Use the response, no fallback |
| 4xx from self-host | Return the body as-is — isValid: false is a legitimate answer, do not fall back |
| 5xx from self-host | Fall back to fallbackUrl |
| Network error / timeout on self-host | Fall back to fallbackUrl |
| 2xx from fallback | Use the response, log via onFallback |
| Fallback also fails (5xx / network / timeout) | Throw X402FacilitatorUnavailableError |
| 4xx from fallback | Return the body as-is (legitimate rejection) |
This matches the AGENTS.md spec: "Never let Swarm serve data without facilitator verification — that breaks the paywall. Fail closed, not open."
Publishing & releases
Tagged releases are published to npm automatically via .github/workflows/release.yml.
One-time setup (maintainer)
- Create an npm granular access token with
publishscope on@aivo-x402/solana. - Add it to the repo as a GitHub secret:
NPM_TOKEN. - Repo already has
id-token: writepermission for npm provenance attestation.
Cutting a release
# 1. Bump version in package.json
# 2. Move [Unreleased] → dated version in CHANGELOG.md
# 3. Commit, tag, push
git add package.json CHANGELOG.md
git commit -m "chore: release v0.1.0"
git tag v0.1.0
git push origin main --tagsCI will:
- ✅ Install deps, typecheck, lint, test, build
- 📦 Upload
dist/artifact - 🚀 Publish to npm with
--provenance --access public
You can also trigger a dry run manually from the Actions tab without publishing.
Architecture diagram
┌──────────────────────────────────────────────────────────┐
│ USER / AGENT │
└────────────┬─────────────────────────┬───────────────────┘
│ │
(1) API │ │ (2) run rover binary
$0.001+ │ │ (revenue: Swarm + Jupiter)
USDC │ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ aivo-swarm │◀──HMAC──│ rover │
│ (Hono + DBOS) │ beacon │ (Hono + Mastra) │
│ │ │ │
│ • pool scoring │ │ • screener │
│ • LLM judge │ │ • position mgr │
│ • x402 paywall │ │ • x402 client │
└────────┬────────┘ └────────┬────────┘
│ │
│ (3) self-pay │ (4) swap volume
│ USDC x402 │ kickback 0.1–0.3%
▼ ▼
┌──────────────────────────────────────────────────┐
│ @aivo-x402/solana (this package) │
│ • Re-exports @x402/* (core, svm, hono, fetch) │
│ • createDID / verifyWallet (auto-DID) │
│ • createFacilitator (self-host → x402.org) │
└──────────────────────────────────────────────────┘
│ │
▼ ▼
Self-hosted facilitator Jupiter Referral Program
(aivo-sh) → fallback: (referralAccount hardcoded)
https://x402.org → 0.1–0.3% kickbackLicense
MIT — © 2026 AIVO.
Built on the open x402 protocol by Coinbase & Cloudflare. ❤️
