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:

  1. Parses the 402 body — extracts the challenge object ID, price in MIST, and Move target
  2. Checks budget caps (per-request and total)
  3. Builds and submits a pay_and_unlock (or pay_and_unlock_split) PTB on SUI
  4. Retries the original request with X-SUI-PAYMENT-TX and X-SUI-CHALLENGE-ID headers
  5. Returns the unlocked Response
Budget is enforced client-side before the PTB is built. A misconfigured server cannot drain your wallet — the cap check happens before any transaction is signed.

Installation

bash
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.

bash
# Testnet faucet
sui client faucet

# Or visit: https://faucet.sui.io/?address=<your-address>

# Check balance
sui client balance

Quick Start

js
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)

js
import { fromKeypairFile } from "ai-paywall-agent-sdk-sui";

signer: fromKeypairFile()                               // ~/.sui/sui_config/sui.keystore
signer: fromKeypairFile("/path/to/sui.keystore")        // custom path

Bech32 private key (from env)

js
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)

js
import { fromSecretKeyBase64 } from "ai-paywall-agent-sdk-sui";

signer: fromSecretKeyBase64(process.env.SUI_AGENT_KEY_BASE64)

Existing Ed25519Keypair

js
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({ ... }).

OptionDefaultDescription
signerrequiredEd25519Keypair from one of the signer helpers.
network"testnet""testnet" or "mainnet".
rpcUrlpublic RPCOverride SUI RPC endpoint. Use a paid RPC in production.
maxPerRequestMistunlimitedHard per-request cap. Throws BudgetExceededError if exceeded.
maxTotalMistunlimitedSession 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.

js
// 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.

js
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 classWhen it throws
BudgetExceededErrorPrice exceeds maxPerRequestMist or maxTotalMist
PaymentRefusedErrorpay_and_unlock TX failed on SUI (balance, expired, etc.)
UnsupportedChallengeError402 body is not a Tollgate SUI challenge

Spend Tracking

js
// 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.