API

Rewards

Pay a verified identity from your Reward Vault — game prizes, bounties, creator payouts, campaign rewards. No wallet collection, and the idempotency key makes every payout exactly-once.

POST/v1/rewardsscope rewards:create
import { FomoPay } from "@fomopay/sdk";

const fomopay = new FomoPay({ apiKey: process.env.FOMOPAY_API_KEY });

// When a FomoRun round ends. No wallet collection: the handle is enough.
const winner = match.winner.handle; // "@lyra"

await fomopay.rewards.create({
  recipient: winner,
  amount: "500",
  asset: "USDG",
  reason: `FomoRun #${round.id}`,
  // same round → same payout, however often this code runs
  idempotencyKey: `FOMORUN_${round.id}_WINNER`,
});
201 Created (abridged) · the full payment object is on Payments
{
  "object": "payment",
  "payment_id": "pay_8k2m4q7r9t1v3x5z6b0c",
  "status": "CREATED",
  "type": "REWARD",
  "source": "vault",
  "recipient": { "handle": "@lyra", "display_name": "Lyra Okoye", "sandbox": false },
  "amount": "500",
  "asset": "USDG",
  "network": "robinhood",
  "reason": "FomoRun #42",
  "idempotency_key": "FOMORUN_42_WINNER",
  "idempotent_replayed": false,
  "tx_hash": null,
  "receipt_url": "https://fomopay.family/payment/pay_8k2m4q7r9t1v3x5z6b0c"
}

Body

recipientstringrequired
The Fomo handle: @lyra. Must be VERIFIED on Robinhood Chain.
amountdecimal stringrequired
USDG, up to 6 decimals: "500", "12.5".
idempotency_keystringrequired
1–255 printable characters, no spaces. Derive it from your event — FOMORUN_42_WINNER, BOUNTY_8812. May be sent as the Idempotency-Key header instead (if both are sent they must match).
asset"USDG"optional
Reward Vaults hold USDG; this is the default. ETH has no ERC-20 allowance — pay ETH with a wallet payment.
reasonstringoptional
Shown on the receipt: FomoRun #42 Winner.
memostringoptional
A note for the recipient.
vault_idstringoptional
Which vault pays. Optional when the project has exactly one active USDG vault.
metadataobjectoptional
Your references (round id, bounty id). Returned on the payment and in every webhook.
eligibilityobjectoptional
Your own eligibility assertions (region, age declaration, campaign). Stored with the payout as you sent them — FomoPay doesn't invent eligibility.
visibilityprivate | unlisted | publicoptional
Receipt visibility. Projects may publish reward receipts; default unlisted.

What happens

  1. Admission. The vault must be ACTIVE, the amount within its per-payout and daily limits, and covered by both its balance and its remaining allowance. A rejected reward never becomes a payment.
  2. Payment. @lyra is resolved, the verified destination snapshotted and the risk engine consulted, exactly like any payment. You get the payment back (201) immediately.
  3. Execution. FomoPay's payout signer sends transferFrom(vault, @lyra's wallet, amount) on Robinhood Chain, paying gas in ETH itself. Payouts are serialized per signer so nonces never collide.
  4. Confirmation. The payment moves SUBMITTED → CONFIRMING → CONFIRMED, a receipt is issued and payment.confirmed is delivered to your webhooks.

Errors you should handle

CodeStatusWhat to do
DUPLICATE_PAYMENT409This key was used with a different body. Don't retry — it's a bug or a genuinely different payout that needs its own key.
RECIPIENT_UNVERIFIED · NO_PAYOUT_DESTINATION422Ask the winner to make their handle FomoPay Ready (/claim), then retry with the same key.
VAULT_INSUFFICIENT422Top up the vault wallet with USDG.
VAULT_ALLOWANCE_EXCEEDED422Raise the approved allowance from the vault wallet.
VAULT_NOT_ACTIVE409Finish setup, unpause, or re-approve the vault.
PAYMENT_LIMIT_EXCEEDED422Over a per-payout, daily, project or recipient limit.
UNSUPPORTED_ASSET422Vaults pay USDG. There is no official USDC on Robinhood Chain.

A reward can also come back with status: "HELD" (for example the winner changed their payout wallet within the cooldown) or PENDING_APPROVAL (over your approval threshold). Neither is an error: the payment exists and executes once released. All codes: Errors.

Bounties

Project approves the bounty → FomoPay resolves the winner's verified identity → the payout executes → the receipt is attached to the bounty. The key is the bounty id, so approving twice can't pay twice.

bounty.ts
// The bounty board marks a submission as approved → pay the winner by handle, once.
export async function payBounty(bounty: { id: string; title: string; rewardUsdg: string }, winnerHandle: string) {
  const payment = await fomopay.rewards.create({
    recipient: winnerHandle, // "@lyra"
    amount: bounty.rewardUsdg, // "750"
    asset: "USDG",
    reason: `Bounty: ${bounty.title}`,
    idempotencyKey: `BOUNTY_${bounty.id}`,
    metadata: { bounty_id: bounty.id },
    visibility: "public", // a public receipt the bounty page can link to
  });

  await db.bounties.update(bounty.id, { paymentId: payment.payment_id, receiptUrl: payment.receipt_url });
  return payment;
}
webhook handler
// payment.confirmed → mark the bounty paid (and only then)
if (event.type === "payment.confirmed" && event.data.metadata.bounty_id) {
  await db.bounties.update(event.data.metadata.bounty_id, { status: "PAID", tx: event.data.tx });
}

send is a reward

fomopay.send() — the one-liner — is rewards.create with an idempotency key generated for you, which keeps the SDK's own network retries safe. For exactly-once across restarts and redeploys, pass idempotencyKey yourself.

TypeScript
// These two calls do the same thing:
await fomopay.send({ to: "@lyra", amount: 100, asset: "USDG" });
await fomopay.rewards.create({ recipient: "@lyra", amount: "100", asset: "USDG", idempotencyKey: crypto.randomUUID() });
Weekly leaderboards and referrals

Paying many people at once? Use bulk payouts: every row is validated first and invalid handles are blocked before anything moves.