ai-paywall-agent-sdk-sui
Drop-in fetch() replacement for AI agents. Automatically detects, pays, and retries SUI HTTP 402 paywalls. Builds pay_and_unlock PTBs with configurable MIST budget caps — supports both simple and vault split payment modes.
Overview
Replace fetch() with client.fetch(). On a 200 response, it is a pure passthrough. On a 402, the SDK:
- Parses the 402 body — extracts the challenge object ID, price in MIST, and Move target
- Checks budget caps (per-request and total)
- Builds and submits a
pay_and_unlock(orpay_and_unlock_split) PTB on SUI - Retries the original request with
X-SUI-PAYMENT-TXandX-SUI-CHALLENGE-IDheaders - Returns the unlocked
Response
Installation
npm install ai-paywall-agent-sdk-sui @mysten/sui@mysten/sui is a peer dependency — your project controls the version.
Fund your agent address
The agent needs SUI to pay for gas and content.
# Testnet faucet
sui client faucet
# Or visit: https://faucet.sui.io/?address=<your-address>
# Check balance
sui client balanceQuick Start
import { createSuiAgentClient, fromKeypairFile } from "ai-paywall-agent-sdk-sui";
const client = createSuiAgentClient({
network: "testnet",
signer: fromKeypairFile(), // reads ~/.sui/sui_config/sui.keystore
maxPerRequestMist: 10_000_000, // hard cap: 0.01 SUI per request
maxTotalMist: 1_000_000_000, // session budget: 1 SUI
onPayment: (p) => console.log("paid:", p.txDigest, p.priceMist, "MIST"),
});
// Drop-in fetch — 402s paid automatically
const res = await client.fetch("https://publisher.com/articles/ai-trends");
const data = await res.json();
// Running spend total in MIST
console.log("spent:", client.spend(), "MIST");
// Agent's SUI address
console.log("address:", client.address());Signers
The SDK needs to sign SUI transactions. Pick the helper that matches your setup.
SUI keystore file (default)
import { fromKeypairFile } from "ai-paywall-agent-sdk-sui";
signer: fromKeypairFile() // ~/.sui/sui_config/sui.keystore
signer: fromKeypairFile("/path/to/sui.keystore") // custom pathBech32 private key (from env)
import { fromSecretKeyBech32 } from "ai-paywall-agent-sdk-sui";
// Export key: sui keytool export --key-identity <address>
signer: fromSecretKeyBech32(process.env.SUI_AGENT_SECRET_KEY)Base64 key (keystore format)
import { fromSecretKeyBase64 } from "ai-paywall-agent-sdk-sui";
signer: fromSecretKeyBase64(process.env.SUI_AGENT_KEY_BASE64)Existing Ed25519Keypair
import { fromKeypair } from "ai-paywall-agent-sdk-sui";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
signer: fromKeypair(Ed25519Keypair.generate())Configuration
Pass these options to createSuiAgentClient({ ... }).
| Option | Default | Description |
|---|---|---|
signer | required | Ed25519Keypair from one of the signer helpers. |
network | "testnet" | "testnet" or "mainnet". |
rpcUrl | public RPC | Override SUI RPC endpoint. Use a paid RPC in production. |
maxPerRequestMist | unlimited | Hard per-request cap. Throws BudgetExceededError if exceeded. |
maxTotalMist | unlimited | Session budget cap. Throws BudgetExceededError when crossed. |
onPayment(info) | — | Callback after each payment: { txDigest, priceMist, challengeObjectId }. |
Vault (Split) Mode
When the publisher enables a PublisherVault, the 402 response body includes challenge.vaultObjectId. The agent SDK detects this automatically and calls pay_and_unlock_split instead of pay_and_unlock. No extra configuration required.
// The SDK handles both modes transparently.
// The 402 body tells the agent which Move function to call:
//
// Simple mode:
// challenge.move.target = "0xff98::paywall::pay_and_unlock"
//
// Vault mode (publisher has SUI_VAULT_ID set):
// challenge.move.target = "0xff98::vault::pay_and_unlock_split"
// challenge.vaultObjectId = "0x..."
//
// client.fetch() reads these fields and builds the correct PTB.
const res = await client.fetch("https://publisher.com/premium/report");
const data = await res.json();
// In vault mode, data.payment.split shows the breakdown:
// { publisherMist: 800000, poolMist: 150000, protocolMist: 50000 }
console.log(data.payment?.split);Error Handling
All SDK errors extend PaywallError.
import {
BudgetExceededError,
PaymentRefusedError,
UnsupportedChallengeError,
} from "ai-paywall-agent-sdk-sui";
try {
const res = await client.fetch("https://publisher.com/article");
const data = await res.json();
} catch (err) {
if (err instanceof BudgetExceededError) {
// Per-request or session budget cap hit.
// Inspect err.message for details. Do NOT retry with a new client
// just to get around the cap.
} else if (err instanceof PaymentRefusedError) {
// pay_and_unlock TX failed on-chain.
// Likely insufficient SUI balance or challenge expired.
} else if (err instanceof UnsupportedChallengeError) {
// The 402 body is not a Tollgate SUI challenge.
// May be a different paywall scheme.
} else {
throw err;
}
}| Error class | When it throws |
|---|---|
BudgetExceededError | Price exceeds maxPerRequestMist or maxTotalMist |
PaymentRefusedError | pay_and_unlock TX failed on SUI (balance, expired, etc.) |
UnsupportedChallengeError | 402 body is not a Tollgate SUI challenge |
Spend Tracking
// client.spend() returns total MIST spent this session
console.log(client.spend()); // e.g. 3000000 (3 payments of 0.001 SUI each)
// client.address() returns the paying agent's SUI address
console.log(client.address()); // "0x24ae..."
// Use onPayment to persist receipts across process restarts:
const client = createSuiAgentClient({
signer: fromKeypairFile(),
onPayment: async ({ txDigest, priceMist, challengeObjectId }) => {
await db.insert({ txDigest, priceMist, ts: new Date() });
},
});The spend counter resets when the client is re-created (per Node.js process). For persistent tracking, use the onPayment hook.