// documentation

Build with Splintr

Accept payment from any token, on any chain. Receive one clean settlement in the asset you want. Splintr routes and auto-swaps under the hood — you just handle a single webhook.

Coming from x402? Compare the workflows →

Resource payments and cross-chain commerce solve different needs. Explore invoicing, MCP tools, and settlement without assuming drop-in x402 compatibility.

// 00 overview

How Splintr works

A merchant creates a PaymentIntent on the server for a target amount and settlement asset (e.g. USDC on Solana). The customer opens the hosted checkout, connects any EVM or Solana wallet, and pays in whatever token they hold — ETH on Base, SOL, USDC on Arbitrum, anything. Splintr scans balances, plans a route across LI.FI (EVM) and Jupiter (Solana), executes swaps and bridges, and settles the target amount into the merchant's wallet.

  • One integration, every chain and token the aggregators reach.
  • Idempotent REST API — safe retries with Idempotency-Key.
  • Signed webhooks with HMAC-SHA256, exponential backoff, replay-safe.
  • Test mode with simulated settlement — no real funds move.
// 01 quickstart

Ship in under 10 minutes

Install both packages.

terminal
# server + react
npm install @splintr-dev/server @splintr-dev/react

# or with your favorite tool
pnpm add @splintr-dev/server @splintr-dev/react
bun add @splintr-dev/server @splintr-dev/react

Then create your first intent on the server and mount the checkout in the client.

server/checkout.ts
import { Splintr } from "@splintr-dev/server";

const splintr = new Splintr({ apiKey: process.env.SPLINTR_SECRET_KEY! });

const intent = await splintr.paymentIntents.create({
  amount: 99,
  currency: "USD",
  settlement: {
    chain: "robinhood", // launch example; any supported settlement chain works
    token: "ETH",
    address: MERCHANT_WALLET,
  },
  metadata: { orderId: "order_8841" },
  successUrl: "https://your.app/thanks?o=order_8841",
  cancelUrl:  "https://your.app/cart",
  idempotencyKey: crypto.randomUUID(),
});

return { id: intent.id, checkoutUrl: intent.checkout_url };
app/Checkout.tsx
import { SplintrProvider, SplintrCheckout } from "@splintr-dev/react";

export function Checkout({ intentId }: { intentId: string }) {
  return (
    <SplintrProvider publicKey={import.meta.env.VITE_SPLINTR_PUBLIC_KEY}>
      <SplintrCheckout
        intentId={intentId}
        onSettled={(i) => console.log("UI settled; fulfill from verified webhook", i.id)}
        onError={(error) => console.error("Checkout error", error)}
      />
    </SplintrProvider>
  );
}
// 02 keys

API keys

You get two key pairs from the dashboard — one for test mode (pk_test_… / sk_test_…) and one for live mode. Secret keys stay on the server. Publishable keys are safe in the browser.

.env
# .env.local
SPLINTR_SECRET_KEY=sk_test_...        # server only
SPLINTR_PUBLIC_KEY=pk_test_...        # safe in the browser
pk_test_… / pk_live_…
Publishable. Used by the React SDK to embed hosted checkout.
sk_test_… / sk_live_…
Secret. Bearer for the REST API.
whsec_…
Webhook signing secret. Verifies incoming webhook signatures.
// 03 server SDK

@splintr-dev/server

Runtime-agnostic — Node 18+, Bun, Deno, Cloudflare Workers, any edge runtime.

server/checkout.ts
import { Splintr } from "@splintr-dev/server";

const splintr = new Splintr({ apiKey: process.env.SPLINTR_SECRET_KEY! });

const intent = await splintr.paymentIntents.create({
  amount: 99,
  currency: "USD",
  settlement: {
    chain: "robinhood", // launch example; any supported settlement chain works
    token: "ETH",
    address: MERCHANT_WALLET,
  },
  metadata: { orderId: "order_8841" },
  successUrl: "https://your.app/thanks?o=order_8841",
  cancelUrl:  "https://your.app/cart",
  idempotencyKey: crypto.randomUUID(),
});

return { id: intent.id, checkoutUrl: intent.checkout_url };

Every method mirrors the REST API and throws a typed SplintrError on non-2xx responses.

createsplintr.paymentIntents.create(input)
Create a new intent
retrievesplintr.paymentIntents.retrieve(id)
Fetch current state
listsplintr.paymentIntents.list({ limit })
List recent intents
cancelsplintr.paymentIntents.cancel(id)
Cancel a pending intent
verifysplintr.webhooks.verify(body, sig, sec)
Verify webhook signature
parsesplintr.webhooks.parse(body)
Parse a verified event
// 04 react SDK

@splintr-dev/react

The fastest path: <SplintrButton> — one-line drop-in that calls your endpoint, opens the hosted checkout, and fires callbacks on settle/fail. Copy the four blocks below top-to-bottom and you have a working checkout.

1 · install

terminal
npm install @splintr-dev/server @splintr-dev/react
# or: bun add @splintr-dev/server @splintr-dev/react

2 · env vars

.env.local
# .env.local
NEXT_PUBLIC_SPLINTR_PUBLIC_KEY=pk_test_...   # browser-safe
SPLINTR_SECRET_KEY=sk_test_...               # server only
MERCHANT_WALLET_ADDRESS=0xYourWallet

3 · wrap your app once

app/providers.tsx
// app/providers.tsx
"use client";
import { SplintrProvider } from "@splintr-dev/react";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SplintrProvider publicKey={process.env.NEXT_PUBLIC_SPLINTR_PUBLIC_KEY!}>
      {children}
    </SplintrProvider>
  );
}

4 · server endpoint that starts the intent

app/api/splintr/route.ts
// app/api/splintr/route.ts
import { Splintr } from "@splintr-dev/server";

const splintr = new Splintr({ secretKey: process.env.SPLINTR_SECRET_KEY! });

export async function POST(req: Request) {
  const { productId, email } = await req.json();
  // Demo catalog: load authorized carts/orders from your trusted store in production.
  const catalog: Record<string, string> = { splintr_demo: "25.00" };
  const amount = typeof productId === "string" ? catalog[productId] : undefined;
  if (!amount) return Response.json({ error: "Unknown product" }, { status: 400 });
  const intent = await splintr.paymentIntents.create({
    amount,
    idempotencyKey: req.headers.get("idempotency-key") ?? undefined,
    currency: "USD",
    customerEmail: email,
    settlement: {
      chain: "robinhood",
      token: "ETH",
      address: process.env.MERCHANT_WALLET_ADDRESS!,
    },
  });
  return Response.json(intent);
}

5 · drop the button anywhere

app/checkout/page.tsx
// app/checkout/page.tsx
"use client";
import { SplintrButton } from "@splintr-dev/react";

export default function Checkout() {
  return (
    <SplintrButton
      endpoint="/api/splintr"
      input={{ productId: "splintr_demo", email: "buyer@example.com" }}
      label="Pay $25 with Splintr"
      onSettled={(intent) => console.log("UI settled; fulfill from verified webhook", intent.id)}
      onError={(error) => console.error("checkout error", error)}
    />
  );
}

Prefer to embed the checkout inline instead of a button? Use <SplintrCheckout> with an intentId you already created server-side:

app/Checkout.tsx
import { SplintrProvider, SplintrCheckout } from "@splintr-dev/react";

export function Checkout({ intentId }: { intentId: string }) {
  return (
    <SplintrProvider publicKey={import.meta.env.VITE_SPLINTR_PUBLIC_KEY}>
      <SplintrCheckout
        intentId={intentId}
        onSettled={(i) => console.log("UI settled; fulfill from verified webhook", i.id)}
        onError={(error) => console.error("Checkout error", error)}
      />
    </SplintrProvider>
  );
}

Prefer to redirect instead of embed?

redirect.tsx
<SplintrCheckout intentId={intent.id} mode="redirect" />
// or just point the user at the hosted URL
window.location.href = intent.checkout_url;

You can also poll intent status yourself with the hook:

hook.ts
import { useSplintrIntent } from "@splintr-dev/react";

const { data, status } = useSplintrIntent(intentId, { intervalMs: 2000 });
// data.status: created → quoted → executing → settled | failed | expired
// 05 REST

Direct HTTP

All endpoints live under /api/public/v1/ and take a bearer secret key.

POST/api/public/v1/payment-intents
Create a payment intent
GET/api/public/v1/payment-intents
List recent intents (limit, status)
GET/api/public/v1/payment-intents/:id
Retrieve one intent
POST/api/public/v1/payment-intents/:id/cancel
Cancel a pending intent
GET/api/public/v1/checkout/:id
Public read for polling (no auth)
create
curl https://splintr.cash/api/public/v1/payment-intents \
  -H "Authorization: Bearer $SPLINTR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount": 99,
    "currency": "USD",
    "settlement_chain": "robinhood",
    "settlement_token": "ETH",
    "settlement_address": "MERCHANT_WALLET",
    "metadata": { "orderId": "order_8841" }
  }'
retrieve
curl https://splintr.cash/api/public/v1/payment-intents/pi_123 \
  -H "Authorization: Bearer $SPLINTR_SECRET_KEY"
list
curl "https://splintr.cash/api/public/v1/payment-intents?limit=20&status=settled" \
  -H "Authorization: Bearer $SPLINTR_SECRET_KEY"
cancel
curl -X POST https://splintr.cash/api/public/v1/payment-intents/pi_123/cancel \
  -H "Authorization: Bearer $SPLINTR_SECRET_KEY"

Always send an Idempotency-Key on POST — retries with the same key and body return the original response; a mismatched body returns 409.

// 06 reference

The PaymentIntent shape

types.ts
type PaymentIntent = {
  id: string;                    // pi_...
  amount: string;                // decimal string
  currency: "USD" | string;
  status:
    | "created"
    | "quoted"
    | "awaiting_signature"
    | "executing"
    | "settled"
    | "failed"
    | "expired"
    | "cancelled"
    | "refunded";
  checkout_url: string;          // hosted Splintr checkout
  expires_at: string;            // ISO — quote validity window (30m)
  created_at: string;
  metadata?: Record<string, unknown>;
};
// 07 webhooks

Signed, retried, replay-safe

Configure endpoints in the dashboard. Splintr POSTs a signed JSON event and retries with exponential backoff (30s → 12h, up to 8 attempts).

POST /your/webhook
{
  "id":   "evt_2b0a...",
  "type": "payment_intent.settled",
  "created_at": "2026-07-16T18:22:31Z",
  "data": {
    "id": "pi_123",
    "amount": "99.00",
    "currency": "USD",
    "settlement_chain": "robinhood",
    "settlement_token": "ETH",
    "metadata": { "orderId": "order_8841" }
  }
}
header
splintr-signature: t=1710000000,v1=af12...9c
// t  = unix seconds signed
// v1 = hex HMAC-SHA256 of `${t}.${rawBody}` with your endpoint's signing secret
// tolerate up to ±5 minutes of clock skew

Verify with the SDK — this works in Node, Bun, Deno, and Workers.

app/api/webhook.ts
// Integration sketch: implement fulfil/releaseHold in your order store.
import { Splintr } from "@splintr-dev/server";

const splintr = new Splintr({ apiKey: process.env.SPLINTR_SECRET_KEY! });

export async function POST(req: Request) {
  const raw = await req.text();
  const sig = req.headers.get("splintr-signature");

  const ok = await splintr.webhooks.verify(
    raw,
    sig,
    process.env.SPLINTR_WEBHOOK_SECRET!,   // whsec_...
  );
  if (!ok) return new Response("bad signature", { status: 401 });

  const event = splintr.webhooks.parse(raw);
  switch (event.type) {
    // Application helpers below must atomically deduplicate event.id and
    // validate the authorized order, amount, currency and payment intent.
    case "payment_intent.settled": await fulfil(event.data, event.id); break;
    case "payment_intent.failed":  await releaseHold(event.data); break;
  }
  return new Response("ok");
}
quote.created
Route locked. Customer is ready to sign.
payment_intent.executed
Customer signed / broadcast succeeded. Awaiting on-chain confirmation.
payment_intent.settled
Funds arrived at merchant wallet. Fulfil order.
payment_intent.expired
Quote window closed. Ask customer to retry.
settlement.confirmed
Per-leg on-chain confirmation for splintrmented payments.
payout.created
Off-ramp / self-custody payout queued.
payout.paid
Payout confirmed at destination.
payout.failed
Payout rail rejected the transfer. Check dashboard for retry.
refund.created
Refund queued.
refund.processing
Refund broadcast, awaiting confirmation.
refund.paid
Refund confirmed at destination.
refund.failed
Refund rail failed. Retry via dashboard.
ping
Test event fired from the dashboard webhook tester.
// 08 errors

Typed, machine-readable

error-handling.ts
// SplintrError has a stable machine-readable code + HTTP status
try {
  await splintr.paymentIntents.create({ /* … */ });
} catch (e) {
  if (e instanceof SplintrError) {
    console.log(e.status, e.code, e.message, e.requestId);
  }
  throw e;
}
unauthorized
Missing or revoked secret key.
invalid input
Zod validation failed. Check details.
idempotency reused
Same key, different body. Rotate the key or fix the body.
not found
Intent id does not belong to this merchant.
rate_limited
Slow down. Retry with backoff.
// 09 test mode

No real funds move

Use sk_test_… keys and any merchant wallet address. The checkout synthesises balances and simulates settlement so you can rehearse the full flow — webhooks, retries, error paths — without touching real chains.

test-mode.ts
// Test keys never move real funds. Any settlement chain/token combo works,
// balances are synthesized in the checkout widget and settlement is simulated.
const splintr = new Splintr({ apiKey: "sk_test_..." });
// 10 lifecycle

Status transitions

created
  → quoted
    → awaiting_signature
      → executing
        → settled
              ↘ failed | expired | cancelled | refunded
// 9 chains

Chains & tokens

Splintr launches on Robinhood Chain, but it is not a Robinhood-only platform. Its payment and settlement architecture is chain-agnostic, so merchants can configure any supported destination while customers pay from any supported source network.

Splintr routes across a pool of aggregators (LI.FI, 0x, ParaSwap, KyberSwap, Across, Socket, Rango, CoW Swap, deBridge, and OpenOcean/Odos where quoted) for EVM and Jupiter for Solana. Anything one of the routers can source is a valid payment token; settlement lands in the asset the merchant configured.

EVM
USDC / USDT / ETH / native on Ethereum, Base, Arbitrum, Optimism, Polygon, BNB, and Avalanche, plus native ETH on Robinhood Chain (id 4663, settles in ETH — no canonical USDC there yet). Cross-chain via LI.FI, Across, Socket, deBridge, and Circle CCTP where available.
Solana
USDC / SOL and any SPL token routed through Jupiter with slippage bounds.
// 20 tools

AI Agents (MCP)

Evaluating x402 for agents? MCP is also part of the x402 specification. Splintr's distinction is its commerce toolset and routing workflows. Read the comparison →

Splintr ships a built-in Model Context Protocol server so AI agents (Claude, ChatGPT, Cursor, Codex, custom LangChain/AI SDK agents) can create invoices, poll settlement, dispatch payouts, and verify webhooks on behalf of a signed-in merchant. Agents authenticate via OAuth 2.1 through your Splintr account — no API key sharing.

Endpoint
https://<your-splintr-host>/mcp
Transport
MCP Streamable HTTP (spec 2025-06-18)
Auth
OAuth 2.1 + Dynamic Client Registration (RFC 7591)
Scopes
none required — RLS scopes every read/write to the OAuth user's merchants
Audience
authenticated (Supabase-issued access tokens)
Token TTL
1h access token, refresh handled by the MCP client

Tools exposed (20 total):

toollist_merchants
List merchant accounts you own
toollist_supported_tokens
Splintr safety registry
toolcreate_payment_intent
Bill a customer
toolget_payment_intent
Read a single intent
toollist_payment_intents
List recent invoices
toolcancel_payment_intent
Void an unpaid invoice
toolpoll_settlement
Poll status until finalized
toolget_route_candidates
Inspect router candidates
toolcreate_refund
Refund a settled or partial intent
toollist_refunds
List refunds for a merchant
toolretry_refund
Requeue a failed refund, optionally to a new address
toolcreate_payout
Enqueue payout for a settled intent
toolget_payout
Read a single payout
toollist_payouts
List recent payouts
toolreplay_payout
Retry a failed payout
toolcreate_webhook_endpoint
Register a webhook URL
toollist_webhook_endpoints
List webhook endpoints
tooldelete_webhook_endpoint
Remove a webhook endpoint
toolrotate_webhook_secret
Rotate an endpoint's signing secret
toolverify_webhook
Verify signed webhook payload

create_payout — request

snippet.tsx
{
  "name": "create_payout",
  "arguments": {
    "payment_intent_id": "22222222-2222-2222-2222-222222222222",
    "idempotency_key": "payout-order-42"
  }
}

create_payout — response

snippet.tsx
{ "result": { "payoutId": "33333333-3333-3333-3333-333333333333" } }

Fully idempotent. Repeats with the same idempotency_key(or same intent) return the existing payoutId and never trigger a second transfer, SPL ATA creation, or Circle API call. Overlapping in-process calls coalesce and are marked coalesced: true.

poll_settlement — response

snippet.tsx
{
  "id": "22222222-…",
  "status": "settled",
  "amount_received_usd": "100.00",
  "platform_fee_usd": "1.00",
  "net_settlement_usd": "99.00",
  "settled_at": "2026-07-17T12:34:56Z"
}

Poll every 3–5s until status is one of settled,expired, refunded, or cancelled. Payout-side terminal states arrive via payout.paid /payout.failed webhooks.

verify_webhook — expectations

snippet.tsx
Header:  Splintr-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
Body:    raw JSON — DO NOT re-serialize before verifying
HMAC:    HMAC_SHA256(secret, `${t}.${rawBody}`)  → hex
Compare: timing-safe equal to v1
Tolerance: reject when |now - t| > 300s

The verify_webhook tool encapsulates all of the above. For local verification, use Splintr({…}).webhooks.verify(body, header, secret)— same timing-safe compare and 5-minute skew window.

Connect from Claude Desktop / Cursor:

~/.claude/mcp.json
{
  "mcpServers": {
    "splintr": { "url": "https://<your-splintr-host>/mcp" }
  }
}

Your agent walks through OAuth once, approves access on the Splintr consent screen, then calls tools as your user — all merchant reads/writes are enforced by RLS. No API key ever leaves your control.

// downloads

Tools, examples & one-click install

Everything you need to try Splintr in 60 seconds.

// connect Splintr to Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "splintr": { "url": "https://splintr.cash/mcp" }
  }
}

// connect Splintr to Cursor

~/.cursor/mcp.json
{
  "mcpServers": {
    "splintr": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://splintr.cash/mcp"]
    }
  }
}

Full install manifest at /.well-known/mcp-install.json. First call triggers OAuth in your browser — approve on the Splintr consent screen and the agent inherits your merchant scope via RLS.

// 14 security

How Splintr protects payments

  • Non-custodial by default — funds route wallet → aggregator → merchant, never a Splintr pool.
  • Secret keys never leave the server; publishable keys are safe in the browser.
  • Webhook signatures use HMAC-SHA256 with a shared secret, ±5m timestamp tolerance, timing-safe compare.
  • Idempotency keys are stored per merchant and replay POSTs with the original response.
  • RLS enforced on every merchant-scoped table; API keys are hashed at rest (SHA-256).
  • Test/live mode isolation — keys and intents cannot cross environments.
Need help? Reach us at hello@splintr.cash.