Build with Frag
Accept payment from any token, on any chain. Receive one clean settlement in the asset you want. Frag routes and auto-swaps under the hood — you just handle a single webhook.
How Frag 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. Frag 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 @fragmentpay/server @fragmentpay/react
# or with your favorite tool
pnpm add @fragmentpay/server @fragmentpay/react
bun add @fragmentpay/server @fragmentpay/reactThen create your first intent on the server and mount the checkout in the client.
import { FragmentPay } from "@fragmentpay/server";
const frag = new FragmentPay({ apiKey: process.env.FRAG_SECRET_KEY! });
const intent = await frag.paymentIntents.create({
amount: 99,
currency: "USD",
settlement: {
chain: "solana",
token: "USDC",
address: MERCHANT_SOLANA_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 { FragmentPayProvider, FragmentPayCheckout } from "@fragmentpay/react";
export function Checkout({ intentId }: { intentId: string }) {
return (
<FragmentPayProvider publicKey={import.meta.env.VITE_FRAG_PUBLIC_KEY}>
<FragmentPayCheckout
intentId={intentId}
onSettled={(i) => unlockOrder(i.metadata?.orderId as string)}
onError={(i) => toast.error(`payment ${i.status}`)}
/>
</FragmentPayProvider>
);
}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
FRAG_SECRET_KEY=sk_test_... # server only
FRAG_PUBLIC_KEY=pk_test_... # safe in the browser@fragmentpay/server
Runtime-agnostic — Node 18+, Bun, Deno, Cloudflare Workers, any edge runtime.
import { FragmentPay } from "@fragmentpay/server";
const frag = new FragmentPay({ apiKey: process.env.FRAG_SECRET_KEY! });
const intent = await frag.paymentIntents.create({
amount: 99,
currency: "USD",
settlement: {
chain: "solana",
token: "USDC",
address: MERCHANT_SOLANA_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 FragError on non-2xx responses.
@fragmentpay/react
The fastest path: <FragmentPayButton> — 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 @fragmentpay/server @fragmentpay/react
# or: bun add @fragmentpay/server @fragmentpay/react2 · env vars
# .env.local
NEXT_PUBLIC_FRAG_PUBLIC_KEY=pk_test_... # browser-safe
FRAG_SECRET_KEY=sk_test_... # server only
MERCHANT_USDC_ADDRESS=0xYourBaseWallet3 · wrap your app once
// app/providers.tsx
"use client";
import { FragmentPayProvider } from "@fragmentpay/react";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<FragmentPayProvider publicKey={process.env.NEXT_PUBLIC_FRAG_PUBLIC_KEY!}>
{children}
</FragmentPayProvider>
);
}4 · server endpoint that starts the intent
// app/api/frag/route.ts
import { Frag } from "@fragmentpay/server";
const frag = new Frag({ secretKey: process.env.FRAG_SECRET_KEY! });
export async function POST(req: Request) {
const { amount, email } = await req.json();
const intent = await frag.paymentIntents.create({
amount,
currency: "USD",
customerEmail: email,
settlement: {
chain: "base",
token: "USDC",
address: process.env.MERCHANT_USDC_ADDRESS!,
},
});
return Response.json(intent);
}5 · drop the button anywhere
// app/checkout/page.tsx
"use client";
import { FragmentPayButton } from "@fragmentpay/react";
export default function Checkout() {
return (
<FragmentPayButton
endpoint="/api/frag"
input={{ amount: 25, email: "buyer@example.com" }}
label="Pay $25 with Frag"
onSettled={(intent) => console.log("paid!", intent.id)}
onError={(intent) => console.error("payment failed", intent.status)}
/>
);
}Prefer to embed the checkout inline instead of a button? Use <FragmentPayCheckout> with an intentId you already created server-side:
import { FragmentPayProvider, FragmentPayCheckout } from "@fragmentpay/react";
export function Checkout({ intentId }: { intentId: string }) {
return (
<FragmentPayProvider publicKey={import.meta.env.VITE_FRAG_PUBLIC_KEY}>
<FragmentPayCheckout
intentId={intentId}
onSettled={(i) => unlockOrder(i.metadata?.orderId as string)}
onError={(i) => toast.error(`payment ${i.status}`)}
/>
</FragmentPayProvider>
);
}Prefer to redirect instead of embed?
<FragmentPayCheckout 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 { useFragmentIntent } from "@fragmentpay/react";
const { data, status } = useFragmentIntent(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://api.frag.dev/api/public/v1/payment-intents \
-H "Authorization: Bearer $FRAG_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 99,
"currency": "USD",
"settlement_chain": "solana",
"settlement_token": "USDC",
"settlement_address": "MERCHANT_SOL_WALLET",
"metadata": { "orderId": "order_8841" }
}'curl https://api.frag.dev/api/public/v1/payment-intents/pi_123 \
-H "Authorization: Bearer $FRAG_SECRET_KEY"curl "https://api.frag.dev/api/public/v1/payment-intents?limit=20&status=settled" \
-H "Authorization: Bearer $FRAG_SECRET_KEY"curl -X POST https://api.frag.dev/api/public/v1/payment-intents/pi_123/cancel \
-H "Authorization: Bearer $FRAG_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 Frag 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. Frag 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": "solana",
"settlement_token": "USDC",
"metadata": { "orderId": "order_8841" }
}
}frag-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.
import { FragmentPay } from "@fragmentpay/server";
const frag = new FragmentPay({ apiKey: process.env.FRAG_SECRET_KEY! });
export async function POST(req: Request) {
const raw = await req.text();
const sig = req.headers.get("frag-signature");
const ok = await frag.webhooks.verify(
raw,
sig,
process.env.FRAG_WEBHOOK_SECRET!, // whsec_...
);
if (!ok) return new Response("bad signature", { status: 401 });
const event = frag.webhooks.parse(raw);
switch (event.type) {
case "payment_intent.settled": await fulfil(event.data); break;
case "payment_intent.failed": await releaseHold(event.data); break;
}
return new Response("ok");
}Typed, machine-readable
// FragError has a stable machine-readable code + HTTP status
try {
await frag.paymentIntents.create({ /* … */ });
} catch (e) {
if (e instanceof FragError) {
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 frag = new FragmentPay({ apiKey: "sk_test_..." });Status transitions
created
→ quoted
→ awaiting_signature
→ executing
→ settled
↘ failed | expired | cancelled | refundedChains & tokens
Frag wraps LI.FI for EVM (Ethereum, Base, Arbitrum, Optimism, Polygon, BNB) and Jupiter for Solana. Anything either aggregator can source is a valid payment token. Settlement can be any of the following pairs today:
AI Agents (MCP)
Frag 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 Frag account — no API key sharing.
Tools exposed (9 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: Frag-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 FragmentPay({…}).webhooks.verify(body, header, secret)— same timing-safe compare and 5-minute skew window.
Connect from Claude Desktop / Cursor:
{
"mcpServers": {
"frag": { "url": "https://<your-frag-host>/mcp" }
}
}Your agent walks through OAuth once, approves access on the Frag 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 Frag in 60 seconds.
postman-collection.json and hit every REST endpoint with your test key. Bruno reads the same v2.1 schema.npm install @fragmentpay/server.<FragmentPayCheckout /> and hooks.// connect Frag to Claude Desktop
{
"mcpServers": {
"frag": { "url": "https://frag.dev/mcp" }
}
}// connect Frag to Cursor
{
"mcpServers": {
"frag": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://frag.dev/mcp"]
}
}
}Full install manifest at /.well-known/mcp-install.json. First call triggers OAuth in your browser — approve on the Frag consent screen and the agent inherits your merchant scope via RLS.
What Frag guarantees
- Non-custodial by default — funds route wallet → aggregator → merchant, never a Frag 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.