Testnet: Monad Testnet, test USDC only, unaudited
Open app
On this page You need
All guide pages

Use the SDK

Reference

@fairpay/sdk gives you public and private payments plus merchant-side verification. These examples come from the SDK's README. Use the addresses from Contract addresses for Monad Testnet.

InstallLink to this section

Until it is on npm, install the packed tarball.

bash
cd packages/sdk && npm run build && npm pack
npm install /path/to/fairpay-sdk-0.1.0.tgz viem

Set up clientsLink to this section

TypeScript
import {
  createPublicClient, createWalletClient, custom, http,
} from "viem"
import { monadTestnet } from "viem/chains"
import * as fairpay from "@fairpay/sdk"

const deployment: fairpay.FairPayDeployment = {
  chainId: 10143,
  token: "0x…",        // USDC (or MockUSDC on testnet)
  fairPay: "0x…",
  pool: "0x…",
  poolDeployBlock: 0n  // block the pool was deployed in
}
const publicClient = createPublicClient({
  chain: monadTestnet,
  transport: http(),
})
const walletClient = createWalletClient({
  chain: monadTestnet,
  transport: custom(window.ethereum),
  account,
})
const clients = { publicClient, walletClient }

Every function that touches the chain checks the chain id first and throws WRONG_CHAIN otherwise.

1. Backend: create the share referenceLink to this section

TypeScript · backend
const secret = fairpay.randomSecret() // store with the participant row
const orderRef = fairpay.createOrderRef({
  orderId: "FE-001",
  participantId: "p2",
  secret,
})

2a. Browser: pay publiclyLink to this section

TypeScript · browser
const { txHash } = await fairpay.payPublic(deployment, clients, {
  merchant,
  amount: 12_500_000n,
  orderRef,
})
// send txHash to your backend for verification

payPublic approves only the exact amount, never an unlimited allowance.

2b. Browser: pay privatelyLink to this section

TypeScript · browser
// Ahead of time (the further ahead, the better the privacy):
// top up the private balance.
const note = fairpay.createNote(deployment)
// store BEFORE depositing
localStorage.setItem("note", fairpay.serializeNote(note))
await fairpay.depositNote(deployment, clients, note)

// At checkout: prove and hand the payment to the relayer.
// Takes a few seconds.
const request = await fairpay.createPrivatePayment(
  deployment,
  publicClient,
  { note, merchant, orderRef },
)
const { txHash } = await fairpay.submitToRelayer("/api/relay", request)

Privacy: a share must be a multiple of the note size. Paying a larger share with several notes sent together can link them. localStorage is only an example: anything that can read that storage can spend the notes. Offer users a backup.

3. Backend: the relayer routeLink to this section

TypeScript · backend
import { createRelayer } from "@fairpay/sdk/relayer"

const relayer = createRelayer(deployment, {
  publicClient,
  walletClient: relayerWallet,
})

export async function POST(req: Request) {
  try {
    return Response.json(await relayer.handle(await req.json()))
  } catch (e) {
    console.error(e)
    return Response.json(
      { error: "payment could not be submitted" },
      { status: 400 },
    )
  }
}

Put rate limiting and your own checks in front of it (is this an open order of yours, does it still owe money?). The relayer pays gas for every request it sends. Map errors to fixed messages: raw errors can contain your RPC URL and the relayer's balance. apps/faireats/server/app.ts is a complete example.

4. Backend: verify before releasing the orderLink to this section

TypeScript · backend
const result = await fairpay.verifyPaymentTx(
  deployment,
  publicClient,
  {
    txHash, orderRef, merchant,
    amount: share.amount,
    expectedPayer: share.wallet, // checked for public payments only
    minConfirmations: 2n,        // default 1
  },
)
if (result.status === "verified") {
  // INSERT with UNIQUE (chain_id, tx_hash, log_index)
  // so one payment can never settle two shares
  await db.payments.insert({ ...result.payment, shareId })
}

result.status is verified, pending or rejected (with a reason). Never mark a share paid from a client-side "success" message.

API at a glanceLink to this section

SDK functions
FunctionSideWhat it does
randomSecret(), createOrderRef({ orderId, participantId, secret })backendMake a share reference.
payPublic(deployment, clients, { merchant, amount, orderRef })browserApprove the exact amount, then FairPay.directPay.
createNote(deployment), serializeNote(note), parseNote(text)browserCreate and store notes.
depositNote(deployment, clients, note)browserDeposit one denomination against a note.
isNoteSpent(deployment, publicClient, note)browserWhether the note's nullifier is used.
fetchGroup(deployment, publicClient, options?)browserRebuild the tree from Deposit logs; checked against the on-chain root.
groupFromCommitments(deployment, publicClient, commitments)browserRebuild the tree from an indexer's list; checked against knownRoots.
createPrivatePayment(deployment, publicClient, { note, merchant, orderRef, group? })browserBuild the proof.
submitToRelayer(url, request)browserPOST the proof to your relayer.
createRelayer(deployment, clients).handle(body)backendValidate, simulate and submit a private payment. Import from @fairpay/sdk/relayer.
verifyPaymentTx(deployment, publicClient, params)backendVerify one payment transaction.
findPayments(deployment, publicClient, { orderRef, merchant?, fromBlock })backendFind payments for a reference by scanning logs.
paymentMessage(merchant, orderRef)eitherThe proof message, keccak256(merchant, orderRef) as a field element.

ErrorsLink to this section

SDK errors are FairPayError with a code: WRONG_CHAIN, INVALID_INPUT, NO_POOL, GROUP_MISMATCH, NOTE_NOT_FOUND, NOTE_SPENT, NOT_MERCHANT, RELAYER_REJECTED, RELAYER_TIMEOUT, RELAYER_BUSY, RELAYER_UNFUNDED.