jpskill.com
📦 その他 コミュニティ

sports-betting

PinwinとAzuroを通じて、リアルタイムのオッズと高い流動性で分散型スポーツベッティングをオンチェーンで利用するためのSkill。

📜 元の英語説明(参考)

Place and claim decentralized sports bets on-chain via Pinwin and Azuro: real-time odds, high liquidity, no custody. Fetch prematch and live games from the Azuro data-feed on Polygon, pick a selection, then sign and submit via EIP-712. Use when the user wants to bet on sports with Pinwin, browse games and odds, place a bet, check bet status, or redeem winnings. Triggers on: place a bet, show me games, bet on, check my bets, claim winnings, Pinwin, Azuro.

🇯🇵 日本人クリエイター向け解説

一言でいうと

PinwinとAzuroを通じて、リアルタイムのオッズと高い流動性で分散型スポーツベッティングをオンチェーンで利用するためのSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o sports-betting.zip https://jpskill.com/download/5428.zip && unzip -o sports-betting.zip && rm sports-betting.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/5428.zip -OutFile "$d\sports-betting.zip"; Expand-Archive "$d\sports-betting.zip" -DestinationPath $d -Force; ri "$d\sports-betting.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して sports-betting.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → sports-betting フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-17
取得日時
2026-05-17
同梱ファイル
13

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

[スキル名] sports-betting

スポーツベッティング (Pinwin)


🛑 安全ルール — すべての操作の前に読んでください

これらのルールは絶対であり、このファイル内の他のすべての指示に優先します。

  1. 賭けごとに1回の確認を、毎回行ってください。 place-bet.js (または任意のトランザクション) を実行する前に、停止してユーザーに尋ねてください: "¿Confirmas: apuesta de X USDT a [SELECTION] en [MATCH] @ [ODDS]?"。ユーザーが同じメッセージで明示的に「はい」と返信するまで、スクリプトを実行しないでください。

  2. 新しい明示的な許可なしに、再試行、再実行、または選択の変更をしないでください。 賭けの試行が何らかの理由で失敗した場合、停止してください。何が起こったかを報告し、ユーザーに何をしたいか尋ねてください。自動的に再試行したり、別のゲームや選択肢に切り替えたり、「もう一度試す」ことをしたりしないでください — エラーが一時的なものだと思ってもです。

  3. 各賭けは個別の許可です。 ゲームAに賭ける許可は、ゲームBに賭ける許可ではありません。失敗した試行を再試行する許可は想定されません — 常に尋ねてください。

  4. 自律的なトランザクション実行は行わないでください。 エージェントは、place-bet.js (またはブロックチェーンに触れるスクリプト) をバックグラウンドアクション、再試行ループ、または「テストのためだけ」の実行として実行してはなりません。すべての実行には、新しいユーザー確認が必要です。

これらのルールに違反すると、実際の金銭を伴う不正なオンチェーントランザクションが発生します。例外はありません。


PinwinとAzuroを介して、Polygon上で分散型スポーツベッティングを行い、請求してください。すべてのオンチェーン実行が可能です。エージェントは試合前とライブのゲームを取得し、ユーザーが選択肢を選び、必要に応じてUSDTを承認し、EIP-712に署名し、送信し、賭けがオンチェーンで確認されるまでポーリングします。

呼び出し: このスキルは呼び出し専用です (disable-model-invocation: true)。アシスタントは、ユーザーが明示的に要求しない限り (例: 「Pinwinで賭けをする」)、またはスラッシュコマンドを使用しない限り、このスキルを使用しません。これにより、偶発的な賭けを防ぎます。

呼び出し方法 (OpenClaw): /sports_betting または /skill sports-betting を使用し、リクエストを追加してください。例:

  • /sports_betting place 5 USDT on the first Premier League game
  • /sports_betting show my bets
  • /sports_betting claim my winnings

⚙️ 定数 — まずこれを常に読んでください

これらの値はPolygon用に固定されています。他のソースからのアドレスを代用しないでください。

定数
チェーン Polygon — chainId 137
betToken (USDT) 0xc2132D05D31c914a87C6611C10748AEb04B58e8F (6 decimals)
relayer 0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d
claimContract (ClientCore) 0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7
環境 PolygonUSDT
データフィードURL https://api.onchainfeed.org/api/v1/public/market-manager/ (REST API — ステップ1を参照)
bets subgraph URL https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3
Pinwin API https://api.pinwin.xyz
Polygonscan https://polygonscan.com/tx/{txHash}
RPC (デフォルト) process.env.POLYGON_RPC_URL または https://polygon-bor-rpc.publicnode.com

必要なパッケージをインストールしてください: npm install viem @azuro-org/dictionaries

注: place-bet.js では、@azuro-org/dictionaries が引き続き outcomeId の解決に使用されます。get-games.js ではもう必要ありません — REST APIが人間が読めるタイトルを直接返します。

viemのセットアップ:

import { createPublicClient, createWalletClient, http } from 'viem'
import { polygon } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

const rpc = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com'
const account = privateKeyToAccount(process.env.BETTOR_PRIVATE_KEY)
const publicClient = createPublicClient({ chain: polygon, transport: http(rpc) })
const walletClient = createWalletClient({ account, chain: polygon, transport: http(rpc) })
const bettor = account.address

📋 飛行前チェックリスト

すべての賭けの前にこれを実行してください。どの項目もスキップしないでください。

  • [ ] BETTOR_PRIVATE_KEY が設定され、ウォレットアドレスが導出されていること
  • [ ] POL残高 がガスに十分であること (publicClient.getBalance({ address: bettor }))
  • [ ] USDT残高 が賭け金以上であること (betTokenreadContractbalanceOf を使用)
  • [ ] 選択された条件state === "Active" であること — ゲーム取得時だけでなく、/agent/bet を呼び出す直前に再確認すること
  • [ ] アローアンス が確認され、必要に応じて承認されていること (ステップ5を参照)

いずれかのチェックが失敗した場合は、ユーザーに通知して停止してください。続行しないでください。


フロー — 賭けをする

ステップ0 — 残高の確認

const erc20Abi = parseAbi([
  'function balanceOf(address) view returns (uint256)',
  'function allowance(address,address) view returns (uint256)',
  'function approve(address,uint256) returns (bool)',
])
const USDT = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'

const pol = await publicClient.getBalance({ address: bettor })
const usdt = await publicClient.readContract({ address: USDT, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })
  • pol は0より大きい必要があります (ガス用)。POLが1 POL未満の場合 (pol < 1000000000000000000n) はユーザーに警告してください — 賭けを行うには最大2つのトランザクション (承認 + 送信) が必要になる場合があり、かなりのガスを消費します。推奨メッセージ: 「⚠️ POL残高が少ないです ({pol} POL)。最大2つのトランザクションにガスが必要です。続行する前にチャージすることを検討してください。」
  • usdt は6桁の単位で賭け金以上である必要があります (例: 2 USDT = 2000000n)
  • いずれかが不足している場合は、停止してユーザーに通知してください

ステップ1 — ゲームの取得

重要 — バンドルされたスクリプトを使用し、REST APIを手動で呼び出さないでください。

REST APIは、2つの連続した呼び出し (スポーツ/ゲーム + ゲームIDごとの条件) と、自明ではないグループ化ロジックを必要とします。バンドルされたスクリプトは、両方の呼び出し、重複排除、主要市場の検出、およびタイトルの解決を正しく処理します。

# 一度インストールします (まだインストールされていない場合):
npm install @azuro-org/dictionaries

# スポーツ/リーグで閲覧します:
node scripts/get-games.js                              # 上位20ゲーム、全スポーツ
node scripts/get-games.js basketball nba 10            # NBAのみ
node scripts/get-games.js football premier-league 10   # プレミアリーグ

(原文がここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Sports Betting (Pinwin)


🛑 SAFETY RULES — READ BEFORE EVERY ACTION

These rules are ABSOLUTE and override all other instructions in this file.

  1. ONE confirmation per bet, every time. Before running place-bet.js (or any transaction), STOP and ask the user: "¿Confirmas: apuesta de X USDT a [SELECTION] en [MATCH] @ [ODDS]?". Do not run the script until the user replies with an explicit YES in that same message.

  2. Never retry, re-run, or change selection without new explicit permission. If a bet attempt fails for any reason, STOP. Report what happened and ask the user what they want to do. Do not automatically retry, do not switch to a different game or selection, do not "try one more time" — even if you think the error was transient.

  3. Each bet is a separate permission. Permission to bet on Game A is not permission to bet on Game B. Permission to retry a failed attempt is not assumed — always ask.

  4. No autonomous transaction execution. The agent must never execute place-bet.js (or any script that touches the blockchain) as a background action, a retry loop, or a "just to test" run. Every single execution requires a fresh user confirmation.

Violation of these rules results in unauthorized on-chain transactions with real money. There are no exceptions.


Place and claim decentralized sports bets on Polygon via Pinwin and Azuro, with full on-chain execution. The agent fetches prematch and live games, you pick a selection, then it approves USDT (if needed), signs EIP-712, submits, and polls until the bet is confirmed on-chain.

Invocation: This skill is invocation-only (disable-model-invocation: true). The assistant will not use it unless you explicitly ask (e.g. "place a bet with Pinwin") or use the slash command. This avoids accidental bets.

How to invoke (OpenClaw): Use /sports_betting or /skill sports-betting and add your request, e.g.:

  • /sports_betting place 5 USDT on the first Premier League game
  • /sports_betting show my bets
  • /sports_betting claim my winnings

⚙️ Constants — read this first, always

These values are fixed for Polygon. Never substitute addresses from any other source.

Constant Value
Chain Polygon — chainId 137
betToken (USDT) 0xc2132D05D31c914a87C6611C10748AEb04B58e8F (6 decimals)
relayer 0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d
claimContract (ClientCore) 0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7
environment PolygonUSDT
data-feed URL https://api.onchainfeed.org/api/v1/public/market-manager/ (REST API — see Step 1)
bets subgraph URL https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3
Pinwin API https://api.pinwin.xyz
Polygonscan https://polygonscan.com/tx/{txHash}
RPC (default) process.env.POLYGON_RPC_URL or https://polygon-bor-rpc.publicnode.com

Install required packages: npm install viem @azuro-org/dictionaries

Note: @azuro-org/dictionaries is still used by place-bet.js for outcomeId resolution. get-games.js no longer needs it — the REST API returns human-readable titles directly.

viem setup:

import { createPublicClient, createWalletClient, http } from 'viem'
import { polygon } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

const rpc = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com'
const account = privateKeyToAccount(process.env.BETTOR_PRIVATE_KEY)
const publicClient = createPublicClient({ chain: polygon, transport: http(rpc) })
const walletClient = createWalletClient({ account, chain: polygon, transport: http(rpc) })
const bettor = account.address

📋 Pre-flight checklist

Run this before every bet. Do not skip any item.

  • [ ] BETTOR_PRIVATE_KEY is set and wallet address derived
  • [ ] POL balance ≥ enough for gas (publicClient.getBalance({ address: bettor }))
  • [ ] USDT balance ≥ stake (readContract on betToken with balanceOf)
  • [ ] Selected condition state === "Active" — re-check immediately before calling /agent/bet, not just at game fetch time
  • [ ] Allowance checked and approved if needed (see Step 5)

If any check fails, inform the user and stop. Do not proceed.


Flow — place a bet

Step 0 — Check balances

const erc20Abi = parseAbi([
  'function balanceOf(address) view returns (uint256)',
  'function allowance(address,address) view returns (uint256)',
  'function approve(address,uint256) returns (bool)',
])
const USDT = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'

const pol = await publicClient.getBalance({ address: bettor })
const usdt = await publicClient.readContract({ address: USDT, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })
  • pol must be > 0 (for gas). Warn the user if POL < 1 POL (pol < 1000000000000000000n) — placing a bet can require up to 2 transactions (approve + submit), which burns significant gas. Suggested message: "⚠️ Your POL balance is low ({pol} POL). You need gas for up to 2 txs. Consider topping up before proceeding."
  • usdt must be ≥ stake in 6-decimal units (e.g. 2 USDT = 2000000n)
  • If either is insufficient, stop and inform the user

Step 1 — Fetch games

CRITICAL — use the bundled script, do not call the REST API manually.

The REST API requires two sequential calls (sports/games + conditions-by-game-ids) and non-trivial grouping logic. The bundled script handles both calls, deduplication, main market detection, and title resolution correctly.

# Install once (if not already installed):
npm install @azuro-org/dictionaries

# Browse by sport/league:
node scripts/get-games.js                              # top 20 games, all sports
node scripts/get-games.js basketball nba 10            # NBA only
node scripts/get-games.js football premier-league 10   # Premier League
node scripts/get-games.js hockey 5                     # NHL

# Search by team or match name:
node scripts/get-games.js --search "Real Madrid"       # find Real Madrid games
node scripts/get-games.js --search "Celtics" 3         # find Celtics games
node scripts/get-games.js --search "Lakers vs" 5       # find Lakers matchups

When to use --search vs sport/league filter:

  • Use --search when the user mentions a specific team, player, or match by name
  • Use sport/league filters when the user asks for a list of games (e.g. "show me NBA tonight")
  • --search queries across all sports and leagues simultaneously

The script outputs:

  1. A clean human-readable list with main market odds per game — show this to the user
  2. A ---JSON--- block with machine-readable data — use the conditionId and outcomeId values from this for bet placement in Step 2

Optional GraphQL filters still available if the user requests by country or time window — pass them as additional arguments or modify the script call. See references/subgraph.md for all filter options.

The script handles all translation and filtering automatically. Show its human-readable output directly to the user.

Secondary markets (only if user explicitly asks — e.g. "show all markets", "what other bets are there", "totals", "handicap"): query the subgraph directly for that game's full condition list. See references/subgraph.md.

CRITICAL — how to identify the main market condition:

Each game returns multiple conditions from the subgraph. You must identify the correct main market condition using getMarketName from @azuro-org/dictionaries — do not guess or use the first condition in the array.

Verified main market names from @azuro-org/dictionaries (confirmed by scanning the full package):

Market name Outcomes Sports
"Match Winner" 2: "1", "2" Basketball (NBA), Tennis, Esports, most 1v1
"Full Time Result" 3: "1", "X", "2" Football/Soccer
"Winner" 2: "1", "2" Hockey (NHL), MMA, some others
"Fight Winner" 2: "1", "2" MMA/Boxing specifically
"Whole game - Full time result Goal" 3: "1", "X", "2" Football variant

These are the exact strings returned by getMarketName({ outcomeId }). Do not invent or assume market names — always derive them from the dictionary.

import { getMarketName, getSelectionName } from '@azuro-org/dictionaries'

// All known main market names — verified against @azuro-org/dictionaries
const MAIN_MARKET_NAMES = [
  'Match Winner',                        // Basketball, Tennis, Esports
  'Full Time Result',                    // Football/Soccer (3-way: 1 X 2)
  'Winner',                              // Hockey (NHL), MMA, others
  'Fight Winner',                        // MMA/Boxing
  'Whole game - Full time result Goal',  // Football variant
]

// For each game, find the main market condition:
const mainCondition = game.conditions
  .filter(c => c.state === 'Active')
  .find(c => {
    try {
      const name = getMarketName({ outcomeId: c.outcomes[0].outcomeId })
      return MAIN_MARKET_NAMES.includes(name)
    } catch { return false }
  })

if (!mainCondition) {
  // No main market active — skip game in default view
  return
}

// Map outcome selections to display labels:
// "1" = participants[0] (home), "2" = participants[1] (away), "X" = Draw
mainCondition.outcomes.forEach(o => {
  const selection = getSelectionName({ outcomeId: o.outcomeId, withPoint: true })
  const label = selection === '1' ? game.participants[0].name
              : selection === '2' ? game.participants[1].name
              : 'Draw'
  console.log(label, '@', o.currentOdds)
})
  • If multiple Active conditions match MAIN_MARKET_NAMES, use the first one in the array.
  • If no condition matches, show the game with "No active main market — ask for all markets to see available options" and skip it in the default view.
  • Never hardcode outcomeIds — always resolve market names via getMarketName at runtime.

Example default output:

🏀 NBA — Tonight
1. Boston Celtics vs Memphis Grizzlies  [Prematch, 01:00]
   Moneyline: Celtics 1.07 | Grizzlies 7.92

2. Toronto Raptors vs Denver Nuggets  [Prematch, 00:30]
   Moneyline: Raptors 3.21 | Nuggets 1.31

3. Dallas Mavericks vs Cleveland Cavaliers  [LIVE 🔴]
   Moneyline: Mavericks 2.10 | Cavaliers 1.68

Never output raw outcomeId numbers, condition arrays, or unfiltered API responses. If no Active conditions exist for the main market, show the game with "No active main market — ask for all markets to see available options."

Step 2 — Choose selection

Ask the user which game and selection they want. Use the ---JSON--- output from the script to get the exact values needed for bet placement — do not re-query the subgraph.

// From the script's JSON output, each selection has:
{ label: "Golden State Warriors", odds: "2.67", outcomeId: 6983, conditionId: "1006..." }

Get from the chosen selection:

  • conditionId (string) — from the JSON output
  • outcomeId (number) — from the JSON output
  • currentOdds (string) — from the JSON output (odds field)

CRITICAL — use the bundled script for bet placement, do not implement Steps 3-7 inline.

Once the user confirms, run:

node scripts/place-bet.js --stake <USDT> --outcome <outcomeId> --condition <conditionId> --odds <currentOdds> --starts-at <startsAt> --match "<Team A vs Team B>"

Example:

node scripts/place-bet.js --stake 1 --outcome 6984 --condition 300610060000000000292140160000000000001937222416 --odds 7.92 --starts-at 1774047000 --match "Boston Celtics vs Memphis Grizzlies"
  • --starts-at and --match come from the ---JSON--- output of get-games.js (startsAt and title fields). Always pass them — they enable automatic result notification via watch-bets.js.
  • If --starts-at is provided, the script automatically launches watch-bets.js in background after the bet is confirmed. No extra action needed.

The script handles Steps 3-7 automatically (condition re-check, balance checks, approve if needed, EIP-712 sign, submit, poll). Do not attempt to run these steps manually or look for other scripts. The only three scripts are get-games.js (fetch), place-bet.js (bet), and watch-bets.js (result notification).

Compute minOdds (for reference only — the script does this internally):

  • Single bet: minOdds = Math.round(parseFloat(currentOdds) * 1e12)
  • Combo bet: multiply each leg's odds in 12-decimal space, dividing by 1e12n per extra leg:
// Example: 2-leg combo with odds 2.5 and 1.8 → combined odds 4.5
// leg1: 2.5 → 2_500_000_000_000n
// leg2: 1.8 → 1_800_000_000_000n
// combined: (2_500_000_000_000n * 1_800_000_000_000n) / 1_000_000_000_000n = 4_500_000_000_000n

const toOdds12 = (o) => BigInt(Math.round(parseFloat(o) * 1e12))
const minOdds = [odds1, odds2, ...oddsN].reduce(
  (acc, o) => (acc * toOdds12(o)) / 1_000_000_000_000n,
  1_000_000_000_000n
)

CRITICAL — re-check the condition is still Active immediately before calling /agent/bet:

GET https://api.onchainfeed.org/api/v1/public/market-manager/conditions-by-game-ids with { "gameIds": [...], "environment": "PolygonUSDT" } and check the condition state in the response

If condition.state !== "Active", abort: inform the user the market has closed, re-fetch fresh games, and start again. Do not call /agent/bet on a closed condition.

Step 3 — Call Pinwin

NOTE: Steps 3-7 are implemented by scripts/place-bet.js. Run the script (see Step 2) and do not execute these steps manually. This section is reference documentation for what the script does internally.

POST https://api.pinwin.xyz/agent/bet
Content-Type: application/json

{
  "amount": <stake in USDT 6-decimal units, e.g. 2000000 for 2 USDT>,
  "minOdds": <computed above>,
  "chain": "polygon",
  "selections": [{ "conditionId": "<string>", "outcomeId": <number> }]
}

For combo, add more objects to selections.

Response: { "encoded": "<base64>" }. Decode:

const payload = JSON.parse(atob(response.encoded))

Step 4 — Explain to user before signing

Always show the full decoded payload before any approval or signing. The user must understand and confirm what they are authorising.

Display:

  • Stake: amount in USDT (divide by 1e6), e.g. "2.00 USDT"
  • Selection(s): human-readable names (from dictionaries), conditionId, outcomeId
  • Relayer fee: relayerFeeAmount (divide by 1e6), e.g. "0.05 USDT"
  • Total USDT needed: stake + relayerFeeAmount
  • apiUrl, environment, expiresAt, affiliate, isFeeSponsored, isBetSponsored

Then ask for explicit confirmation before proceeding.

Step 5 — Approve USDT (if needed)

CRITICAL — never skip this step.

const relayer = '0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d'
const USDT    = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'
const relayerFeeAmount = BigInt(payload.signableClientBetData.clientData?.relayerFeeAmount ?? 0)
const stakeAmount      = BigInt(payload.signableClientBetData.bet?.amount ?? payload.signableClientBetData.amount)
const buffer           = 200000n // 0.2 USDT in 6-decimal units
const required         = stakeAmount + relayerFeeAmount + buffer

const allowance = await publicClient.readContract({
  address: USDT, abi: erc20Abi, functionName: 'allowance', args: [bettor, relayer]
})

if (allowance < required) {
  const approveTx = await walletClient.sendTransaction({
    to: USDT,
    data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [relayer, required] })
  })
  await publicClient.waitForTransactionReceipt({ hash: approveTx })
  // Inform user: "USDT approval confirmed."
}

Step 6 — Verify payload

Before signing, verify:

  1. payload.signableClientBetData.bet.amount (single) or payload.signableClientBetData.amount (combo) matches the user's requested stake
  2. conditionId and outcomeId in the payload match the user's chosen selection(s)
  3. payload.signableClientBetData.clientData.core.toLowerCase() === 0xf9548be470a4e130c90cea8b179fcd66d2972ac7 (claimContract)

If any check fails, do not sign. Report the mismatch to the user and stop.

Step 7 — Sign, submit, and poll

// Determine primaryType
const primaryType = payload.types.ClientComboBetData ? 'ClientComboBetData' : 'ClientBetData'

// Sign
const bettorSignature = await walletClient.signTypedData({
  account,
  domain: payload.domain,
  types: payload.types,
  primaryType,
  message: payload.signableClientBetData,
})

// Submit to Azuro order API
const submitRes = await fetch(payload.apiUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    environment: payload.environment,
    bettor,
    betOwner: bettor,
    clientBetData: payload.apiClientBetData,
    bettorSignature,
  })
})
const submitData = await submitRes.json()

If submission fails (4xx/5xx or state: "Rejected" / "Canceled"), report submitData.errorMessage or submitData.error to the user and stop.

CRITICAL — poll until txHash. Do not declare success on state: "Created".

The order is only on-chain when txHash is present. state: "Created" is only an initial state — the bet is NOT confirmed yet.

// Derive base URL from apiUrl
// e.g. "https://api.onchainfeed.org/v1/bet/orders/ordinar" → "https://api.onchainfeed.org/v1"
const apiBase = payload.apiUrl.replace(/\/bet\/orders\/(ordinar|combo)$/, '')
const orderId = submitData.id

let txHash = null
for (let i = 0; i < 30; i++) {
  const delayMs = Math.min(2000 + (i * 1000), 10000) // progressive backoff: 2s, 3s, 4s … 10s max
  await new Promise(r => setTimeout(r, delayMs))
  const poll = await fetch(`${apiBase}/bet/orders/${orderId}`).then(r => r.json())
  if (poll.txHash) { txHash = poll.txHash; break }
  if (poll.state === 'Rejected' || poll.state === 'Canceled') {
    // Report failure and stop
    throw new Error(`Order ${poll.state}: ${poll.errorMessage || 'unknown error'}`)
  }
}

if (!txHash) throw new Error('Order did not settle after 90s — check manually on Polygonscan')

On success, show the user:

✅ Bet confirmed on-chain!
   Match:      {teams}
   Selection:  {market} — {selection} @ {odds}
   Stake:      {stake} USDT
   To win:     {potential payout} USDT
   Tx:         https://polygonscan.com/tx/{txHash}

Flow — resume interrupted flow

If the user resumes a bet request after an interruption (e.g. session ended mid-flow), check current state before starting over to avoid double-approvals or duplicate bets:

  1. Check allowance — if allowance(bettor, relayer) >= stake + fee, approval was already sent. Skip Step 5.
  2. Check for pending bets — query the bets subgraph for status: "Accepted" bets from this wallet placed in the last 10 minutes. If found, poll that order instead of creating a new one.
  3. If no pending bet found and no clear state, start the flow from Step 1 with a fresh game fetch.

Flow — check bet status

Query the bets subgraph to see pending, resolved, or redeemable bets.

POST https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3
Content-Type: application/json

{
  "query": "query BettorBets($where: V3_Bet_filter!, $first: Int, $orderBy: V3_Bet_orderBy, $orderDirection: OrderDirection) { v3Bets(where: $where, first: $first, orderBy: $orderBy, orderDirection: $orderDirection) { betId status result isRedeemable isRedeemed amount payout createdBlockTimestamp resolvedBlockTimestamp } }",
  "variables": {
    "where": { "bettor": "<address in lowercase>" },
    "first": 50,
    "orderBy": "createdBlockTimestamp",
    "orderDirection": "desc"
  }
}

To fetch only redeemable bets: add "isRedeemable": true to where.

Interpret results:

status result isRedeemable Meaning
Accepted false Pending — not settled yet
Resolved Won true Won — claim available
Resolved Lost false Lost
Canceled true Canceled — stake refundable
Resolved Won false Already claimed

On invocation, always check for newly resolved bets from previous sessions and surface them proactively:

"You have a resolved bet: {match} — {selection} — {Won/Lost}. Payout: {payout} USDT." If won: "Would you like to claim your winnings?"


Flow — claim

Only for bets where isRedeemable === true and isRedeemed === false. Collect betId values from the subgraph.

Step 1 — Call Pinwin

POST https://api.pinwin.xyz/agent/claim
Content-Type: application/json

{ "betIds": [<betId>, ...], "chain": "polygon" }

Decode: payload = JSON.parse(atob(response.encoded))

Explain to the user in plain terms: "This transaction claims your winnings for bet IDs [X, Y] from the Azuro ClientCore contract on Polygon. No ETH/POL value is sent."

Display the full decoded payload: to, data, value, chainId.

Step 2 — Verify claim contract

CRITICAL: payload.to.toLowerCase() must equal 0xf9548be470a4e130c90cea8b179fcd66d2972ac7.

If it does not match, do not send the transaction. Report the mismatch and stop.

Step 3 — Send and confirm

const claimTx = await walletClient.sendTransaction({
  to: payload.to,
  data: payload.data,
  value: 0n,
  chainId: payload.chainId,
})
await publicClient.waitForTransactionReceipt({ hash: claimTx })

On success, show:

✅ Winnings claimed!
   Bet IDs:  {betIds}
   Tx:       https://polygonscan.com/tx/{claimTx}

Tools summary

Step Tool Purpose
Balance check viem getBalance, readContract POL gas, USDT balance and allowance
Fetch games Data-feed subgraph (GraphQL) Prematch/live games, conditions, odds
Translate odds @azuro-org/dictionaries outcomeId → human-readable market/selection names
Place bet POST /agent/bet Get encoded EIP-712 payload
Sign viem signTypedData EIP-712 bet signature
Submit Azuro order API (payload.apiUrl) Submit signed bet
Poll GET {apiBase}/bet/orders/{orderId} Wait for txHash confirmation
Watch result scripts/watch-bets.js (auto-launched by place-bet.js) Waits until kickoff + 2h, queries bets subgraph, notifies user via sendPrompt
Bet status Bets subgraph (GraphQL) Check status, result, isRedeemable
Claim POST /agent/claim + viem sendTransaction Redeem winnings on-chain

Error handling

Error Cause Action
state: "Rejected" / "Canceled" on order Invalid bet, market moved, odds expired Report errorMessage, do not retry automatically
Empty order ID from submission Condition no longer Active (market closed) Re-check condition state, re-fetch games
allowance < required USDT not approved Run Step 5 (approve) before signing
txHash not received after 90s Relayer slow or order stuck Show last known order state, give orderId for manual check on Polygonscan
Subgraph returns 200 with data.errors GraphQL error Read data.errors[0].message, report to user
POL balance = 0 No gas Inform user to fund wallet with POL for gas
Payload stake = 0 after /agent/bet Always run place-bet.js --dry-run first to see request body. Likely causes: (1) outcomeId not valid for that conditionId — always take both from the same entry in the ---JSON--- output of get-games.js, never mix them; (2) conditionId format wrong (must be string, not number); (3) Pinwin API temporarily down — retry after 2 min. Do NOT diagnose this by eye — use --dry-run to inspect the exact request body then compare with the condition in the data-feed.

Reference files

Load only if you need full detail beyond what is in this file:

同梱ファイル

※ ZIPに含まれるファイル一覧。`SKILL.md` 本体に加え、参考資料・サンプル・スクリプトが入っている場合があります。