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.
Resource payments and cross-chain commerce solve different needs. Explore invoicing, MCP tools, and settlement without assuming drop-in x402 compatibility.
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.
Ship in under 10 minutes
Install both packages.
# 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/reactThen create your first intent on the server and mount the checkout in the client.
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 };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>
);
}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.local
SPLINTR_SECRET_KEY=sk_test_... # server only
SPLINTR_PUBLIC_KEY=pk_test_... # safe in the browser@splintr-dev/server
Runtime-agnostic — Node 18+, Bun, Deno, Cloudflare Workers, any edge runtime.
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.
@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
npm install @splintr-dev/server @splintr-dev/react
# or: bun add @splintr-dev/server @splintr-dev/react2 · env vars
# .env.local
NEXT_PUBLIC_SPLINTR_PUBLIC_KEY=pk_test_... # browser-safe
SPLINTR_SECRET_KEY=sk_test_... # server only
MERCHANT_WALLET_ADDRESS=0xYourWallet3 · wrap your app once
// 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
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
"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:
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?
<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:
import { useSplintrIntent } from "@splintr-dev/react";
const { data, status } = useSplintrIntent(intentId, { intervalMs: 2000 });
// data.status: created → quoted → executing → settled | failed | expiredDirect HTTP
All endpoints live under /api/public/v1/ and take a bearer secret key.
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" }
}'curl https://splintr.cash/api/public/v1/payment-intents/pi_123 \
-H "Authorization: Bearer $SPLINTR_SECRET_KEY"curl "https://splintr.cash/api/public/v1/payment-intents?limit=20&status=settled" \
-H "Authorization: Bearer $SPLINTR_SECRET_KEY"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.
The PaymentIntent shape
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>;
};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).
{
"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" }
}
}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 skewVerify with the SDK — this works in Node, Bun, Deno, and Workers.
// 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");
}Typed, machine-readable
// 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;
}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 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_..." });Status transitions
created
→ quoted
→ awaiting_signature
→ executing
→ settled
↘ failed | expired | cancelled | refundedChains & 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.
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.
Tools exposed (20 total):
create_payout — request
{
"name": "create_payout",
"arguments": {
"payment_intent_id": "22222222-2222-2222-2222-222222222222",
"idempotency_key": "payout-order-42"
}
}create_payout — response
{ "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
{
"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
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| > 300sThe 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:
{
"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.
Tools, examples & one-click install
Everything you need to try Splintr in 60 seconds.
postman-collection.json and hit every REST endpoint with your test key. Bruno reads the same v2.1 schema.npm install @splintr-dev/server.<SplintrCheckout /> and hooks.// connect Splintr to Claude Desktop
{
"mcpServers": {
"splintr": { "url": "https://splintr.cash/mcp" }
}
}// connect Splintr to Cursor
{
"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.
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.