Skip to content
Spendkit
GuidesBuild with Spendkit
DEVELOPER GUIDE · COPYABLE CODE

Copy an integration example

Choose your language, supply an Agent connection's credentials, and run the example. Each file calls Spendkit OAuth and MCP directly, discovers tools, reads the bound Agent and Policy, and previews a payment. Your own business logic goes at the marked comment. The same public examples can be read by an AI coding assistant.

What the examples do immediately

With a Client ID and Client Secret, they connect to Spendkit and read the Agent and Policy. Add SPENDKIT_RECIPIENT to preview a proposed USDG payment. This default path does not send money. To enable payment, first implement the marked wallet sender using the Agent's bound wallet; then the example can request authorization, submit the exact transaction, record its hash, and query status.

Before you copy

  1. Open AI Agents in the dashboard → Create an Agent, open its details, bind and verify its Payment Wallet, and set its onchain Spending Policy.
  2. In that Agent's details, select Connect Agent and choose Full Runtime Access for payments. Copy the Client ID and Client Secret into your Runtime environment or protected secret store. Open Agent configuration →
  3. Copy one complete file below. For real payment, add your own wallet signer at createPaymentWalletSender (or the equivalent function). Provide a stable SPENDKIT_IDEMPOTENCY_KEY for each logical payment.

Where each value comes from

Value in the exampleWhere to get or set it
SPENDKIT_BASE_URLUse https://spendkit-alpha.vercel.app for this hosted service; the examples already use it by default. It is a public URL, not a credential created in the dashboard.
SPENDKIT_CLIENT_ID and SPENDKIT_CLIENT_SECRETIn AI Agents → open your Agent and select Connect Agent. Copy the values from its connection settings. Store the secret in protected Runtime configuration, never in source code or an AI prompt.
Payment Wallet address and signerBind and verify your own wallet in the Agent setup → You can check its bound address in the dashboard or with spendkit_get_connection. Your wallet app or secure signing service supplies the signer. If it uses a private key, keep that key in your own protected secret store. The dashboard does not generate or show a private key; never paste one there.
SPENDKIT_RECIPIENT and SPENDKIT_AMOUNTSupply the recipient and proposed amount from your application's payment request. They are not dashboard credentials.
SPENDKIT_IDEMPOTENCY_KEYGenerate a stable ID in your application for one logical payment, such as an order ID; reuse it only when retrying that same payment.
DefaultConnect → get_connection → get_policy → preview_payment
After your wallet code is readyauthorize_payment → wallet send → record_payment → get_payment_status
Keep the payment tracking calls.

After your wallet submits the authorized transaction, keep spendkit_record_payment(intentId, txHash) and spendkit_get_payment_status(intentId) in your integration. The first links the submitted transaction to its Spendkit payment request so Payment Activity can update promptly; the second checks the final onchain result. If recording fails, retry recording or query the same intent and transaction—do not send the payment again. Without recording, the dashboard may show an outdated status until onchain reconciliation catches up. The Router still enforces the onchain spending limits.

Choose a language

All four examples use the current MCP protocol and the same tool sequence. They are one-shot command-line clients: a long-running Agent should cache and refresh OAuth tokens before expiry. The tool reference explains each input and output.

Save as javascript.mjs and run node javascript.mjs. No extra package is required. Open the complete source file →

// Node.js 20+. Run with SPENDKIT_CLIENT_ID, SPENDKIT_CLIENT_SECRET,
// SPENDKIT_RECIPIENT and optionally SPENDKIT_AMOUNT in your environment.
// Default run is read-only. Payment requires SPENDKIT_ENABLE_PAYMENT=1 and
// your own implementation of createPaymentWalletSender below.
// Public base URL defaults below. Dashboard > AI Agents > your Agent > Connect Agent
// provides Client ID/Secret. Your own wallet signer supplies the payment key;
// never enter a private key in the dashboard or expose it to the AI model.
const baseUrl = (process.env.SPENDKIT_BASE_URL || "https://spendkit-alpha.vercel.app").replace(/\/$/, "");
const protocol = "2026-07-28";
const clientInfo = { name: "spendkit-javascript-example", version: "1.0.0" };
const clientId = process.env.SPENDKIT_CLIENT_ID;
const clientSecret = process.env.SPENDKIT_CLIENT_SECRET;
if (!clientId || !clientSecret) throw new Error("Set SPENDKIT_CLIENT_ID and SPENDKIT_CLIENT_SECRET");

async function accessToken() {
  const body = new URLSearchParams({
    grant_type: "client_credentials",
    client_id: clientId,
    client_secret: clientSecret,
  });
  const response = await fetch(`${baseUrl}/oauth/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  });
  const data = await response.json();
  if (!response.ok || !data.access_token) throw new Error(`OAuth failed: ${JSON.stringify(data)}`);
  return data.access_token;
}

function createMcpClient(token) {
  let id = 0;
  async function rpc(method, params = {}) {
    const name = params.name;
    const body = {
      jsonrpc: "2.0",
      id: ++id,
      method,
      params: {
        ...params,
        _meta: {
          "io.modelcontextprotocol/protocolVersion": protocol,
          "io.modelcontextprotocol/clientInfo": clientInfo,
          "io.modelcontextprotocol/clientCapabilities": {},
        },
      },
    };
    const response = await fetch(`${baseUrl}/mcp`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
        Accept: "application/json",
        "MCP-Protocol-Version": protocol,
        "Mcp-Method": method,
        ...(name ? { "Mcp-Name": name } : {}),
      },
      body: JSON.stringify(body),
    });
    const message = await response.json();
    if (!response.ok || message.error) throw new Error(`MCP ${method} failed: ${JSON.stringify(message.error || message)}`);
    return message.result;
  }
  async function tool(name, args = {}) {
    const result = await rpc("tools/call", { name, arguments: args });
    const data = result.structuredContent ?? JSON.parse(result.content.find((item) => item.type === "text").text);
    if (result.isError && data.allowed !== false && data.accepted !== false) {
      throw new Error(`${name} failed: ${data.reason || data.error || JSON.stringify(data)}`);
    }
    return data;
  }
  return { rpc, tool };
}

function createPaymentWalletSender() {
  // YOUR WALLET CODE: return an async function that validates and submits the
  // exact transaction using the wallet bound to this Agent. Keep the private
  // key outside the AI model and Spendkit Server. Return the real tx hash.
  throw new Error("Implement createPaymentWalletSender before enabling payment");
}

const token = await accessToken();
const mcp = createMcpClient(token);
await mcp.rpc("server/discover");
await mcp.rpc("tools/list");
const connection = await mcp.tool("spendkit_get_connection");
const policy = await mcp.tool("spendkit_get_policy");
console.log("Agent:", connection.agent.name, "Wallet:", connection.agent.paymentWallet?.address);
console.log("Policy ready:", policy.ready, "Remaining today:", policy.remainingDaily);

// YOUR BUSINESS CODE: replace these environment values with the payment
// amount and recipient selected by your own application or AI Agent.
const payment = {
  amount: process.env.SPENDKIT_AMOUNT || "0.01",
  recipient: process.env.SPENDKIT_RECIPIENT,
  token: "USDG",
};
if (!payment.recipient) {
  console.log("Set SPENDKIT_RECIPIENT to preview a payment");
  process.exit(0);
}
const preview = await mcp.tool("spendkit_preview_payment", payment);
console.log("Preview:", preview.allowed, preview.reason);
if (!preview.allowed || process.env.SPENDKIT_ENABLE_PAYMENT !== "1") process.exit(0);

// Prepare the wallet before requesting an authorization. The default example
// stops here until you implement the wallet sender above.
const sendFromBoundWallet = createPaymentWalletSender();
const idempotencyKey = process.env.SPENDKIT_IDEMPOTENCY_KEY;
if (!idempotencyKey) throw new Error("Set one stable SPENDKIT_IDEMPOTENCY_KEY per logical payment");
const authorized = await mcp.tool("spendkit_authorize_payment", { ...payment, idempotencyKey });
if (!authorized.allowed || !authorized.transaction) throw new Error("No new Router transaction was authorized; inspect intent status");
const boundWallet = connection.agent.paymentWallet?.address?.toLowerCase();
const transaction = authorized.transaction;
if (!boundWallet || transaction.from.toLowerCase() !== boundWallet
  || authorized.authorization.paymentWallet.toLowerCase() !== boundWallet
  || authorized.authorization.recipient.toLowerCase() !== payment.recipient.toLowerCase()
  || authorized.authorization.amount !== preview.amountAtomic
  || authorized.authorization.token.toLowerCase() !== preview.policy.tokenAddress.toLowerCase()
  || transaction.to.toLowerCase() !== connection.network.routerAddress.toLowerCase()
  || transaction.chainId !== connection.network.chainId) {
  throw new Error("Authorization does not match the intended payment or bound wallet");
}
const txHash = await sendFromBoundWallet(transaction);
// KEEP THIS CALL after wallet submission: intentId + txHash lets Spendkit
// track the payment promptly. Without it, Activity/status can lag until
// onchain reconciliation; the Router still enforces spending limits.
const recorded = await mcp.tool("spendkit_record_payment", { intentId: authorized.intentId, txHash });
// If recording fails, retry with this intentId/txHash; never resend the payment.
if (!recorded.accepted) throw new Error(`Record failed: ${recorded.error}`);
// KEEP THIS CALL to reconcile the receipt; submitted is not final success.
const status = await mcp.tool("spendkit_get_payment_status", { intentId: authorized.intentId });
console.log("Payment status:", status.status, "Transaction:", status.txHash);
Copying a client is not the same as granting wallet access.

The default examples read and preview only. Before turning on SPENDKIT_ENABLE_PAYMENT=1, connect a wallet signer that owns the address returned by spendkit_get_connection. It must validate the authorized amount, recipient, chain, Router, and wallet, then submit the exact returned transaction. Spendkit Server does not receive the private key or broadcast the payment.

Read the payment steps → Read the raw HTTP request format → Give these examples to an AI coding tool →