// documentation

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.

// 00 overview

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.
// 01 quickstart

Ship in under 10 minutes

Install both packages.

terminal
# server + react
npm install @fragmentpay/server @fragmentpay/react

# or with your favorite tool
pnpm add @fragmentpay/server @fragmentpay/react
bun add @fragmentpay/server @fragmentpay/react

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

server/checkout.ts
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 };
app/Checkout.tsx
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>
  );
}
// 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
FRAG_SECRET_KEY=sk_test_...        # server only
FRAG_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

@fragmentpay/server

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

server/checkout.ts
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.

createfrag.paymentIntents.create(input)
Create a new intent
retrievefrag.paymentIntents.retrieve(id)
Fetch current state
listfrag.paymentIntents.list({ limit })
List recent intents
cancelfrag.paymentIntents.cancel(id)
Cancel a pending intent
verifyfrag.webhooks.verify(body, sig, sec)
Verify webhook signature
parsefrag.webhooks.parse(body)
Parse a verified event
// 04 react SDK

@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

terminal
npm install @fragmentpay/server @fragmentpay/react
# or: bun add @fragmentpay/server @fragmentpay/react

2 · env vars

.env.local
# .env.local
NEXT_PUBLIC_FRAG_PUBLIC_KEY=pk_test_...   # browser-safe
FRAG_SECRET_KEY=sk_test_...               # server only
MERCHANT_USDC_ADDRESS=0xYourBaseWallet

3 · wrap your app once

app/providers.tsx
// 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
// 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
// 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:

app/Checkout.tsx
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?

redirect.tsx
<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:

hook.ts
import { useFragmentIntent } from "@fragmentpay/react";

const { data, status } = useFragmentIntent(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://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" }
  }'
retrieve
curl https://api.frag.dev/api/public/v1/payment-intents/pi_123 \
  -H "Authorization: Bearer $FRAG_SECRET_KEY"
list
curl "https://api.frag.dev/api/public/v1/payment-intents?limit=20&status=settled" \
  -H "Authorization: Bearer $FRAG_SECRET_KEY"
cancel
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.

// 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 Frag 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. Frag 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": "solana",
    "settlement_token": "USDC",
    "metadata": { "orderId": "order_8841" }
  }
}
header
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 skew

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

app/api/webhook.ts
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");
}
payment_intent.created
Intent created.
payment_intent.quoted
Route locked, waiting on customer signature.
payment_intent.executing
On-chain execution in progress.
payment_intent.settled
Funds arrived at merchant wallet. Fulfil order.
payment_intent.failed
Route or on-chain execution failed. Nothing to reconcile.
payment_intent.expired
Quote window closed. Ask customer to retry.
payment_intent.cancelled
Merchant or customer cancelled the intent.
payment_intent.refunded
Merchant issued a refund.
// 08 errors

Typed, machine-readable

error-handling.ts
// 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;
}
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 frag = new FragmentPay({ apiKey: "sk_test_..." });
// 10 lifecycle

Status transitions

created
  → quoted
    → awaiting_signature
      → executing
        → settled
              ↘ failed | expired | cancelled | refunded
// 11 support

Chains & 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:

EVM
USDC / USDT / ETH / native on Ethereum, Base, Arbitrum, Optimism, Polygon, BNB. Cross-chain via LI.FI + Circle CCTP where available.
Solana
USDC / SOL and any SPL token routed through Jupiter with slippage bounds.
// 12 agents

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.

Endpoint
https://<your-frag-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 (9 total):

toollist_merchants
List merchant accounts you own
toollist_supported_tokens
Frag 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
toolcreate_payout
Enqueue payout for a settled intent
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:  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| > 300s

The 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:

~/.claude/mcp.json
{
  "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.

// 13 tools

Tools, examples & one-click install

Everything you need to try Frag in 60 seconds.

// connect Frag to Claude Desktop

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

// connect Frag to Cursor

~/.cursor/mcp.json
{
  "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.

// 14 security

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.
Need help? Reach us at hello@frag.dev.