Sera Sera for Agents / Docs / Tutorials / Prediction market
Tutorial 03

Prediction market on FX.

Binary markets like "USD/JPY > 158 by Friday close" using Sera's reference rate as the settlement oracle. Markets settle in stablecoins; payouts route through Sera FX so winners can withdraw in whatever currency they want.

60 min Solidity Foundry / Hardhat sera.get_fx_rate
What you'll have at the end

A deployed binary-market contract on Ethereum that resolves against Sera's reference FX rate, plus an off-chain "resolver" service that posts the final rate at expiry.

Honest framing

Prediction markets are regulated in most jurisdictions. This tutorial shows the technical pattern. Treat it as infrastructure for a permitted product (internal employee market, on-chain airdrop game, sanctioned jurisdiction), not a public consumer offering without legal review.

Architecture

┌──────────────┐    ┌────────────────┐    ┌─────────────────┐
│  User funds  │───▶│  Market contract│   │  Resolver svc   │
│  YES or NO   │    │  (escrows USDC)│    │  (off-chain)    │
└──────────────┘    └────────┬───────┘    └────────┬────────┘
                             │                     │
                             │   At expiry:        │
                             │   ┌─────────────────┘
                             │   │
                             │   ▼
                             │  sera.get_fx_rate({USD,JPY})
                             │   │
                             │   ▼
                             │  resolver.postRate(rate, sig)
                             ▼
                    ┌─────────────────┐
                    │  Market resolves│
                    │  Winners claim  │
                    │  USDC → Sera    │
                    │  → any currency │
                    └─────────────────┘

Steps

1The market contract (minimal)

contracts/FxBinaryMarket.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract FxBinaryMarket {
    IERC20 public immutable usdc;
    address public immutable resolver;
    string public question;       // "USD/JPY > 158 at 2026-05-20T16:00Z"
    uint256 public immutable threshold;
    uint256 public immutable expiry;
    enum Outcome { Pending, Yes, No }
    Outcome public outcome;

    mapping(address => uint256) public yesShares;
    mapping(address => uint256) public noShares;
    uint256 public totalYes;
    uint256 public totalNo;

    function bet(bool yes, uint256 amount) external {
        require(block.timestamp < expiry, "closed");
        usdc.transferFrom(msg.sender, address(this), amount);
        if (yes) { yesShares[msg.sender] += amount; totalYes += amount; }
        else     { noShares[msg.sender]  += amount; totalNo  += amount; }
    }

    function resolve(uint256 finalRate) external {
        require(msg.sender == resolver, "!resolver");
        require(block.timestamp >= expiry, "early");
        require(outcome == Outcome.Pending, "resolved");
        outcome = finalRate > threshold ? Outcome.Yes : Outcome.No;
    }

    function claim() external {
        require(outcome != Outcome.Pending, "pending");
        uint256 total = totalYes + totalNo;
        uint256 winning = outcome == Outcome.Yes ? yesShares[msg.sender] : noShares[msg.sender];
        uint256 pool = outcome == Outcome.Yes ? totalYes : totalNo;
        uint256 payout = (winning * total) / pool;
        if (outcome == Outcome.Yes) yesShares[msg.sender] = 0;
        else noShares[msg.sender] = 0;
        usdc.transfer(msg.sender, payout);
    }
}

2The off-chain resolver

A small Node service that wakes at expiry, calls sera.get_fx_rate, and posts the result.

resolver/index.ts
import { spawnSeraMcp } from './mcpClient';
import { ethers } from 'ethers';

const sera   = spawnSeraMcp({ env: { SERA_NETWORK: 'mainnet' } });
const wallet = new ethers.Wallet(process.env.RESOLVER_KEY!, provider);
const market = new ethers.Contract(MARKET_ADDR, ABI, wallet);

async function resolveAtExpiry() {
  const expiry = await market.expiry();
  const now    = Math.floor(Date.now() / 1000);
  if (now < expiry) return;

  const r = await sera.call('get_fx_rate', { base: 'USD', quote: 'JPY' });
  const finalRate = ethers.parseUnits(r.rate.toString(), 8);
  await market.resolve(finalRate);
}

3(Better) Median over a resolution window

A single rate read at expiry is manipulable. In production, average over a resolution window (e.g. last 30 min before expiry) using sera.fx_history + multi_source_mid as a sanity check.

const series = await sera.call('fx_history', {
  pair: 'USD/JPY', hours: 0.5
});
const median = series.map(s => s.rate).sort()[Math.floor(series.length / 2)];

const ext = await sera.call('multi_source_mid', { base: 'USD', quote: 'JPY' });
if (Math.abs(median - ext.median) / ext.median > 0.005) {
  // More than 50bps disagreement — pause resolution, alert humans.
  throw new Error('Sera vs external mid disagree');
}

4Multi-currency payouts

Winners claim USDC from the contract. To let them withdraw in MYR, JPYC, or whatever, point them to a Sera-powered swap UI right after they claim. Use the payment widget pattern with recipient = winner_address.

Common gotchas

Next: Treasury rebalancer uses Sera as state-mutation infra rather than oracle, or x402-paid API shows how to monetize the resolver itself.