10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

生成AIと並び、いま世界で最も競争の激しい領域が予測市場です。PolymarketやKalshiの創業者が史上最年少ビリオネアとなったことでも話題となりました。
本記事では、microCMSとNext.jsを活用した予測市場の設計を紹介します。

image.png
image.png
image.png

はじめに

こんな人におすすめ

  • Web3プロダクトの開発者: 予測市場やDeFiプロダクトのフロントエンド開発に関わる方
  • メディア運営者・マーケター: オウンドメディアやSEOメディアの次の戦略を考えている方
  • スタートアップ・新規事業担当者: 予測市場事業の立ち上げを検討している方

予測市場とは

予測市場とは、将来起こる事象に対して「どうなるか」を参加者が資金を賭けて予測する仕組みです。
選挙結果やスポーツの勝敗、価格の動向などを対象に、人々の判断が価格として集約されます。
正しい予測をした参加者は報酬を得られ、外れれば損をするため、発言ではなく実際の信念が反映されます。
この性質により、予測市場はアンケートや評論よりも高い情報集約能力を持つと評価されており、集合知を使った意思決定支援や新しい情報メディアとして注目されています。

下記に予測市場の参考資料一覧をまとめています。
予測市場の参考資料(Prediction Market, Polymarket, Kalshi)

microcmsとは

microCMSはAPIベースの日本製ヘッドレスCMSです。
コンテンツのためのサーバー管理は一切不要、サインアップするだけですぐに利用開始できます。
JAMStackアーキテクチャやNext.jsなどのモダンな技術スタックが前提となっており、リリース当初から国内のエンジニアから強く支持されてきました。
ContenfulやGhost, Netlify CMSなどHeadless CMSは一通り触ってきたのですが、国産プロダクトであることを差し引いてもmicroCMSは一番使いやすかったです。

microCMS x Next.js開発については下記書籍もおすすめです。
『Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門』

さて、やっと次から本題です。

予測市場にHeadless CMSを活用するメリット

予測市場はAMM設計や流動性管理、Oracle連携などを含む、高度なWeb3技術の集大成です。
同時に、Yes / Noに賭けるだけというシンプルな体験によって、最も大衆化に成功したWeb3アプリケーションでもあります。
エンドユーザーから見れば難しい技術は一切不要で、究極的にシンプル化されています。
興味のあるテーマに対してYes or Noを選び、1ドルからBetするだけ。

ここで重要になるのが、テーマごとのタイトルや説明文、サムネイルなどの最適化です。
これらはユーザー参加に直結する要素であり、理想的にはエンジニアを介さず、Bizやマーケターが高速に改善できるべきです。

そこで注目したのがHeadless CMSです。
本記事では、microCMSを活用した予測市場の設計アプローチを紹介します。

ロードマップ

Next.jsテンプレート v1

まずは、Next.jsのテンプレートとして本構成を実装し、microCMSのテンプレートとして申請する予定です。予測市場向けのスキーマ設計や責務分離の考え方を、そのまま再利用できる形にまとめます。
本記事で扱う内容もこちらです。

Next.jsテンプレート v2

当初はインハウスで予測市場プロトコルを開発するケースを想定していましたが、一方でサードパーティの予測市場APIを使ったメディア構築も有力なユースケースだと考えています。例えばPolymarketやKalshiなどからマーケットデータを取得し、microCMSで解説記事を編集・配信するような形です。

特に後者のアプローチであれば、Polymarket Builders Program(開発者支援の助成金)のようなインキュベーションプログラムにも申請しやすいです。

AI Agent x microCMS MCP

AI Agentとの親和性も高く、さまざまなユースケースが可能です。
例えば、サードパーティの予測市場からデータを取得して、AI Agentに解説記事を作成させるのは有望です。
microCMS MCP Serverが公開されているので、AI Agentに直接microCMSのコンテンツ作成を任せることができます。

あるいは記事に対して、該当する予測市場からのデータに基づき、記事のレビューを依頼することも可能です。
例えばニュースサイトが「2025年度ノーベル平和賞の候補者」についての記事を執筆する際、Polymarketから各候補者の確率を取得することが可能です。

Headless CMSとオンチェーンの責務分離の考え方

予測市場で扱うデータを2種類に大別して考えます。

1つ目は、Betに関わる定量データです。
プロトコル全体のTVLや、各MarketにおけるYes / Noそれぞれのデポジット総額、現在のオッズなどです。
これらはオンチェーンからリアルタイムで取得し、正確性と速度の両立が求められます。
Solidity Smart Contractや各種Web3 APIからフェッチします。

2つ目は、ユーザー体験を左右する定性的データです。
トップページで表示するFeaturedテーマ、カテゴリ構成、Marketごとのタイトルや説明文、サムネイル画像などがこれに当たります。
Headless CMS(microCMS)で管理します。

この責務分離によって、トレーダー体験や信頼性を保ったまま、表現や導線だけを柔軟に改善できるようになります。

データスキーマ設計

予測市場におけるMarketのデータ構造を、Solidity Smart Contract、Headless CMS(microCMS)、Next.jsのそれぞれで定義します。
各レイヤーの責務を明確に分離しつつ、型安全性を保ちながら統合する設計を示します。

Solidity Smart Contractで定義するMarket Interface

オンチェーンで管理する定量データのみを扱います。
Betの実行、決済、資金管理など、信頼性が最優先される部分です。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IMarket {
    enum MarketStatus {
        Active,      // Bet受付中
        Locked,      // 結果確定待ち
        Resolved,    // 決済完了
        Cancelled    // キャンセル
    }

    struct Market {
        uint256 marketId;           // Market ID
        MarketStatus status;        // 現在のステータス
        uint256 yesPool;            // Yesへの総Bet額(wei)
        uint256 noPool;             // Noへの総Bet額(wei)
        uint256 totalVolume;        // 累計取引高
        uint256 createdAt;          // 作成タイムスタンプ
        uint256 endTime;            // Bet締切時刻
        uint256 resolutionTime;     // 決済予定時刻
        address oracle;             // 結果を確定するOracle
        bool outcome;               // 最終結果(true = Yes, false = No)
    }

    function getMarket(uint256 marketId) external view returns (Market memory);
    function getCurrentOdds(uint256 marketId) external view returns (uint256 yesOdds, uint256 noOdds);
    function placeBet(uint256 marketId, bool betOnYes, uint256 amount) external payable;
    function resolveMarket(uint256 marketId, bool outcome) external;
    function claimRewards(uint256 marketId) external;
}

このインターフェースでは金額は全てwei単位で管理することで小数点以下の精度を保証し、計算誤差を防いでいます。
また MarketStatus enum によるステータス管理を導入することで、不正な状態遷移や操作を防止する仕組みを実装しています。

Headless CMSのMarket API設計

ユーザー体験を左右する定性データを管理します。
microCMSの管理画面から、エンジニアを介さずに編集可能です。

microCMSのAPIスキーマ定義(リスト形式)

{
  "apiFields": [
    {
      "fieldId": "title",
      "name": "タイトル",
      "kind": "text",
      "required": true,
      "description": "Market名として表示されるタイトル",
      "textSizeLimitValidation": {
        "max": 100
      }
    },
    {
      "fieldId": "slug",
      "name": "スラッグ",
      "kind": "text",
      "required": true,
      "description": "URL用のユニークなスラッグ(例: trump-2024-election)",
      "isUnique": true
    },
    {
      "fieldId": "description",
      "name": "説明文",
      "kind": "textArea",
      "required": true,
      "description": "Marketの詳細説明",
      "textSizeLimitValidation": {
        "max": 500
      }
    },
    {
      "fieldId": "thumbnail",
      "name": "サムネイル画像",
      "kind": "media",
      "required": false,
      "description": "推奨サイズ: 1200x630px"
    },
    {
      "fieldId": "category",
      "name": "カテゴリ",
      "kind": "text",
      "required": true,
      "description": "カテゴリーのslug(例: politics, crypto)"
    },
    {
      "fieldId": "status",
      "name": "ステータス",
      "kind": "select",
      "required": true,
      "selectItems": [
        { "value": "draft", "label": "下書き" },
        { "value": "active", "label": "公開中" },
        { "value": "archived", "label": "アーカイブ" }
      ],
      "description": "CMS上での公開ステータス(Smart Contractのstatusとは別管理)"
    },
    {
      "fieldId": "endDate",
      "name": "終了日時",
      "kind": "date",
      "required": true,
      "description": "Bet締切日時(ISO 8601形式)"
    },
    {
      "fieldId": "marketId",
      "name": "Market ID",
      "kind": "text",
      "required": true,
      "description": "Smart ContractのMarket IDと対応する外部ID",
      "isUnique": true
    },
    {
      "fieldId": "featured",
      "name": "Featured表示",
      "kind": "boolean",
      "description": "トップページで優先表示するかどうか"
    }
  ]
}

型定義(TypeScript)

export type Market = {
  title: string;
  slug: string;
  description: string;
  thumbnail?: MicroCMSImage;
  category: string; // カテゴリーのslug(例: "politics", "crypto")
  status: 'draft' | 'active' | 'archived';
  endDate: string; // ISO 8601形式
  marketId: string; // 外部マーケットID(Smart ContractのID)
  featured?: boolean;
};

このスキーマ設計では、marketId(文字列型)によってSmart Contractとの紐付けを行います。
slugフィールドを用いることで、SEOフレンドリーなURL生成が可能になります。
statusフィールドはCMS上での公開状態を管理するためのもので、オンチェーン上のstatusとは独立して運用されます。
categoryは文字列型で柔軟に管理し、カテゴリのマスタデータは別途定義します。
featuredフラグを設定することで、トップページでの優先表示を制御できます。
また、thumbnailはオプショナルフィールドとして定義されており、未設定の場合はデフォルト画像を使用する想定です。

Next.jsで定義するInterface

フロントエンドでは、オンチェーンデータとCMSデータを統合した型を定義します。
型安全性を保ちながら、両方のデータソースを扱います。

// types/market.ts
import type { MicroCMSImage } from 'microcms-js-sdk';

/**
 * Smart Contractから取得するオンチェーンデータ
 */
export interface OnChainMarketData {
  marketId: bigint;
  status: 'Active' | 'Locked' | 'Resolved' | 'Cancelled';
  yesPool: bigint;        // wei
  noPool: bigint;         // wei
  totalVolume: bigint;    // wei
  createdAt: number;      // Unix timestamp
  endTime: number;        // Unix timestamp
  resolutionTime: number; // Unix timestamp
  oracle: `0x${string}`;
  outcome?: boolean;
}

/**
 * microCMSから取得するCMSデータ
 */
export interface CMSMarketData {
  id: string;             // microCMSの内部ID
  title: string;
  slug: string;
  description: string;
  thumbnail?: MicroCMSImage;
  category: string;
  status: 'draft' | 'active' | 'archived';
  endDate: string;        // ISO 8601
  marketId: string;       // Smart ContractのMarket ID(文字列)
  featured?: boolean;
  createdAt: string;      // ISO 8601
  updatedAt: string;      // ISO 8601
  publishedAt?: string;   // ISO 8601
  revisedAt?: string;     // ISO 8601
}

/**
 * フロントエンドで使用する統合されたMarket型
 */
export interface UnifiedMarket {
  // 識別情報
  id: string;             // microCMSの内部ID
  marketId: string;       // Smart ContractのMarket ID
  slug: string;           // URL用スラッグ
  
  // CMSデータ(定性情報)
  title: string;
  description: string;
  thumbnail?: MicroCMSImage;
  category: string;
  featured: boolean;
  cmsStatus: 'draft' | 'active' | 'archived';
  
  // オンチェーンデータ(定量情報)
  contractStatus: 'Active' | 'Locked' | 'Resolved' | 'Cancelled';
  yesPoolEth: string;     // ETH単位(表示用)
  noPoolEth: string;      // ETH単位(表示用)
  totalVolumeEth: string; // ETH単位(表示用)
  yesOdds: number;        // パーセンテージ(0-100)
  noOdds: number;         // パーセンテージ(0-100)
  endTime: Date;
  resolutionTime: Date;
  outcome?: boolean;
  
  // メタ情報
  createdAt: Date;
  updatedAt: Date;
}

/**
 * オンチェーンデータとCMSデータを統合する関数
 */
export function mergeMarketData(
  onChainData: OnChainMarketData,
  cmsData: CMSMarketData
): UnifiedMarket {
  // marketIdの整合性チェック
  if (BigInt(cmsData.marketId) !== onChainData.marketId) {
    throw new Error(
      `Market ID mismatch: CMS=${cmsData.marketId}, OnChain=${onChainData.marketId}`
    );
  }

  // オッズ計算
  const totalPool = onChainData.yesPool + onChainData.noPool;
  const yesOdds = totalPool > 0n 
    ? Number((onChainData.yesPool * 10000n) / totalPool) / 100 
    : 50;
  const noOdds = 100 - yesOdds;

  return {
    // 識別情報
    id: cmsData.id,
    marketId: cmsData.marketId,
    slug: cmsData.slug,
    
    // CMSデータ
    title: cmsData.title,
    description: cmsData.description,
    thumbnail: cmsData.thumbnail,
    category: cmsData.category,
    featured: cmsData.featured || false,
    cmsStatus: cmsData.status,
    
    // オンチェーンデータ(表示用に変換)
    contractStatus: onChainData.status,
    yesPoolEth: (Number(onChainData.yesPool) / 1e18).toFixed(4),
    noPoolEth: (Number(onChainData.noPool) / 1e18).toFixed(4),
    totalVolumeEth: (Number(onChainData.totalVolume) / 1e18).toFixed(4),
    yesOdds,
    noOdds,
    endTime: new Date(onChainData.endTime * 1000),
    resolutionTime: new Date(onChainData.resolutionTime * 1000),
    outcome: onChainData.outcome,
    
    // メタ情報
    createdAt: new Date(onChainData.createdAt * 1000),
    updatedAt: new Date(cmsData.updatedAt),
  };
}

/**
 * 表示可能なMarketかどうかを判定
 */
export function isDisplayableMarket(market: UnifiedMarket): boolean {
  // CMS上で公開中、かつSmart Contract上でActive/Locked/Resolvedのいずれか
  return (
    market.cmsStatus === 'active' &&
    ['Active', 'Locked', 'Resolved'].includes(market.contractStatus)
  );
}

/**
 * Featured Marketとして表示可能かどうかを判定
 */
export function isFeaturedMarket(market: UnifiedMarket): boolean {
  return isDisplayableMarket(market) && market.featured === true;
}

このインターフェース設計では、3つの型を明確に分離しています(OnChainMarketData, CMSMarketData, UnifiedMarket)。
UnifiedMarket型がUIレイヤーで使用する最終的な型として機能します。
cmsStatuscontractStatusを別々に管理することで、CMS上とオンチェーン上の両方の状態を把握できるようにしています。
mergeMarketData関数では両データソースを統合するだけでなく、marketIDの整合性チェックも実施します。
BigIntから数値への変換、weiからETHへの単位変換、timestampからDateオブジェクトへの変換など、データ変換処理を一箇所に集約することで保守性を高めています。
さらに、isDisplayableMarketisFeaturedMarketなどのヘルパー関数を用意し、ビジネスロジックを型レイヤーに集約することで、コンポーネント側のロジックをシンプルに保ちます。
また、slugを活用することで、/market/trump-2024-electionのようなSEOフレンドリーなルーティングを実現しています。

Tech Stack

オーソドックスな技術構成です。
AIツールとしてはClaude Codeをメインで使用しています。

  • TypeScript
  • Solidity
  • Next.js
  • Taildwind, Shadcn
  • Vercel, VSCode, GitHub
  • microCMS
  • RedStone Oracle
  • Claude Code, Codex, Anti Gravity

参考資料

10
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?