Install#
Node ≥ 18. Works in Node, Bun, Deno, edge runtimes, and the browser.
npm install @sovrn/wallet
# or
pnpm add @sovrn/wallet
# or
bun add @sovrn/walletQuickstart#
Generate a wallet, hit an x402 endpoint. The wallet's keys are created and signed entirely in your process — Sovrn has no backend in this path.
import { Wallet } from "@sovrn/wallet";
const agent = Wallet.create({
name: "research-agent.04",
chains: ["solana", "base"],
policy: { dailyCapMicros: 500_000_000 }, // $500 / 24h
});
const result = await agent.fetch(
"https://sovrn.fun/api/x402/demo?resource=arxiv",
);
if (result.response.ok) {
console.log(await result.response.json());
}Multi-chain#
One wallet handles both Solana (ed25519) and EVM (secp256k1) accounts. All EVM chains share a single 0x address.
const agent = Wallet.create({
chains: ["solana", "base", "ethereum", "arbitrum", "polygon"],
});
agent.address // Solana base58 address
agent.evmAddress // 0x... checksummed
agent.addressFor("base") // 0x...
agent.signEvmMessage("hi") // EIP-191 personal_signx402 auto-pay#
agent.fetch() is a drop-in replacement for window.fetch. When the server responds with HTTP 402, the SDK parses the challenge, applies the wallet policy, signs an authorization, and retries with x-payment-authorization.
const { response, authorization, event } = await agent.fetch(url);
console.log(authorization.signature); // base58 ed25519 sig
console.log(event.kind); // "x402.paid"
console.log(await response.json()); // the actual dataIf policy rejects the payment, you get back { decision, event } instead, with event.kind === "x402.denied" and a reason like daily_cap_exceeded.
Policies#
Every payment runs through evaluate(). You can mutate the policy at runtime; the ledger keeps a signed history of every change.
agent.updatePolicy({
dailyCapMicros: 250_000_000, // $250 / 24h
perCallCapMicros: 5_000_000, // $5 max per call
allowlist: ["compute.fly.io"],
denylist: ["sketchy.api"],
});
agent.pause(); // immediately blocks all auto-pays
agent.resume(); // back onSigned ledger#
Every wallet event (creation, policy change, attempt, paid, denied) is appended to an in-process log, each entry signed with the wallet's ed25519 key and chained to the previous event's signature.
for (const e of agent.events()) {
console.log(e.seq, e.kind, e.signature);
}
agent.spent24hMicros(); // sum of x402.paid events in the last 24hVerifying signatures#
Anyone with the payer's public key can independently verify an authorization. No Sovrn server in the loop.
import { verify, decodeBase58 } from "@sovrn/wallet";
const ok = verify(
decodeBase58(payerAddress),
new TextEncoder().encode(canonicalEnvelope),
decodeBase58(signatureBase58),
);For EVM, verifyEvmMessage(address, message, signatureHex) recovers the signer from an EIP-191 personal_sign signature.
Wire format#
The x402 challenge and authorization headers are semicolon-delimited key/value pairs. Field order doesn't matter; the canonical envelope used for signing is alphabetically sorted by key, newline-joined.
# Server → client (HTTP 402)
x-payment-required: price=0.014; recipient=sovrn_demo_treasury_5GqL; chain=solana; nonce=ab12cd34; resource=arxiv; expires=1733080000000
# Client → server (retry)
x-payment-authorization: payer=<base58 pubkey>; recipient=...; chain=solana;
nonce=ab12cd34; price=0.014000; resource=arxiv;
signed_at=1733079940123; signature=<base58 sig>The canonical message that gets signed is:
chain=solana
nonce=ab12cd34
payer=<base58 pubkey>
priceMicros=14000
recipient=sovrn_demo_treasury_5GqL
resource=arxiv
signedAt=1733079940123