1
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?

microCMS x Next.jsで予測市場を開発する ~ ポストAI時代の新しいメディア~

Last updated at Posted at 2025-12-30

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

image.png

はじめに

こんな人におすすめ

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

自己紹介

Web3エンジニア・起業家。複数社でCTOやJapan Headを歴任しました。
主な実績として、読売テレビおよびNETFLIXと連携した「Tomie by Junji Ito」NFT開発(約5000万円が2分で完売)、SonyやSony銀行のブロックチェーンR&D、Bunzzのシードラウンド6億円資金調達への貢献など。
現在は、予測市場の開発・事業化に取り組んでいます。

予測市場とは

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

下記に予測市場の参考資料一覧をまとめています。
予測市場の参考資料(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を活用した予測市場の設計アプローチを紹介します。

Headless CMSとオンチェーンの責務分離という設計思想

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

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

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

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

ご指摘ありがとうございます。実際のmicroCMS APIスキーマに合わせて、「Headless CMSのMarket API設計」セクションと「Next.jsで定義するInterface」セクションを修正します。

データスキーマ設計

予測市場におけるMarketのデータ構造を、Solidity Smart Contract、Headless CMS(microCMS)、Next.jsのそれぞれで定義します。
各レイヤーの責務を明確に分離しつつ、型安全性を保ちながら統合する設計を示します。
この設計により、オンチェーンの信頼性とHeadless CMSの柔軟性を両立させた予測市場アプリケーションを実現できます。

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 によるステータス管理を導入することで、不正な状態遷移や操作を防止する仕組みを実装しています。
さらにOracle連携を組み込むことで、外部の信頼できる情報源から結果を取得し、公平かつ透明性の高い決済プロセスを実現しています。

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はオプショナルフィールドとして定義されており、未設定の場合はデフォルト画像を使用する想定です。

image.png
image.png

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フレンドリーなルーティングを実現しています。

ロードマップ

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

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

特に後者のアプローチであれば、Polymarket Builders Program(開発者支援の助成金)に申請できると考えています。

おわりに: microCMSの方へ

予測市場はmicroCMSのグロースにおいても有力なユースケースになり得ると考えています。
特に、既存の予測市場データを取得し、メディアとして再構成・配信すること自体に問題はありません。
(※自社で予測市場を開発・運営することは日本ではまだ法規制のハードルがあります)

Post Truthと呼ばれる時代において、言葉ではなく「身銭を切った意思」が集約される予測市場は、新しいメディアとして信頼を獲得しつつあります。
その入口となるUI・フロントエンド構築において、Headless CMSは相性が良いと感じています。
もしご興味があれば、協業の可能性も含めて、ぜひ一度カジュアルにお話しできれば嬉しいです。

Tech Stack

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

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

参考資料

1
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
1
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?