@uzproof/verify
v2.0.0
Published
Proof-of-Use verification SDK for Solana. First gasless x402+MPP verification provider. Verify real on-chain usage — swaps, perpetuals (Drift), staking, token holds, NFTs, and more.
Maintainers
Readme
@uzproof/verify
Proof-of-Use verification SDK for Solana. Verify real on-chain usage — swaps, staking, token holds, NFTs, and more.
UZPROOF is the first Proof-of-Use Attestor on the Solana Attestation Service (SAS).
Install
npm install @uzproof/verifyQuick Start
import { UzproofClient } from '@uzproof/verify';
const client = new UzproofClient();
// Read-only calls work without authentication
const token = await client.getTokenInfo('JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN');
const attestation = await client.getAttestation('7H4RVLZfGLBvY6k1DdmGSUcCKCw6nJG28emTztkyyrLZ');
const protocol = await client.detectContract('JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4');Authentication
verify() is the only paid endpoint. It accepts two auth paths:
- Self-verify (browser) — the user is signed into uzproof.com with a
sol-authcookie. The SDK's default behavior. Wallet in the request must match the cookie. - x402 pay-per-call (backend) — pay 0.05 USDC on Solana to the
UZPROOF treasury, pass the signature in the
xPaymentoption. One signature = one call; signatures expire after 5 minutes.
Read-only endpoints (detectContract, getTokenInfo, getAttestation)
are free and require no authentication.
Self-verify example
const result = await client.verify({
wallet: '7H4RVLZfGLBvY6k1DdmGSUcCKCw6nJG28emTztkyyrLZ',
action: 'defi_swap',
config: { min_amount_usd: 5 }
});
console.log(result.verified); // true
console.log(result.result.matchingTxCount); // 3x402 backend example
import { UzproofClient, PaymentRequiredError } from '@uzproof/verify';
const client = new UzproofClient();
try {
const result = await client.verify(
{ wallet: '...', action: 'defi_swap_volume', config: { min_volume_usd: 1000 } },
{ xPayment: paymentTxSignature },
);
} catch (e) {
if (e instanceof PaymentRequiredError) {
// e.details carries the x402 payment instructions —
// pay USDC per /api/x402/pricing, retry with the new signature.
console.log(e.details);
}
}See uzproof.com/api/x402/pricing for current rates and treasury address.
API
verify(request, options?) — Verify on-chain action
const result = await client.verify(
{
wallet: 'WALLET_ADDRESS',
action: 'defi_swap', // 26 auto-verified action types
config: {
program_id: 'JUP6Lk...', // Optional: specific program
min_amount_usd: 10, // Optional: minimum value
token_mint: 'So11...', // Optional: specific token
}
},
{ xPayment: 'SIG...' }, // Optional: x402 backend auth
);detectContract(programId) — Auto-detect protocol
const info = await client.detectContract('JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4');
// info.program.name = "Jupiter Aggregator v6"
// info.program.supportedActions = ["defi_swap", "defi_swap_buy", "defi_swap_sell"]getTokenInfo(mint) — Fetch token metadata + price
const token = await client.getTokenInfo('JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN');
// token.symbol = "JUP"
// token.priceUsd = 0.143getAttestation(wallet) — Check on-chain SAS attestation
const status = await client.getAttestation('7H4RVL...');
// status.hasAttestation = true
// status.explorer = "https://explorer.solana.com/address/..."Supported Actions (26 auto-verified)
| Category | Actions |
|----------|---------|
| DeFi | defi_swap, defi_swap_buy, defi_swap_sell, defi_swap_volume, defi_hold_token, defi_hold_stablecoin, defi_stake_sol, defi_hold_staked, defi_lend, defi_borrow, defi_vote, defi_repay, defi_claim |
| Perpetuals | defi_perp_trade, defi_perp_volume (Drift v2 — strict feePayer match, keeper fills excluded) |
| On-Chain | nft_hold, nft_check, token_balance, tx_verify, gaming_play |
Supported Protocols (15)
Jupiter, Marinade, Sanctum, Orca, Raydium, Drift, Drift Vaults, Kamino, MarginFi, Meteora, Jito, Tensor, Magic Eden, Metaplex, SPL Token.
MPP (Multi-Party Payment) helpers in SDK 1.4
The 1.4 release adds first-class support for the MPP charge
challenge (Solana Charge variant — draft-solana-charge-00) on
/api/verify, alongside the existing x402 micropayment protocol. The
endpoint chooses which 402 challenge to issue based on the request's
Accept header, so existing 1.3.x callers are unaffected by default.
| Protocol | Trigger | 402 body | Retry header | Gasless |
| --- | --- | --- | --- | --- |
| x402 (default) | no Accept header | {x402Version:2, resource:{url,…}, accepts:[…], schemes:[…]} (dual-emit canon v2 + legacy v1.x) | X-Payment | no |
| MPP (opt-in) | Accept: application/x-mpp | {challenge,expires,id} + WWW-Authenticate: Payment … (canon-only, RFC-9110) | Authorization: Payment … (canon, SDK 2.0+) | yes |
SDK 2.0 — canon migration. The 402 envelope now advertises both the canonical x402 v2 accepts[] array (per coinbase/x402 specs/v2 §5.1) and the legacy schemes[] array (@uzproof/[email protected] contract). The MPP WWW-Authenticate header emits a single canonical RFC-9110 Payment auth-scheme (per solana-foundation/mpp-sdk/protocol/core/headers.rs:PAYMENT_SCHEME) — a dual-emit Payment …, solana … form was tested and rolled back because comma-joining trips strict RFC-9110 parsers with a Duplicate parameter: method error. SDK 2.0's buildMppAuthorizationHeader emits Payment <base64url>; the UZPROOF server's verifyMppCredential dual-accepts both Payment and legacy solana prefixes through the deprecation window. @uzproof/[email protected] clients running against the new server classify MPP via the X-Payment-Protocol: mpp header marker (the WWW-Authenticate substring match against solana no longer fires; the fallback marker is what the 1.4.x detectProtocol consults next).
The four building blocks an integrator needs are documented below.
1. detectProtocol(headers) — branch retry logic
import { detectProtocol, type PaymentProtocol } from "@uzproof/verify";
const proto: PaymentProtocol | null = detectProtocol(res.headers);
// "mpp" → MPP charge challenge (use buildMppAuthorizationHeader)
// "x402" → classic x402 micropayment (use the X-Payment header)
// null → not a 402, or an opaque errorInspects an HTTP response's headers and returns the protocol the server selected. The function reads (in order):
WWW-Authenticate: Payment …(canon RFC-9110, what the UZPROOF server emits post-2026-05) ORWWW-Authenticate: solana …(legacy, what pre-2026-05 UZPROOF servers and any 1.4.x-compatible deployment emit) — authoritative per RFC 9110.X-Payment-Protocol: mpp | x402— explicit protocol marker. The UZPROOF server emits this on every 402 so a client that does not match the WWW-Authenticate auth-scheme (e.g.@uzproof/[email protected]against the post-2026-05 canon-only emit) still classifies MPP correctly via this header.X-Payment-Required: true— fallback for older x402 deployments.
It accepts any object that exposes a get(name): string | null
method, so Response.headers, node-fetch headers, undici
Headers, and Next.js NextRequest.headers all work without
adapters.
2. Parsing the MPP response body
When detectProtocol(res.headers) === "mpp" and res.status === 402,
the JSON body conforms to the MppChallenge shape — there is no
custom parser; standard res.json() plus the exported types is the
contract:
import type { MppChallenge } from "@uzproof/verify";
const { challenge } = (await res.json()) as { challenge: MppChallenge };
// challenge.id – opaque server-issued challenge id
// challenge.realm – e.g. "uzproof.com"
// challenge.method – "solana"
// challenge.intent – "charge"
// challenge.expires – unix-seconds expiry
// challenge.charge.network – "solana-mainnet"
// challenge.charge.recipient – treasury wallet (base58)
// challenge.charge.amount – amount as integer string in token base units
// challenge.charge.mint – SPL mint (USDC by default)
// challenge.methodDetails?.feePayer – true when gasless is offered
// challenge.methodDetails?.feePayerKey – UZPROOF feePayer pubkey (gasless)The exported MppChallenge type is the parser — TypeScript narrows
unsafe unknown access at compile time. JS-without-TS callers should
treat any of the above as required-when-present (challenges are
issued only after server-side schema validation).
3. buildMppAuthorizationHeader(challenge, payload) — retry credential
import { buildMppAuthorizationHeader } from "@uzproof/verify";
// Path A — signature: caller settled the charge themselves
const authA = buildMppAuthorizationHeader(challenge, {
type: "signature",
value: "<base58 of confirmed Solana tx>",
});
// Path B — transaction: gasless via UZPROOF feePayer
const authB = buildMppAuthorizationHeader(challenge, {
type: "transaction",
value: "<base64url of partially-signed VersionedTransaction>",
});Renders the value of the Authorization: Payment … retry header
(canon RFC-9110 auth-scheme per solana-foundation/mpp-sdk). The
credential body is canonicalized with RFC 8785 (JCS) before
base64url-encoding, so the byte sequence the server verifies is
independent of in-memory key order on the caller side. The function
is dependency-free and runs identically in browsers, Node ≥ 18, and
edge runtimes.
The header value is Payment <base64url(JCS({challenge, payload}))> —
no extra parameters.
2.0 migration note: SDK 1.4.x emitted
solana <base64url>. The UZPROOF server (src/lib/mpp.ts:verifyMppCredential) dual-accepts both prefixes through the deprecation window, so a 1.4.x client remains functional against the new server. Any canon-compliant MPP server (including the Coinbase referencempp-sdk) requires thePaymentprefix — switch to SDK 2.0 to interop with those.
4. Retry-with-fee-payer (gasless flow)
When challenge.methodDetails?.feePayer === true, the server is
offering to co-sign the SOL transaction fee. The agent needs USDC
only — no SOL. The flow is:
- Build a
VersionedTransactionwhosefeePayeris set tochallenge.methodDetails.feePayerKey(UZPROOF's pubkey). - Add the SPL-Token transfer instruction (USDC →
challenge.charge.recipient, amountchallenge.charge.amount). - Partially sign with the user's wallet (do not send to the network — UZPROOF co-signs and broadcasts).
- Serialize with
tx.serialize({ requireAllSignatures: false })→ base64url-encode → pass aspayload.valuewithtype: "transaction". - Resend
/api/verifywith theAuthorization: Payment …header (canon, SDK 2.0+) — the server dual-accepts the legacyAuthorization: solana …form too.
UZPROOF validates the partial signature, attaches the fee-payer signature, broadcasts to mainnet, waits for confirmation, and answers the verify request — all in a single round-trip from the agent's perspective.
Code examples
Browser example (Phantom + fetch)
import {
detectProtocol,
buildMppAuthorizationHeader,
type MppChallenge,
} from "@uzproof/verify";
import {
Connection,
PublicKey,
VersionedTransaction,
TransactionMessage,
} from "@solana/web3.js";
import {
createTransferInstruction,
getAssociatedTokenAddress,
} from "@solana/spl-token";
async function verifyGasless(wallet: { publicKey: PublicKey; signTransaction: (tx: VersionedTransaction) => Promise<VersionedTransaction> }) {
// 1. Cold call /api/verify with MPP preference.
const cold = await fetch("https://uzproof.com/api/verify", {
method: "POST",
headers: {
Accept: "application/x-mpp",
"Content-Type": "application/json",
},
body: JSON.stringify({
wallet: wallet.publicKey.toBase58(),
action: "defi_swap",
}),
});
if (cold.status !== 402 || detectProtocol(cold.headers) !== "mpp") {
throw new Error("Server did not offer an MPP challenge");
}
const { challenge } = (await cold.json()) as { challenge: MppChallenge };
// 2. Build a partially-signed VersionedTransaction with the
// UZPROOF feePayer in the fee-payer slot.
const conn = new Connection("https://api.mainnet-beta.solana.com");
const feePayer = new PublicKey(challenge.methodDetails!.feePayerKey!);
const recipient = new PublicKey(challenge.charge.recipient);
const mint = new PublicKey(challenge.charge.mint);
const amount = BigInt(challenge.charge.amount);
const transferIx = createTransferInstruction(
/* source ATA */ await getAssociatedTokenAddress(mint, wallet.publicKey),
/* dest ATA */ await getAssociatedTokenAddress(mint, recipient),
/* owner */ wallet.publicKey,
/* amount */ amount,
);
const { blockhash } = await conn.getLatestBlockhash();
const msg = new TransactionMessage({
payerKey: feePayer,
recentBlockhash: blockhash,
instructions: [transferIx],
}).compileToV0Message();
const vtx = new VersionedTransaction(msg);
// 3. Phantom partial-sign (no broadcast — UZPROOF co-signs).
const signed = await wallet.signTransaction(vtx);
const serialized = signed.serialize();
const payloadValue = btoa(String.fromCharCode(...serialized))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
// 4. Build retry credential and resend.
const auth = buildMppAuthorizationHeader(challenge, {
type: "transaction",
value: payloadValue,
});
const verifyRes = await fetch("https://uzproof.com/api/verify", {
method: "POST",
headers: {
Authorization: auth,
"Content-Type": "application/json",
},
body: JSON.stringify({
wallet: wallet.publicKey.toBase58(),
action: "defi_swap",
}),
});
return verifyRes.json();
}Node example (server-side agent, signature path)
import {
detectProtocol,
buildMppAuthorizationHeader,
type MppChallenge,
} from "@uzproof/verify";
async function verifyWithSignature(
walletAddress: string,
settledTxSignatureBase58: string,
) {
// 1. Cold call /api/verify with MPP preference.
const cold = await fetch("https://uzproof.com/api/verify", {
method: "POST",
headers: {
Accept: "application/x-mpp",
"Content-Type": "application/json",
},
body: JSON.stringify({ wallet: walletAddress, action: "defi_swap" }),
});
if (cold.status !== 402 || detectProtocol(cold.headers) !== "mpp") {
throw new Error(`Unexpected response: ${cold.status}`);
}
const { challenge } = (await cold.json()) as { challenge: MppChallenge };
// 2. The agent has already settled the USDC charge on-chain
// (challenge.charge.amount → challenge.charge.recipient on
// challenge.charge.network) and holds the confirmed signature.
const auth = buildMppAuthorizationHeader(challenge, {
type: "signature",
value: settledTxSignatureBase58,
});
// 3. Resend with the retry credential.
const verifyRes = await fetch("https://uzproof.com/api/verify", {
method: "POST",
headers: {
Authorization: auth,
"Content-Type": "application/json",
},
body: JSON.stringify({ wallet: walletAddress, action: "defi_swap" }),
});
if (!verifyRes.ok) {
throw new Error(`UZPROOF verify failed: ${verifyRes.status}`);
}
return verifyRes.json();
}Runnable end-to-end examples (including the gasless
VersionedTransactionflow with full instruction-builder code) are published underpackages/verify-sdk/examples/.
On-Chain (SAS)
UZPROOF attestations are stored on Solana mainnet via the Solana Attestation Service:
- SAS Program:
22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG - Credential:
2chgBfvkwhnHQVVAyXKDK6CBjbCRMQ8aLWrysL5UQyyF - Schema (v2):
8yW2BboQuhp2MMmrQLFz35V6VSqC48MF7wZ5bmzcTeTF
Links
- Website: https://uzproof.com
- Docs: https://uzproof.com/docs
- Twitter: https://x.com/uzproof
- Telegram: https://t.me/uz_proof
License
MIT
