{"id":"b8d58b4a-3e98-4655-acb0-bc5384b184b1","entityType":"agent","slug":"clawhub-skinnynoizze-sports-betting","name":"Sports Betting","canonicalUrl":"https://www.xpersona.co/agent/clawhub-skinnynoizze-sports-betting","canonicalPath":"/agent/clawhub-skinnynoizze-sports-betting","generatedAt":"2026-10-10T07:12:24.504Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T16:46:24.076Z","emptyReason":null},"description":"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 A...","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 2.3K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s175pg1aqq5cx8v6h85f5xgdds845gj9:sports-betting","sourceUrl":"https://clawhub.ai/skinnynoizze/sports-betting","homepage":"https://clawhub.ai/skinnynoizze/skills/sports-betting","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/skinnynoizze/sports-betting","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/skinnynoizze/skills/sports-betting","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":60,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Sports Betting technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T16:46:24.076Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T16:46:24.076Z","emptyReason":null},"stars":null,"forks":null,"downloads":2277,"packageName":null,"latestVersion":"1.3.1","tractionLabel":"2.3K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T16:46:24.075Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T16:46:24.076Z","lastCrawledAt":"2026-10-09T16:46:24.075Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T16:46:24.075Z","lastVerifiedAt":null,"highlights":[{"version":"1.3.1","createdAt":"2026-04-03T13:16:37.019Z","changelog":"sports-betting 1.2.3 — Major cleanup and claim script addition. - Added scripts/claim-bets.js for handling bet claim/redeem actions. - Removed legacy reference files (api.md, bets-subgraph.md, dictionaries.md, polygon.md, subgraph.md, viem.md). - Updated and clarified contract addresses for claimContract and clientCore. - Expanded documentation on secure handling of BETTOR_PRIVATE_KEY, emphasizing .env usage and security. - Improved and clarified constants, setup instructions, and best practices for user safety.","fileCount":10,"zipByteSize":30524},{"version":"1.2.2","createdAt":"2026-03-22T13:26:54.671Z","changelog":"sports-betting 1.2.2 - Updated Azuro data-feed source to the new REST API endpoint (`https://api.onchainfeed.org/api/v1/public/market-manager/`) for fetching games and odds. - The get-games.js script now uses the REST API, with two-step data fetching, improved main market detection, and direct human-readable titles (no more subgraph JSON parsing). - Added support for team/match search via a new `--search` option in the get-games.js script. - Clarified usage: use the bundled script for all listings; do not call the REST API directly. - @azuro-org/dictionaries is now required only for bet placement, not for listing games.","fileCount":14,"zipByteSize":31435},{"version":"1.0.4","createdAt":"2026-03-21T15:18:41.588Z","changelog":"**sports-betting v1.0.4** Initial code and documentation release with strict safety and invocation rules. - Added standalone betting scripts: `get-games.js`, `place-bet.js`, and `watch-bets.js` for listing games, placing bets, and monitoring bet status. - Introduced full skill documentation with strict user confirmation and transaction safety requirements. - Defined all Polygon constants (addresses, URLs, tokens) directly in documentation for reliable reference. - Required explicit user confirmation for on-chain actions; autonomous or background execution is prohibited. - Clarified invocation instructions, dependencies, and environment requirements.","fileCount":14,"zipByteSize":29737},{"version":"1.0.3","createdAt":"2026-02-21T13:22:19.464Z","changelog":"No functional changes in this release. - Quoted description, compatibility, and homepage, so YAML parses them consistently and colons/commas in the text won’t cause issues. - Internal documentation and flow descriptions were significantly shortened and clarified in SKILL.md for easier reference. - User-facing feature set and integration points remain unchanged. - No modifications detected in code or logic.","fileCount":9,"zipByteSize":12287},{"version":"1.0.2","createdAt":"2026-02-19T06:15:36.212Z","changelog":"sports-betting 1.0.2 - Clarified environment variable requirements: BETTOR_PRIVATE_KEY is now required, POLYGON_RPC_URL is optional (defaults to public endpoints if unset). - Updated compatibility and metadata to reflect these env changes. - Improved documentation for security best practices: strongly recommends using a dedicated betting wallet and warns against exposing private keys. - Changed homepage URL in metadata to point to https://github.com/skinnynoizze/pinwin-agent. - No changes to code or implementation; documentation and metadata only.","fileCount":9,"zipByteSize":12695},{"version":"1.0.1","createdAt":"2026-02-19T05:56:55.599Z","changelog":"**Enhanced transparency and security for bet placement and approval.** - Now always displays and explains all decoded bet payload details (amount, selections, relayer fee, etc.) to the user before any signing or approval. - Adds strict verification that the payload matches the user's requested amount and selections before signing—if there’s a mismatch, the process is stopped and the user is alerted. - Requires bet approval only up to the bet amount plus relayer fee (plus a 0.2 USDT buffer), not unlimited—improving safety. - Verifies that only the correct relayer and Polygon claim contract are used (never using values passed from external payloads) for all approvals and signing.","fileCount":9,"zipByteSize":12329},{"version":"1.0.0","createdAt":"2026-02-18T22:36:18.440Z","changelog":"sports-betting v1.0.0 - Initial release enabling on-chain sports betting on Polygon via Pinwin and Azuro Protocol. - Fetch prematch/live games, pick selections, place bets, and claim winnings using the Pinwin API. - Compatibility requires Node, viem, @azuro-org/dictionaries, Polygon RPC URL, and wallet private key. - Includes detailed flows for placing bets and claiming winnings, with user prompts for missing preferences. - Ensures sufficient balances before betting and provides status checks for bet resolution and redemption.","fileCount":9,"zipByteSize":10404}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s175pg1aqq5cx8v6h85f5xgdds845gj9:sports-betting","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s175pg1aqq5cx8v6h85f5xgdds845gj9:sports-betting` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/skinnynoizze/sports-betting before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T07:12:24.501Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-skinnynoizze-sports-betting/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T16:46:24.076Z","emptyReason":null},"readme":"Skill: Sports Betting\n\nOwner: skinnynoizze\n\nSummary: 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 A...\n\nTags: latest:1.3.1\n\nVersion history:\n\nv1.3.1 | 2026-04-03T13:16:37.019Z | user\n\nsports-betting 1.2.3 — Major cleanup and claim script addition.\n\n- Added scripts/claim-bets.js for handling bet claim/redeem actions.\n- Removed legacy reference files (api.md, bets-subgraph.md, dictionaries.md, polygon.md, subgraph.md, viem.md).\n- Updated and clarified contract addresses for claimContract and clientCore.\n- Expanded documentation on secure handling of BETTOR_PRIVATE_KEY, emphasizing .env usage and security.\n- Improved and clarified constants, setup instructions, and best practices for user safety.\n\nv1.2.2 | 2026-03-22T13:26:54.671Z | user\n\nsports-betting 1.2.2\n\n- Updated Azuro data-feed source to the new REST API endpoint (`https://api.onchainfeed.org/api/v1/public/market-manager/`) for fetching games and odds.\n- The get-games.js script now uses the REST API, with two-step data fetching, improved main market detection, and direct human-readable titles (no more subgraph JSON parsing).\n- Added support for team/match search via a new `--search` option in the get-games.js script.\n- Clarified usage: use the bundled script for all listings; do not call the REST API directly.\n- @azuro-org/dictionaries is now required only for bet placement, not for listing games.\n\nv1.0.4 | 2026-03-21T15:18:41.588Z | user\n\n**sports-betting v1.0.4**\n\nInitial code and documentation release with strict safety and invocation rules.\n\n- Added standalone betting scripts: `get-games.js`, `place-bet.js`, and `watch-bets.js` for listing games, placing bets, and monitoring bet status.\n- Introduced full skill documentation with strict user confirmation and transaction safety requirements.\n- Defined all Polygon constants (addresses, URLs, tokens) directly in documentation for reliable reference.\n- Required explicit user confirmation for on-chain actions; autonomous or background execution is prohibited.\n- Clarified invocation instructions, dependencies, and environment requirements.\n\nv1.0.3 | 2026-02-21T13:22:19.464Z | user\n\nNo functional changes in this release.\n- Quoted description, compatibility, and homepage, so YAML parses them consistently and colons/commas in the text won’t cause issues.\n- Internal documentation and flow descriptions were significantly shortened and clarified in SKILL.md for easier reference.\n- User-facing feature set and integration points remain unchanged.\n- No modifications detected in code or logic.\n\nv1.0.2 | 2026-02-19T06:15:36.212Z | user\n\nsports-betting 1.0.2\n\n- Clarified environment variable requirements: BETTOR_PRIVATE_KEY is now required, POLYGON_RPC_URL is optional (defaults to public endpoints if unset).\n- Updated compatibility and metadata to reflect these env changes.\n- Improved documentation for security best practices: strongly recommends using a dedicated betting wallet and warns against exposing private keys.\n- Changed homepage URL in metadata to point to https://github.com/skinnynoizze/pinwin-agent.\n- No changes to code or implementation; documentation and metadata only.\n\nv1.0.1 | 2026-02-19T05:56:55.599Z | user\n\n**Enhanced transparency and security for bet placement and approval.**\n\n- Now always displays and explains all decoded bet payload details (amount, selections, relayer fee, etc.) to the user before any signing or approval.\n- Adds strict verification that the payload matches the user's requested amount and selections before signing—if there’s a mismatch, the process is stopped and the user is alerted.\n- Requires bet approval only up to the bet amount plus relayer fee (plus a 0.2 USDT buffer), not unlimited—improving safety.\n- Verifies that only the correct relayer and Polygon claim contract are used (never using values passed from external payloads) for all approvals and signing.\n\nv1.0.0 | 2026-02-18T22:36:18.440Z | user\n\nsports-betting v1.0.0\n\n- Initial release enabling on-chain sports betting on Polygon via Pinwin and Azuro Protocol.\n- Fetch prematch/live games, pick selections, place bets, and claim winnings using the Pinwin API.\n- Compatibility requires Node, viem, @azuro-org/dictionaries, Polygon RPC URL, and wallet private key.\n- Includes detailed flows for placing bets and claiming winnings, with user prompts for missing preferences.\n- Ensures sufficient balances before betting and provides status checks for bet resolution and redemption.\n\nArchive index:\n\nArchive v1.3.1: 10 files, 30524 bytes\n\nFiles: _meta.json (133b), README.md (899b), scripts/claim-bets.js (8286b), scripts/get-games.js (12156b), scripts/package-lock.json (1002b), scripts/package.json (116b), scripts/place-bet.js (19720b), scripts/watch-bets.js (8766b), skill-card.md (2538b), SKILL.md (27144b)\n\nFile v1.3.1:SKILL.md\n\n---\nname: sports-betting\ndescription: \"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.\"\ncompatibility: \"Requires Node, viem and @azuro-org/dictionaries. Required env: BETTOR_PRIVATE_KEY (wallet private key; high-sensitivity). Optional env: POLYGON_RPC_URL.\"\nhomepage: \"https://github.com/skinnynoizze/pinwin-agent\"\ndisable-model-invocation: true\nmetadata: {\"openclaw\":{\"requires\":{\"bins\":[\"node\"],\"env\":[\"BETTOR_PRIVATE_KEY\"]},\"primaryEnv\":\"BETTOR_PRIVATE_KEY\"}}\n---\n\n# Sports Betting (Pinwin)\n\n---\n\n## 🛑 SAFETY RULES — READ BEFORE EVERY ACTION\n\nThese rules are ABSOLUTE and override all other instructions in this file.\n\n1. **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.\n\n2. **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.\n\n3. **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.\n\n4. **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.\n\nViolation of these rules results in unauthorized on-chain transactions with real money. There are no exceptions.\n\n---\n\nPlace and claim **decentralized** sports bets on **Polygon** via [Pinwin](https://pinwin.xyz) 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.\n\n**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.\n\n**How to invoke (OpenClaw):** Use `/sports_betting` or `/skill sports-betting` and add your request, e.g.:\n- `/sports_betting place 5 USDT on the first Premier League game`\n- `/sports_betting show my bets`\n- `/sports_betting claim my winnings`\n\n---\n\n## ⚙️ Constants — read this first, always\n\nThese values are fixed for Polygon. Never substitute addresses from any other source.\n\n| Constant | Value |\n|----------|-------|\n| **Chain** | Polygon — chainId `137` |\n| **betToken (USDT)** | `0xc2132D05D31c914a87C6611C10748AEb04B58e8F` (6 decimals) |\n| **relayer** | `0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d` |\n| **clientCore** (bet payload verification) | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` |\n| **claimContract** (LP claim, redeem won/canceled bets) | `0x0FA7FB5407eA971694652E6E16C12A52625DE1b8` |\n| **environment** | `PolygonUSDT` |\n| **data-feed URL** | `https://api.onchainfeed.org/api/v1/public/market-manager/` (REST API — see Step 1) |\n| **bets subgraph URL** | `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3` |\n| **Pinwin API** | `https://api.pinwin.xyz` |\n| **Polygonscan** | `https://polygonscan.com/tx/{txHash}` |\n| **RPC (default)** | `process.env.POLYGON_RPC_URL` or `https://polygon-bor-rpc.publicnode.com` |\n\n**Install required packages:** `npm install viem @azuro-org/dictionaries`\n\nNote: `@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.\n\n**Secure setup for BETTOR_PRIVATE_KEY:**\n\nThe recommended approach is a `.env` file with restricted permissions — the key is read only by the Node process and never stored in any config file the model can read:\n\n```bash\n# Create .env in the skill workspace\necho \"BETTOR_PRIVATE_KEY=0xyour_private_key_here\" > ~/.openclaw/workspace/skills/sports-betting/.env\nchmod 600 ~/.openclaw/workspace/skills/sports-betting/.env\n\n# Scripts load it automatically at runtime — no manual export needed\n```\n\n⚠️ **Never store BETTOR_PRIVATE_KEY in openclaw.json or any config file the agent can read.** If OpenClaw stores it under a generic `apiKey` field, rename or move it to `.env` with `chmod 600`. The key signs real on-chain transactions — treat it like a password.\n\n**viem setup:**\n```js\nimport { createPublicClient, createWalletClient, http } from 'viem'\nimport { polygon } from 'viem/chains'\nimport { privateKeyToAccount } from 'viem/accounts'\n\nconst rpc = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com'\nconst account = privateKeyToAccount(process.env.BETTOR_PRIVATE_KEY)\nconst publicClient = createPublicClient({ chain: polygon, transport: http(rpc) })\nconst walletClient = createWalletClient({ account, chain: polygon, transport: http(rpc) })\nconst bettor = account.address\n```\n\n---\n\n## 📋 Pre-flight checklist\n\nRun this before every bet. Do not skip any item.\n\n- [ ] **BETTOR_PRIVATE_KEY** is set and wallet address derived\n- [ ] **POL balance** ≥ enough for gas (`publicClient.getBalance({ address: bettor })`)\n- [ ] **USDT balance** ≥ stake (`readContract` on betToken with `balanceOf`)\n- [ ] **Selected condition** `state === \"Active\"` — re-check immediately before calling `/agent/bet`, not just at game fetch time\n- [ ] **Allowance** checked and approved if needed (see Step 5)\n\nIf any check fails, inform the user and stop. Do not proceed.\n\n---\n\n## Flow — place a bet\n\n### Step 0 — Check balances\n\n```js\nconst erc20Abi = parseAbi([\n  'function balanceOf(address) view returns (uint256)',\n  'function allowance(address,address) view returns (uint256)',\n  'function approve(address,uint256) returns (bool)',\n])\nconst USDT = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'\n\nconst pol = await publicClient.getBalance({ address: bettor })\nconst usdt = await publicClient.readContract({ address: USDT, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })\n```\n\n- `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.\"\n- `usdt` must be ≥ stake in 6-decimal units (e.g. 2 USDT = `2000000n`)\n- If either is insufficient, stop and inform the user\n\n### Step 1 — Fetch games\n\n**CRITICAL — use the bundled script, do not call the REST API manually.**\n\nThe 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.\n\n```bash\n# Install once (if not already installed):\nnpm install @azuro-org/dictionaries\n\n# Browse by sport/league:\nnode scripts/get-games.js                              # top 20 games, all sports\nnode scripts/get-games.js basketball nba 10            # NBA only\nnode scripts/get-games.js football premier-league 10   # Premier League\nnode scripts/get-games.js hockey 5                     # NHL\n\n# Search by team or match name:\nnode scripts/get-games.js --search \"Real Madrid\"       # find Real Madrid games\nnode scripts/get-games.js --search \"Celtics\" 3         # find Celtics games\nnode scripts/get-games.js --search \"Lakers vs\" 5       # find Lakers matchups\n```\n\n**When to use each mode:**\n- Use `--search` when the user mentions a specific team, player, or match by name (e.g. \"show me Real Madrid games\", \"find Celtics\")\n- Use sport/league filters when the user asks for a list of games (e.g. \"show me NBA tonight\", \"Premier League games\")\n- `--search` queries across all sports and leagues simultaneously\n\n**CRITICAL — slug resolution:**\nThe `--sport` and `--league` args require **exact API slugs** (e.g. `ice-hockey`, `laliga`, `nhl`), not human-friendly names. If the user uses a name that doesn't match a known alias (e.g. \"La Liga\", \"Spanish football\", \"Champions League\"), run `--list-sports` first to find the correct slug:\n\n```bash\nnode scripts/get-games.js --list-sports\n```\n\nThis calls the `/navigation` endpoint and shows all available sports and leagues with their exact slugs and active game counts. Then use the correct slug in the next call. Do NOT guess slugs — always verify with `--list-sports` if unsure.\n\nThe script outputs:\n1. A **clean human-readable list** with main market odds per game — show this to the user\n2. A `---JSON---` block with machine-readable data — use the `conditionId` and `outcomeId` values from this for bet placement in Step 2\n\nOptional 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.\n\n**The script handles all translation and filtering automatically.** Show its human-readable output directly to the user.\n\n**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`.\n\n**CRITICAL — how to identify the main market condition:**\n\nEach 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.\n\n**Verified main market names from `@azuro-org/dictionaries` (confirmed by scanning the full package):**\n\n| Market name | Outcomes | Sports |\n|-------------|----------|--------|\n| `\"Match Winner\"` | 2: `\"1\"`, `\"2\"` | Basketball (NBA), Tennis, Esports, most 1v1 |\n| `\"Full Time Result\"` | 3: `\"1\"`, `\"X\"`, `\"2\"` | Football/Soccer |\n| `\"Winner\"` | 2: `\"1\"`, `\"2\"` | Hockey (NHL), MMA, some others |\n| `\"Fight Winner\"` | 2: `\"1\"`, `\"2\"` | MMA/Boxing specifically |\n| `\"Whole game - Full time result Goal\"` | 3: `\"1\"`, `\"X\"`, `\"2\"` | Football variant |\n\nThese are the **exact strings** returned by `getMarketName({ outcomeId })`. Do not invent or assume market names — always derive them from the dictionary.\n\n```js\nimport { getMarketName, getSelectionName } from '@azuro-org/dictionaries'\n\n// All known main market names — verified against @azuro-org/dictionaries\nconst MAIN_MARKET_NAMES = [\n  'Match Winner',                        // Basketball, Tennis, Esports\n  'Full Time Result',                    // Football/Soccer (3-way: 1 X 2)\n  'Winner',                              // Hockey (NHL), MMA, others\n  'Fight Winner',                        // MMA/Boxing\n  'Whole game - Full time result Goal',  // Football variant\n]\n\n// For each game, find the main market condition:\nconst mainCondition = game.conditions\n  .filter(c => c.state === 'Active')\n  .find(c => {\n    try {\n      const name = getMarketName({ outcomeId: c.outcomes[0].outcomeId })\n      return MAIN_MARKET_NAMES.includes(name)\n    } catch { return false }\n  })\n\nif (!mainCondition) {\n  // No main market active — skip game in default view\n  return\n}\n\n// Map outcome selections to display labels:\n// \"1\" = participants[0] (home), \"2\" = participants[1] (away), \"X\" = Draw\nmainCondition.outcomes.forEach(o => {\n  const selection = getSelectionName({ outcomeId: o.outcomeId, withPoint: true })\n  const label = selection === '1' ? game.participants[0].name\n              : selection === '2' ? game.participants[1].name\n              : 'Draw'\n  console.log(label, '@', o.currentOdds)\n})\n```\n\n- If multiple Active conditions match `MAIN_MARKET_NAMES`, use the first one in the array.\n- 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.\n- **Never hardcode outcomeIds** — always resolve market names via `getMarketName` at runtime.\n\nExample default output:\n```\n🏀 NBA — Tonight\n1. Boston Celtics vs Memphis Grizzlies  [Prematch, 01:00]\n   Moneyline: Celtics 1.07 | Grizzlies 7.92\n\n2. Toronto Raptors vs Denver Nuggets  [Prematch, 00:30]\n   Moneyline: Raptors 3.21 | Nuggets 1.31\n\n3. Dallas Mavericks vs Cleveland Cavaliers  [LIVE 🔴]\n   Moneyline: Mavericks 2.10 | Cavaliers 1.68\n```\n\nNever 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.\"\n\n### Step 2 — Choose selection\n\nAsk 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.\n\n```\n// From the script's JSON output, each selection has:\n{ label: \"Golden State Warriors\", odds: \"2.67\", outcomeId: 6983, conditionId: \"1006...\" }\n```\n\nGet from the chosen selection:\n- `conditionId` (string) — from the JSON output\n- `outcomeId` (number) — from the JSON output  \n- `currentOdds` (string) — from the JSON output (`odds` field)\n\n**CRITICAL — use the bundled script for bet placement, do not implement Steps 3-7 inline.**\n\nOnce the user confirms, run:\n\n```bash\nnode scripts/place-bet.js --stake <USDT> --outcome <outcomeId> --condition <conditionId> --odds <currentOdds> --starts-at <startsAt> --match \"<Team A vs Team B>\"\n```\n\nExample:\n```bash\nnode scripts/place-bet.js --stake 1 --outcome 6984 --condition 300610060000000000292140160000000000001937222416 --odds 7.92 --starts-at 1774047000 --match \"Boston Celtics vs Memphis Grizzlies\"\n```\n\n- `--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`.\n- If `--starts-at` is provided, the script automatically launches `watch-bets.js` in background after the bet is confirmed. No extra action needed.\n\nThe 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).\n\nCompute `minOdds` (for reference only — the script does this internally):\n- Single bet: `minOdds = Math.round(parseFloat(currentOdds) * 1e12)`\n- Combo bet: multiply each leg's odds in 12-decimal space, dividing by `1e12n` per extra leg:\n\n```js\n// Example: 2-leg combo with odds 2.5 and 1.8 → combined odds 4.5\n// leg1: 2.5 → 2_500_000_000_000n\n// leg2: 1.8 → 1_800_000_000_000n\n// combined: (2_500_000_000_000n * 1_800_000_000_000n) / 1_000_000_000_000n = 4_500_000_000_000n\n\nconst toOdds12 = (o) => BigInt(Math.round(parseFloat(o) * 1e12))\nconst minOdds = [odds1, odds2, ...oddsN].reduce(\n  (acc, o) => (acc * toOdds12(o)) / 1_000_000_000_000n,\n  1_000_000_000_000n\n)\n```\n\n**CRITICAL — re-check the condition is still Active immediately before calling `/agent/bet`:**\n```json\nGET 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\n```\nIf `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.\n\n### Step 3 — Call Pinwin\n\n> **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.**\n\n```\nPOST https://api.pinwin.xyz/agent/bet\nContent-Type: application/json\n\n{\n  \"amount\": <stake in USDT 6-decimal units, e.g. 2000000 for 2 USDT>,\n  \"minOdds\": <computed above>,\n  \"chain\": \"polygon\",\n  \"selections\": [{ \"conditionId\": \"<string>\", \"outcomeId\": <number> }]\n}\n```\n\nFor combo, add more objects to `selections`.\n\nResponse: `{ \"encoded\": \"<base64>\" }`. Decode:\n```js\nconst payload = JSON.parse(atob(response.encoded))\n```\n\n### Step 4 — Explain to user before signing\n\n**Always show the full decoded payload before any approval or signing.** The user must understand and confirm what they are authorising.\n\nDisplay:\n- Stake: `amount` in USDT (divide by 1e6), e.g. \"2.00 USDT\"\n- Selection(s): human-readable names (from dictionaries), conditionId, outcomeId\n- Relayer fee: `relayerFeeAmount` (divide by 1e6), e.g. \"0.05 USDT\"\n- Total USDT needed: stake + relayerFeeAmount\n- `apiUrl`, `environment`, `expiresAt`, `affiliate`, `isFeeSponsored`, `isBetSponsored`\n\nThen ask for explicit confirmation before proceeding.\n\n### Step 5 — Approve USDT (if needed)\n\n**CRITICAL — never skip this step.**\n\n```js\nconst relayer = '0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d'\nconst USDT    = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'\nconst relayerFeeAmount = BigInt(payload.signableClientBetData.clientData?.relayerFeeAmount ?? 0)\nconst stakeAmount      = BigInt(payload.signableClientBetData.bet?.amount ?? payload.signableClientBetData.amount)\nconst buffer           = 200000n // 0.2 USDT in 6-decimal units\nconst required         = stakeAmount + relayerFeeAmount + buffer\n\nconst allowance = await publicClient.readContract({\n  address: USDT, abi: erc20Abi, functionName: 'allowance', args: [bettor, relayer]\n})\n\nif (allowance < required) {\n  const approveTx = await walletClient.sendTransaction({\n    to: USDT,\n    data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [relayer, required] })\n  })\n  await publicClient.waitForTransactionReceipt({ hash: approveTx })\n  // Inform user: \"USDT approval confirmed.\"\n}\n```\n\n### Step 6 — Verify payload\n\nBefore signing, verify:\n\n1. `payload.signableClientBetData.bet.amount` (single) or `payload.signableClientBetData.amount` (combo) matches the user's requested stake\n2. `conditionId` and `outcomeId` in the payload match the user's chosen selection(s)\n3. `payload.signableClientBetData.clientData.core.toLowerCase()` === `0xf9548be470a4e130c90cea8b179fcd66d2972ac7` (clientCore — NOT the claimContract)\n\nIf any check fails, **do not sign**. Report the mismatch to the user and stop.\n\n### Step 7 — Sign, submit, and poll\n\n```js\n// Determine primaryType\nconst primaryType = payload.types.ClientComboBetData ? 'ClientComboBetData' : 'ClientBetData'\n\n// Sign\nconst bettorSignature = await walletClient.signTypedData({\n  account,\n  domain: payload.domain,\n  types: payload.types,\n  primaryType,\n  message: payload.signableClientBetData,\n})\n\n// Submit to Azuro order API\nconst submitRes = await fetch(payload.apiUrl, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    environment: payload.environment,\n    bettor,\n    betOwner: bettor,\n    clientBetData: payload.apiClientBetData,\n    bettorSignature,\n  })\n})\nconst submitData = await submitRes.json()\n```\n\nIf submission fails (4xx/5xx or `state: \"Rejected\"` / `\"Canceled\"`), report `submitData.errorMessage` or `submitData.error` to the user and stop.\n\n**CRITICAL — poll until txHash. Do not declare success on `state: \"Created\"`.**\n\nThe order is only on-chain when `txHash` is present. `state: \"Created\"` is only an initial state — the bet is NOT confirmed yet.\n\n```js\n// Derive base URL from apiUrl\n// e.g. \"https://api.onchainfeed.org/v1/bet/orders/ordinar\" → \"https://api.onchainfeed.org/v1\"\nconst apiBase = payload.apiUrl.replace(/\\/bet\\/orders\\/(ordinar|combo)$/, '')\nconst orderId = submitData.id\n\nlet txHash = null\nfor (let i = 0; i < 30; i++) {\n  const delayMs = Math.min(2000 + (i * 1000), 10000) // progressive backoff: 2s, 3s, 4s … 10s max\n  await new Promise(r => setTimeout(r, delayMs))\n  const poll = await fetch(`${apiBase}/bet/orders/${orderId}`).then(r => r.json())\n  if (poll.txHash) { txHash = poll.txHash; break }\n  if (poll.state === 'Rejected' || poll.state === 'Canceled') {\n    // Report failure and stop\n    throw new Error(`Order ${poll.state}: ${poll.errorMessage || 'unknown error'}`)\n  }\n}\n\nif (!txHash) throw new Error('Order did not settle after 90s — check manually on Polygonscan')\n```\n\n**On success**, show the user:\n```\n✅ Bet confirmed on-chain!\n   Match:      {teams}\n   Selection:  {market} — {selection} @ {odds}\n   Stake:      {stake} USDT\n   To win:     {potential payout} USDT\n   Tx:         https://polygonscan.com/tx/{txHash}\n```\n\n---\n\n## Flow — resume interrupted flow\n\nIf 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:\n\n1. **Check allowance** — if `allowance(bettor, relayer) >= stake + fee`, approval was already sent. Skip Step 5.\n2. **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.\n3. If no pending bet found and no clear state, start the flow from Step 1 with a fresh game fetch.\n\n---\n\n## Flow — check bet status\n\nQuery the bets subgraph to see pending, resolved, or redeemable bets.\n\n```\nPOST https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3\nContent-Type: application/json\n\n{\n  \"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 } }\",\n  \"variables\": {\n    \"where\": { \"bettor\": \"<address in lowercase>\" },\n    \"first\": 50,\n    \"orderBy\": \"createdBlockTimestamp\",\n    \"orderDirection\": \"desc\"\n  }\n}\n```\n\nTo fetch only redeemable bets: add `\"isRedeemable\": true` to `where`.\n\n**Interpret results:**\n\n| `status` | `result` | `isRedeemable` | Meaning |\n|----------|----------|----------------|---------|\n| `Accepted` | — | false | Pending — not settled yet |\n| `Resolved` | `Won` | true | Won — claim available |\n| `Resolved` | `Lost` | false | Lost |\n| `Canceled` | — | true | Canceled — stake refundable |\n| `Resolved` | `Won` | false | Already claimed |\n\n**On invocation, always check for newly resolved bets** from previous sessions and surface them proactively:\n> \"You have a resolved bet: {match} — {selection} — **{Won/Lost}**. Payout: {payout} USDT.\"\n> If won: \"Would you like to claim your winnings?\"\n\n---\n\n## Flow — claim\n\nOnly for bets where `isRedeemable === true` and `isRedeemed === false`.\n\n**CRITICAL — use the bundled script, do not implement the claim flow manually.**\n\n```bash\n# Check redeemable bets and claim all:\nnode scripts/claim-bets.js\n\n# Check only (no claim):\nnode scripts/claim-bets.js --check-only\n\n# Claim specific betIds directly:\nnode scripts/claim-bets.js --betIds 42 43\n```\n\nThe script: queries the bets subgraph for redeemable bets, shows a summary with total claimable USDT, asks for explicit user confirmation, verifies the claim contract address, sends the transaction, and waits for on-chain confirmation.\n\nThe steps below are reference documentation for what the script does internally.\n\n### Step 1 — Call Pinwin\n\n```\nPOST https://api.pinwin.xyz/agent/claim\nContent-Type: application/json\n\n{ \"betIds\": [<betId>, ...], \"chain\": \"polygon\" }\n```\n\nDecode: `payload = JSON.parse(atob(response.encoded))`\n\nExplain 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.\"\n\nDisplay the full decoded payload: `to`, `data`, `value`, `chainId`.\n\n### Step 2 — Verify claim contract\n\n**CRITICAL:** `payload.to.toLowerCase()` must equal `0x0fa7fb5407ea971694652e6e16c12a52625de1b8`.\n\nIf it does not match, **do not send the transaction**. Report the mismatch and stop.\n\n### Step 3 — Send and confirm\n\n```js\nconst claimTx = await walletClient.sendTransaction({\n  to: payload.to,\n  data: payload.data,\n  value: 0n,\n  chainId: payload.chainId,\n})\nawait publicClient.waitForTransactionReceipt({ hash: claimTx })\n```\n\nOn success, show:\n```\n✅ Winnings claimed!\n   Bet IDs:  {betIds}\n   Tx:       https://polygonscan.com/tx/{claimTx}\n```\n\n---\n\n## Tools summary\n\n| Step | Tool | Purpose |\n|------|------|---------|\n| Balance check | viem `getBalance`, `readContract` | POL gas, USDT balance and allowance |\n| Fetch games | Data-feed subgraph (GraphQL) | Prematch/live games, conditions, odds |\n| Translate odds | `@azuro-org/dictionaries` | `outcomeId` → human-readable market/selection names |\n| Place bet | `POST /agent/bet` | Get encoded EIP-712 payload |\n| Sign | viem `signTypedData` | EIP-712 bet signature |\n| Submit | Azuro order API (`payload.apiUrl`) | Submit signed bet |\n| Poll | `GET {apiBase}/bet/orders/{orderId}` | Wait for `txHash` confirmation |\n| Watch result | `scripts/watch-bets.js` (auto-launched by `place-bet.js`) | Waits until kickoff + 2h, queries bets subgraph, notifies user via sendPrompt |\n| Bet status | Bets subgraph (GraphQL) | Check status, result, isRedeemable |\n| Claim | `scripts/claim-bets.js` | Check redeemable bets, confirm with user, redeem on-chain |\n\n---\n\n## Error handling\n\n| Error | Cause | Action |\n|-------|-------|--------|\n| `state: \"Rejected\"` / `\"Canceled\"` on order | Invalid bet, market moved, odds expired | Report `errorMessage`, do not retry automatically |\n| Empty order ID from submission | Condition no longer Active (market closed) | Re-check condition state, re-fetch games |\n| `allowance` < required | USDT not approved | Run Step 5 (approve) before signing |\n| `txHash` not received after 90s | Relayer slow or order stuck | Show last known order state, give orderId for manual check on Polygonscan |\n| Subgraph returns 200 with `data.errors` | GraphQL error | Read `data.errors[0].message`, report to user |\n| POL balance = 0 | No gas | Inform user to fund wallet with POL for gas |\n| 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. |\n\n---\n\nFile v1.3.1:README.md\n\n# Sports betting skill\n\nAI agent skill for **Polygon** betting via [Pinwin](https://pinwin.xyz), with on-chain execution. This skill covers **Polygon only**. Fetch prematch and live games, markets, and odds from the **Azuro subgraph**; optionally place bets and claim winnings through the Pinwin API. Use it from [OpenClaw](https://docs.openclaw.ai) (or any AgentSkills-compatible assistant)—e.g. to list games only, or to bet and claim.\n\n- **Entry point:** [SKILL.md](SKILL.md). Reference details in **references/** (api, subgraph, dictionaries, polygon, viem).\n- **Invocation:** The skill is **invocation-only** (`disable-model-invocation: true`): the assistant will not use it automatically. You must explicitly ask (e.g. “place a bet with Pinwin”) or use the slash command (e.g. `/sports_betting`). This avoids accidental bets.\n- **Packaging:** ZIP this folder for upload (ClawHub, etc.).\n\nFile v1.3.1:_meta.json\n\n{\n  \"ownerId\": \"kn7cxsyqe45fp21d6pn9wp7g6n81d0gn\",\n  \"slug\": \"sports-betting\",\n  \"version\": \"1.3.1\",\n  \"publishedAt\": 1775222197019\n}\n\nFile v1.3.1:scripts/package-lock.json\n\n{\n  \"name\": \"scripts\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"dependencies\": {\n        \"@azuro-org/dictionaries\": \"^3.0.28\"\n      }\n    },\n    \"node_modules/@azuro-org/dictionaries\": {\n      \"version\": \"3.0.28\",\n      \"resolved\": \"https://registry.npmjs.org/@azuro-org/dictionaries/-/dictionaries-3.0.28.tgz\",\n      \"integrity\": \"sha512-OwuLSYWaHgHOo2RF7aUHA3HJpT7b6FJqVAKTacEFcsNQgDw7Dx5XQfcVYalHMB/bD3MsjsBLsYfwfvS4cUfRNQ==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"minimist\": \"^1.2.7\"\n      },\n      \"bin\": {\n        \"get-outcomes\": \"bin/get-outcomes/index.js\"\n      }\n    },\n    \"node_modules/minimist\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz\",\n      \"integrity\": \"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    }\n  }\n}\n\nFile v1.3.1:scripts/package.json\n\n{\n  \"dependencies\": {\n    \"@azuro-org/dictionaries\": \"^3.0.28\",\n    \"dotenv\": \"^16.0.0\",\n    \"viem\": \"^2.0.0\"\n  }\n}\n\nFile v1.3.1:skill-card.md\n\n## Description:\n\nSports Betting helps an agent browse Polygon sports markets through Pinwin and Azuro, then place, monitor, and claim on-chain bets when the user explicitly requests it.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[skinnynoizze](https://clawhub.ai/user/skinnynoizze)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal users use this skill to browse decentralized sports markets, place Pinwin/Azuro bets on Polygon, monitor bet status, and claim winnings from a wallet they control.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can sign and submit real-money wallet transactions using BETTOR_PRIVATE_KEY.\n\nMitigation: Use a dedicated low-balance betting wallet, keep the private key outside agent-readable config when possible, and require explicit confirmation before every transaction.\n\nRisk: A confirmation-bypass flag can skip the interactive confirmation in the betting script.\n\nMitigation: Do not use the bypass flag for normal operation; manually review the decoded payload, stake, odds, fees, addresses, and transaction before signing.\n\nRisk: Pinwin/Azuro remote payloads and market data may have validation gaps.\n\nMitigation: Treat remote payloads as trusted only after checking condition, outcome, stake, odds, relayer fee, contract addresses, and current market state.\n\n## Reference(s):\n\n- [ClawHub Skill Page](https://clawhub.ai/skinnynoizze/skills/sports-betting)\n- [Publisher Profile](https://clawhub.ai/user/skinnynoizze)\n- [Pinwin](https://pinwin.xyz)\n- [Azuro Data Feed](https://api.onchainfeed.org/api/v1/public/market-manager/)\n- [Azuro Bets Subgraph](https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3)\n- [Polygonscan Transaction Lookup](https://polygonscan.com/tx/{txHash})\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown guidance with shell commands and script JSON output]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires Node and BETTOR_PRIVATE_KEY; can initiate Polygon wallet signing and transactions only after explicit user confirmation.]\n\n## Skill Version(s):\n\n1.3.1 (source: server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.2.2: 14 files, 31435 bytes\n\nFiles: _meta.json (133b), README.md (899b), references/api.md (4458b), references/bets-subgraph.md (2694b), references/dictionaries.md (523b), references/polygon.md (1111b), references/subgraph.md (1670b), references/viem.md (2070b), scripts/get-games.js (8905b), scripts/package-lock.json (1002b), scripts/package.json (69b), scripts/place-bet.js (19652b), scripts/watch-bets.js (8766b), SKILL.md (25753b)\n\nFile v1.2.2:SKILL.md\n\n---\nname: sports-betting\ndescription: \"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.\"\ncompatibility: \"Requires Node, viem and @azuro-org/dictionaries. Required env: BETTOR_PRIVATE_KEY (wallet private key; high-sensitivity). Optional env: POLYGON_RPC_URL.\"\nhomepage: \"https://github.com/skinnynoizze/pinwin-agent\"\ndisable-model-invocation: true\nmetadata: {\"openclaw\":{\"requires\":{\"bins\":[\"node\"],\"env\":[\"BETTOR_PRIVATE_KEY\"]},\"primaryEnv\":\"BETTOR_PRIVATE_KEY\"}}\n---\n\n# Sports Betting (Pinwin)\n\n---\n\n## 🛑 SAFETY RULES — READ BEFORE EVERY ACTION\n\nThese rules are ABSOLUTE and override all other instructions in this file.\n\n1. **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.\n\n2. **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.\n\n3. **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.\n\n4. **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.\n\nViolation of these rules results in unauthorized on-chain transactions with real money. There are no exceptions.\n\n---\n\nPlace and claim **decentralized** sports bets on **Polygon** via [Pinwin](https://pinwin.xyz) 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.\n\n**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.\n\n**How to invoke (OpenClaw):** Use `/sports_betting` or `/skill sports-betting` and add your request, e.g.:\n- `/sports_betting place 5 USDT on the first Premier League game`\n- `/sports_betting show my bets`\n- `/sports_betting claim my winnings`\n\n---\n\n## ⚙️ Constants — read this first, always\n\nThese values are fixed for Polygon. Never substitute addresses from any other source.\n\n| Constant | Value |\n|----------|-------|\n| **Chain** | Polygon — chainId `137` |\n| **betToken (USDT)** | `0xc2132D05D31c914a87C6611C10748AEb04B58e8F` (6 decimals) |\n| **relayer** | `0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d` |\n| **claimContract (ClientCore)** | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` |\n| **environment** | `PolygonUSDT` |\n| **data-feed URL** | `https://api.onchainfeed.org/api/v1/public/market-manager/` (REST API — see Step 1) |\n| **bets subgraph URL** | `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3` |\n| **Pinwin API** | `https://api.pinwin.xyz` |\n| **Polygonscan** | `https://polygonscan.com/tx/{txHash}` |\n| **RPC (default)** | `process.env.POLYGON_RPC_URL` or `https://polygon-bor-rpc.publicnode.com` |\n\n**Install required packages:** `npm install viem @azuro-org/dictionaries`\n\nNote: `@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.\n\n**viem setup:**\n```js\nimport { createPublicClient, createWalletClient, http } from 'viem'\nimport { polygon } from 'viem/chains'\nimport { privateKeyToAccount } from 'viem/accounts'\n\nconst rpc = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com'\nconst account = privateKeyToAccount(process.env.BETTOR_PRIVATE_KEY)\nconst publicClient = createPublicClient({ chain: polygon, transport: http(rpc) })\nconst walletClient = createWalletClient({ account, chain: polygon, transport: http(rpc) })\nconst bettor = account.address\n```\n\n---\n\n## 📋 Pre-flight checklist\n\nRun this before every bet. Do not skip any item.\n\n- [ ] **BETTOR_PRIVATE_KEY** is set and wallet address derived\n- [ ] **POL balance** ≥ enough for gas (`publicClient.getBalance({ address: bettor })`)\n- [ ] **USDT balance** ≥ stake (`readContract` on betToken with `balanceOf`)\n- [ ] **Selected condition** `state === \"Active\"` — re-check immediately before calling `/agent/bet`, not just at game fetch time\n- [ ] **Allowance** checked and approved if needed (see Step 5)\n\nIf any check fails, inform the user and stop. Do not proceed.\n\n---\n\n## Flow — place a bet\n\n### Step 0 — Check balances\n\n```js\nconst erc20Abi = parseAbi([\n  'function balanceOf(address) view returns (uint256)',\n  'function allowance(address,address) view returns (uint256)',\n  'function approve(address,uint256) returns (bool)',\n])\nconst USDT = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'\n\nconst pol = await publicClient.getBalance({ address: bettor })\nconst usdt = await publicClient.readContract({ address: USDT, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })\n```\n\n- `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.\"\n- `usdt` must be ≥ stake in 6-decimal units (e.g. 2 USDT = `2000000n`)\n- If either is insufficient, stop and inform the user\n\n### Step 1 — Fetch games\n\n**CRITICAL — use the bundled script, do not call the REST API manually.**\n\nThe 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.\n\n```bash\n# Install once (if not already installed):\nnpm install @azuro-org/dictionaries\n\n# Browse by sport/league:\nnode scripts/get-games.js                              # top 20 games, all sports\nnode scripts/get-games.js basketball nba 10            # NBA only\nnode scripts/get-games.js football premier-league 10   # Premier League\nnode scripts/get-games.js hockey 5                     # NHL\n\n# Search by team or match name:\nnode scripts/get-games.js --search \"Real Madrid\"       # find Real Madrid games\nnode scripts/get-games.js --search \"Celtics\" 3         # find Celtics games\nnode scripts/get-games.js --search \"Lakers vs\" 5       # find Lakers matchups\n```\n\n**When to use --search vs sport/league filter:**\n- Use `--search` when the user mentions a specific team, player, or match by name\n- Use sport/league filters when the user asks for a list of games (e.g. \"show me NBA tonight\")\n- `--search` queries across all sports and leagues simultaneously\n\nThe script outputs:\n1. A **clean human-readable list** with main market odds per game — show this to the user\n2. A `---JSON---` block with machine-readable data — use the `conditionId` and `outcomeId` values from this for bet placement in Step 2\n\nOptional 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.\n\n**The script handles all translation and filtering automatically.** Show its human-readable output directly to the user.\n\n**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`.\n\n**CRITICAL — how to identify the main market condition:**\n\nEach 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.\n\n**Verified main market names from `@azuro-org/dictionaries` (confirmed by scanning the full package):**\n\n| Market name | Outcomes | Sports |\n|-------------|----------|--------|\n| `\"Match Winner\"` | 2: `\"1\"`, `\"2\"` | Basketball (NBA), Tennis, Esports, most 1v1 |\n| `\"Full Time Result\"` | 3: `\"1\"`, `\"X\"`, `\"2\"` | Football/Soccer |\n| `\"Winner\"` | 2: `\"1\"`, `\"2\"` | Hockey (NHL), MMA, some others |\n| `\"Fight Winner\"` | 2: `\"1\"`, `\"2\"` | MMA/Boxing specifically |\n| `\"Whole game - Full time result Goal\"` | 3: `\"1\"`, `\"X\"`, `\"2\"` | Football variant |\n\nThese are the **exact strings** returned by `getMarketName({ outcomeId })`. Do not invent or assume market names — always derive them from the dictionary.\n\n```js\nimport { getMarketName, getSelectionName } from '@azuro-org/dictionaries'\n\n// All known main market names — verified against @azuro-org/dictionaries\nconst MAIN_MARKET_NAMES = [\n  'Match Winner',                        // Basketball, Tennis, Esports\n  'Full Time Result',                    // Football/Soccer (3-way: 1 X 2)\n  'Winner',                              // Hockey (NHL), MMA, others\n  'Fight Winner',                        // MMA/Boxing\n  'Whole game - Full time result Goal',  // Football variant\n]\n\n// For each game, find the main market condition:\nconst mainCondition = game.conditions\n  .filter(c => c.state === 'Active')\n  .find(c => {\n    try {\n      const name = getMarketName({ outcomeId: c.outcomes[0].outcomeId })\n      return MAIN_MARKET_NAMES.includes(name)\n    } catch { return false }\n  })\n\nif (!mainCondition) {\n  // No main market active — skip game in default view\n  return\n}\n\n// Map outcome selections to display labels:\n// \"1\" = participants[0] (home), \"2\" = participants[1] (away), \"X\" = Draw\nmainCondition.outcomes.forEach(o => {\n  const selection = getSelectionName({ outcomeId: o.outcomeId, withPoint: true })\n  const label = selection === '1' ? game.participants[0].name\n              : selection === '2' ? game.participants[1].name\n              : 'Draw'\n  console.log(label, '@', o.currentOdds)\n})\n```\n\n- If multiple Active conditions match `MAIN_MARKET_NAMES`, use the first one in the array.\n- 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.\n- **Never hardcode outcomeIds** — always resolve market names via `getMarketName` at runtime.\n\nExample default output:\n```\n🏀 NBA — Tonight\n1. Boston Celtics vs Memphis Grizzlies  [Prematch, 01:00]\n   Moneyline: Celtics 1.07 | Grizzlies 7.92\n\n2. Toronto Raptors vs Denver Nuggets  [Prematch, 00:30]\n   Moneyline: Raptors 3.21 | Nuggets 1.31\n\n3. Dallas Mavericks vs Cleveland Cavaliers  [LIVE 🔴]\n   Moneyline: Mavericks 2.10 | Cavaliers 1.68\n```\n\nNever 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.\"\n\n### Step 2 — Choose selection\n\nAsk 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.\n\n```\n// From the script's JSON output, each selection has:\n{ label: \"Golden State Warriors\", odds: \"2.67\", outcomeId: 6983, conditionId: \"1006...\" }\n```\n\nGet from the chosen selection:\n- `conditionId` (string) — from the JSON output\n- `outcomeId` (number) — from the JSON output  \n- `currentOdds` (string) — from the JSON output (`odds` field)\n\n**CRITICAL — use the bundled script for bet placement, do not implement Steps 3-7 inline.**\n\nOnce the user confirms, run:\n\n```bash\nnode scripts/place-bet.js --stake <USDT> --outcome <outcomeId> --condition <conditionId> --odds <currentOdds> --starts-at <startsAt> --match \"<Team A vs Team B>\"\n```\n\nExample:\n```bash\nnode scripts/place-bet.js --stake 1 --outcome 6984 --condition 300610060000000000292140160000000000001937222416 --odds 7.92 --starts-at 1774047000 --match \"Boston Celtics vs Memphis Grizzlies\"\n```\n\n- `--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`.\n- If `--starts-at` is provided, the script automatically launches `watch-bets.js` in background after the bet is confirmed. No extra action needed.\n\nThe 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).\n\nCompute `minOdds` (for reference only — the script does this internally):\n- Single bet: `minOdds = Math.round(parseFloat(currentOdds) * 1e12)`\n- Combo bet: multiply each leg's odds in 12-decimal space, dividing by `1e12n` per extra leg:\n\n```js\n// Example: 2-leg combo with odds 2.5 and 1.8 → combined odds 4.5\n// leg1: 2.5 → 2_500_000_000_000n\n// leg2: 1.8 → 1_800_000_000_000n\n// combined: (2_500_000_000_000n * 1_800_000_000_000n) / 1_000_000_000_000n = 4_500_000_000_000n\n\nconst toOdds12 = (o) => BigInt(Math.round(parseFloat(o) * 1e12))\nconst minOdds = [odds1, odds2, ...oddsN].reduce(\n  (acc, o) => (acc * toOdds12(o)) / 1_000_000_000_000n,\n  1_000_000_000_000n\n)\n```\n\n**CRITICAL — re-check the condition is still Active immediately before calling `/agent/bet`:**\n```json\nGET 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\n```\nIf `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.\n\n### Step 3 — Call Pinwin\n\n> **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.**\n\n```\nPOST https://api.pinwin.xyz/agent/bet\nContent-Type: application/json\n\n{\n  \"amount\": <stake in USDT 6-decimal units, e.g. 2000000 for 2 USDT>,\n  \"minOdds\": <computed above>,\n  \"chain\": \"polygon\",\n  \"selections\": [{ \"conditionId\": \"<string>\", \"outcomeId\": <number> }]\n}\n```\n\nFor combo, add more objects to `selections`.\n\nResponse: `{ \"encoded\": \"<base64>\" }`. Decode:\n```js\nconst payload = JSON.parse(atob(response.encoded))\n```\n\n### Step 4 — Explain to user before signing\n\n**Always show the full decoded payload before any approval or signing.** The user must understand and confirm what they are authorising.\n\nDisplay:\n- Stake: `amount` in USDT (divide by 1e6), e.g. \"2.00 USDT\"\n- Selection(s): human-readable names (from dictionaries), conditionId, outcomeId\n- Relayer fee: `relayerFeeAmount` (divide by 1e6), e.g. \"0.05 USDT\"\n- Total USDT needed: stake + relayerFeeAmount\n- `apiUrl`, `environment`, `expiresAt`, `affiliate`, `isFeeSponsored`, `isBetSponsored`\n\nThen ask for explicit confirmation before proceeding.\n\n### Step 5 — Approve USDT (if needed)\n\n**CRITICAL — never skip this step.**\n\n```js\nconst relayer = '0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d'\nconst USDT    = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'\nconst relayerFeeAmount = BigInt(payload.signableClientBetData.clientData?.relayerFeeAmount ?? 0)\nconst stakeAmount      = BigInt(payload.signableClientBetData.bet?.amount ?? payload.signableClientBetData.amount)\nconst buffer           = 200000n // 0.2 USDT in 6-decimal units\nconst required         = stakeAmount + relayerFeeAmount + buffer\n\nconst allowance = await publicClient.readContract({\n  address: USDT, abi: erc20Abi, functionName: 'allowance', args: [bettor, relayer]\n})\n\nif (allowance < required) {\n  const approveTx = await walletClient.sendTransaction({\n    to: USDT,\n    data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [relayer, required] })\n  })\n  await publicClient.waitForTransactionReceipt({ hash: approveTx })\n  // Inform user: \"USDT approval confirmed.\"\n}\n```\n\n### Step 6 — Verify payload\n\nBefore signing, verify:\n\n1. `payload.signableClientBetData.bet.amount` (single) or `payload.signableClientBetData.amount` (combo) matches the user's requested stake\n2. `conditionId` and `outcomeId` in the payload match the user's chosen selection(s)\n3. `payload.signableClientBetData.clientData.core.toLowerCase()` === `0xf9548be470a4e130c90cea8b179fcd66d2972ac7` (claimContract)\n\nIf any check fails, **do not sign**. Report the mismatch to the user and stop.\n\n### Step 7 — Sign, submit, and poll\n\n```js\n// Determine primaryType\nconst primaryType = payload.types.ClientComboBetData ? 'ClientComboBetData' : 'ClientBetData'\n\n// Sign\nconst bettorSignature = await walletClient.signTypedData({\n  account,\n  domain: payload.domain,\n  types: payload.types,\n  primaryType,\n  message: payload.signableClientBetData,\n})\n\n// Submit to Azuro order API\nconst submitRes = await fetch(payload.apiUrl, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    environment: payload.environment,\n    bettor,\n    betOwner: bettor,\n    clientBetData: payload.apiClientBetData,\n    bettorSignature,\n  })\n})\nconst submitData = await submitRes.json()\n```\n\nIf submission fails (4xx/5xx or `state: \"Rejected\"` / `\"Canceled\"`), report `submitData.errorMessage` or `submitData.error` to the user and stop.\n\n**CRITICAL — poll until txHash. Do not declare success on `state: \"Created\"`.**\n\nThe order is only on-chain when `txHash` is present. `state: \"Created\"` is only an initial state — the bet is NOT confirmed yet.\n\n```js\n// Derive base URL from apiUrl\n// e.g. \"https://api.onchainfeed.org/v1/bet/orders/ordinar\" → \"https://api.onchainfeed.org/v1\"\nconst apiBase = payload.apiUrl.replace(/\\/bet\\/orders\\/(ordinar|combo)$/, '')\nconst orderId = submitData.id\n\nlet txHash = null\nfor (let i = 0; i < 30; i++) {\n  const delayMs = Math.min(2000 + (i * 1000), 10000) // progressive backoff: 2s, 3s, 4s … 10s max\n  await new Promise(r => setTimeout(r, delayMs))\n  const poll = await fetch(`${apiBase}/bet/orders/${orderId}`).then(r => r.json())\n  if (poll.txHash) { txHash = poll.txHash; break }\n  if (poll.state === 'Rejected' || poll.state === 'Canceled') {\n    // Report failure and stop\n    throw new Error(`Order ${poll.state}: ${poll.errorMessage || 'unknown error'}`)\n  }\n}\n\nif (!txHash) throw new Error('Order did not settle after 90s — check manually on Polygonscan')\n```\n\n**On success**, show the user:\n```\n✅ Bet confirmed on-chain!\n   Match:      {teams}\n   Selection:  {market} — {selection} @ {odds}\n   Stake:      {stake} USDT\n   To win:     {potential payout} USDT\n   Tx:         https://polygonscan.com/tx/{txHash}\n```\n\n---\n\n## Flow — resume interrupted flow\n\nIf 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:\n\n1. **Check allowance** — if `allowance(bettor, relayer) >= stake + fee`, approval was already sent. Skip Step 5.\n2. **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.\n3. If no pending bet found and no clear state, start the flow from Step 1 with a fresh game fetch.\n\n---\n\n## Flow — check bet status\n\nQuery the bets subgraph to see pending, resolved, or redeemable bets.\n\n```\nPOST https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3\nContent-Type: application/json\n\n{\n  \"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 } }\",\n  \"variables\": {\n    \"where\": { \"bettor\": \"<address in lowercase>\" },\n    \"first\": 50,\n    \"orderBy\": \"createdBlockTimestamp\",\n    \"orderDirection\": \"desc\"\n  }\n}\n```\n\nTo fetch only redeemable bets: add `\"isRedeemable\": true` to `where`.\n\n**Interpret results:**\n\n| `status` | `result` | `isRedeemable` | Meaning |\n|----------|----------|----------------|---------|\n| `Accepted` | — | false | Pending — not settled yet |\n| `Resolved` | `Won` | true | Won — claim available |\n| `Resolved` | `Lost` | false | Lost |\n| `Canceled` | — | true | Canceled — stake refundable |\n| `Resolved` | `Won` | false | Already claimed |\n\n**On invocation, always check for newly resolved bets** from previous sessions and surface them proactively:\n> \"You have a resolved bet: {match} — {selection} — **{Won/Lost}**. Payout: {payout} USDT.\"\n> If won: \"Would you like to claim your winnings?\"\n\n---\n\n## Flow — claim\n\nOnly for bets where `isRedeemable === true` and `isRedeemed === false`. Collect `betId` values from the subgraph.\n\n### Step 1 — Call Pinwin\n\n```\nPOST https://api.pinwin.xyz/agent/claim\nContent-Type: application/json\n\n{ \"betIds\": [<betId>, ...], \"chain\": \"polygon\" }\n```\n\nDecode: `payload = JSON.parse(atob(response.encoded))`\n\nExplain 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.\"\n\nDisplay the full decoded payload: `to`, `data`, `value`, `chainId`.\n\n### Step 2 — Verify claim contract\n\n**CRITICAL:** `payload.to.toLowerCase()` must equal `0xf9548be470a4e130c90cea8b179fcd66d2972ac7`.\n\nIf it does not match, **do not send the transaction**. Report the mismatch and stop.\n\n### Step 3 — Send and confirm\n\n```js\nconst claimTx = await walletClient.sendTransaction({\n  to: payload.to,\n  data: payload.data,\n  value: 0n,\n  chainId: payload.chainId,\n})\nawait publicClient.waitForTransactionReceipt({ hash: claimTx })\n```\n\nOn success, show:\n```\n✅ Winnings claimed!\n   Bet IDs:  {betIds}\n   Tx:       https://polygonscan.com/tx/{claimTx}\n```\n\n---\n\n## Tools summary\n\n| Step | Tool | Purpose |\n|------|------|---------|\n| Balance check | viem `getBalance`, `readContract` | POL gas, USDT balance and allowance |\n| Fetch games | Data-feed subgraph (GraphQL) | Prematch/live games, conditions, odds |\n| Translate odds | `@azuro-org/dictionaries` | `outcomeId` → human-readable market/selection names |\n| Place bet | `POST /agent/bet` | Get encoded EIP-712 payload |\n| Sign | viem `signTypedData` | EIP-712 bet signature |\n| Submit | Azuro order API (`payload.apiUrl`) | Submit signed bet |\n| Poll | `GET {apiBase}/bet/orders/{orderId}` | Wait for `txHash` confirmation |\n| Watch result | `scripts/watch-bets.js` (auto-launched by `place-bet.js`) | Waits until kickoff + 2h, queries bets subgraph, notifies user via sendPrompt |\n| Bet status | Bets subgraph (GraphQL) | Check status, result, isRedeemable |\n| Claim | `POST /agent/claim` + viem `sendTransaction` | Redeem winnings on-chain |\n\n---\n\n## Error handling\n\n| Error | Cause | Action |\n|-------|-------|--------|\n| `state: \"Rejected\"` / `\"Canceled\"` on order | Invalid bet, market moved, odds expired | Report `errorMessage`, do not retry automatically |\n| Empty order ID from submission | Condition no longer Active (market closed) | Re-check condition state, re-fetch games |\n| `allowance` < required | USDT not approved | Run Step 5 (approve) before signing |\n| `txHash` not received after 90s | Relayer slow or order stuck | Show last known order state, give orderId for manual check on Polygonscan |\n| Subgraph returns 200 with `data.errors` | GraphQL error | Read `data.errors[0].message`, report to user |\n| POL balance = 0 | No gas | Inform user to fund wallet with POL for gas |\n| 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. |\n\n---\n\n## Reference files\n\nLoad only if you need full detail beyond what is in this file:\n\n- [`references/api.md`](references/api.md) — Full Pinwin `/agent/bet` and `/agent/claim` request/response schemas, EIP-712 payload structure, combo bet details\n- [`references/subgraph.md`](references/subgraph.md) — Data-feed GraphQL query, all filter/orderBy options, full response shape\n- [`references/bets-subgraph.md`](references/bets-subgraph.md) — Bets subgraph query, all fields, pagination\n- [`references/dictionaries.md`](references/dictionaries.md) — `@azuro-org/dictionaries` full API\n- [`references/viem.md`](references/viem.md) — viem full setup, all chain calls\n- [`references/polygon.md`](references/polygon.md) — All Polygon addresses and URLs (same as Constants table above)\n\nFile v1.2.2:README.md\n\n# Sports betting skill\n\nAI agent skill for **Polygon** betting via [Pinwin](https://pinwin.xyz), with on-chain execution. This skill covers **Polygon only**. Fetch prematch and live games, markets, and odds from the **Azuro subgraph**; optionally place bets and claim winnings through the Pinwin API. Use it from [OpenClaw](https://docs.openclaw.ai) (or any AgentSkills-compatible assistant)—e.g. to list games only, or to bet and claim.\n\n- **Entry point:** [SKILL.md](SKILL.md). Reference details in **references/** (api, subgraph, dictionaries, polygon, viem).\n- **Invocation:** The skill is **invocation-only** (`disable-model-invocation: true`): the assistant will not use it automatically. You must explicitly ask (e.g. “place a bet with Pinwin”) or use the slash command (e.g. `/sports_betting`). This avoids accidental bets.\n- **Packaging:** ZIP this folder for upload (ClawHub, etc.).\n\nFile v1.2.2:_meta.json\n\n{\n  \"ownerId\": \"kn7cxsyqe45fp21d6pn9wp7g6n81d0gn\",\n  \"slug\": \"sports-betting\",\n  \"version\": \"1.2.2\",\n  \"publishedAt\": 1774186014671\n}\n\nFile v1.2.2:references/api.md\n\n# Pinwin API reference\n\n**Base URL:** `https://api.pinwin.xyz`  \nAll requests: `Content-Type: application/json`. On error (4xx/5xx), body may have `error` or `message`.\n\n## POST /agent/bet\n\n**Request:** `amount` (number, USDT smallest units, 6 decimals), `minOdds` (positive integer, 12 decimals), `chain: \"polygon\"`, `selections` (array of `{ conditionId: string, outcomeId: number }`).\n\n**Response 200:** `{ \"encoded\": \"<base64>\" }`. Decode: `payload = JSON.parse(atob(response.encoded))`.\n\n**Decoded payload:** `signableClientBetData`, `apiClientBetData`, `domain`, `types`, `apiUrl`, `environment`.\n\n**Decoded payload structure (Azuro-aligned).** The payload matches the Azuro V3 order API. Top-level: `signableClientBetData`, `apiClientBetData`, `domain`, `types`, `apiUrl`, `environment`. **clientData** (under `signableClientBetData` or `apiClientBetData`): `attention`, `affiliate`, `core`, `expiresAt`, `chainId`, `relayerFeeAmount`, `isFeeSponsored`, `isBetSponsored`, `isSponsoredBetReturnable`. **Single bet:** `signableClientBetData.bet` has `conditionId`, `outcomeId`, `minOdds`, `amount`, `nonce`. **Combo bet:** `signableClientBetData.bets` (array of `{ conditionId, outcomeId }`), and top-level `amount`, `minOdds`, `nonce` on the signable message.\n\n**Before signing:** (1) **Display to the user all decoded payload fields** relevant to the bet: amount (from `bet.amount` or combo `amount`), selections (conditionId/outcomeId with human-readable names from data-feed/dictionaries), `relayerFeeAmount`, `apiUrl`, `environment`, and all **clientData** fields (`affiliate`, `core`, `expiresAt`, `chainId`, `attention`, `isFeeSponsored`, `isBetSponsored`, `isSponsoredBetReturnable`). (2) Then give a short **human-readable summary**: stake in USDT, selection/market names, relayer fee, and that this is the bet they are authorising. (3) **Verify before signing:** the payload’s amount and selections (conditionId/outcomeId or combo `bets[]`) must match the user’s requested stake and chosen selections. Verify that the payload’s **clientData.core** (lowercase) equals the **claimContract** (ClientCore) for Polygon in [polygon.md](polygon.md); if not, do not sign and report the mismatch. Use only the **relayer** from [polygon.md](polygon.md) for allowance and approve (do not use any address from the payload as relayer). If amount or selections do not match, do not sign and report the mismatch to the user.\n\n**EIP-712 primaryType:** Use `primaryType: 'ClientComboBetData'` if `payload.types.ClientComboBetData` exists, otherwise `primaryType: 'ClientBetData'`. Pass this to viem `signTypedData` along with `domain`, `types`, and `message: payload.signableClientBetData`.\n\n**After sign:** POST to `payload.apiUrl` with body: `environment`, `bettor`, `betOwner`, `clientBetData` (= `payload.apiClientBetData`), `bettorSignature` (hex with `0x`).\n\n**Order submission response (Azuro order API):** JSON with `id` (string, **order id**), `state` (e.g. `Created`, `Rejected`, `Canceled`, `Accepted`), and optional `errorMessage`, `error`. The order id is **always this response `id`** — use it for polling.\n\n**Poll order status (when you have an order id):** Same base URL as `apiUrl` (e.g. strip `/bet/orders/ordinar` or `/bet/orders/combo` to get the base). Request: **GET** `{apiBase}/bet/orders/{orderId}`. Poll until terminal: **success** = response includes `txHash`; **failure** = `state` is `Rejected` or `Canceled` (use `errorMessage` if present). Stop polling when you get `txHash` or a failure state.\n\n## POST /agent/claim\n\n**Request:** `betIds` (number[], on-chain bet ids — e.g. from bets subgraph where `isRedeemable: true`), `chain: \"polygon\"`.\n\n**Response 200:** `{ \"encoded\": \"<base64>\" }`. Decode: `payload = JSON.parse(atob(response.encoded))`.\n\n**Decoded payload:** `to` (contract address), `data` (hex calldata), `value` (\"0\"), `chainId` (number). **Explain to the user in human-readable terms** what the claim tx does: e.g. claiming winnings for bet IDs [X, Y], transaction target (Azuro ClientCore on Polygon), no value sent. You may also display the full decoded payload. Verify `payload.to` (lowercase) equals the **claimContract** (ClientCore) for Polygon in [polygon.md](polygon.md)—this is the redeem contract for won/canceled bets. If it does not match, do not send. Then send tx with viem: `sendTransaction({ to: payload.to, data: payload.data, value: 0n, chainId: payload.chainId })`. Wait for receipt.\n\nFile v1.2.2:references/bets-subgraph.md\n\n# Bets subgraph (history / status)\n\nUse this subgraph to list a bettor’s bets and see whether each is **pending**, **resolved** (won/lost), or **canceled**, and whether it can be **redeemed**. This is a different endpoint from the data-feed (which serves games and conditions).\n\n**URL (Polygon):** `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3`  \n**Header:** `Content-Type: application/json`.  \n**Body:** JSON with `query` (string) and `variables` (object).\n\n## Query\n\n```graphql\nquery BettorBets($where: V3_Bet_filter!, $first: Int, $orderBy: V3_Bet_orderBy, $orderDirection: OrderDirection) {\n  v3Bets(where: $where, first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {\n    betId\n    status\n    result\n    isRedeemable\n    isRedeemed\n    amount\n    payout\n    createdBlockTimestamp\n    resolvedBlockTimestamp\n  }\n}\n```\n\n## Example variables\n\n`bettor` must be the wallet address in **lowercase**.\n\n**All bets (any status):**\n```json\n{\n  \"where\": { \"bettor\": \"0xcdb8eaf44f1e22707df63bfad380b72aa1274e67\" },\n  \"first\": 50,\n  \"orderBy\": \"createdBlockTimestamp\",\n  \"orderDirection\": \"desc\"\n}\n```\n\n**Only redeemable bets** (ready to claim): add `\"isRedeemable\": true` to `where`. Use this to get **betIds** for `POST /agent/claim` without filtering client-side.\n```json\n{\n  \"where\": { \"bettor\": \"0xcdb8eaf44f1e22707df63bfad380b72aa1274e67\", \"isRedeemable\": true },\n  \"first\": 50,\n  \"orderBy\": \"createdBlockTimestamp\",\n  \"orderDirection\": \"desc\"\n}\n```\n\n## Response shape\n\n`data.v3Bets` = array of bets. Each bet:\n\n| Field | Meaning |\n|-------|--------|\n| **betId** | On-chain bet id (number). Use in Pinwin `POST /agent/claim` as `betIds[]`. |\n| **status** | `Accepted` (pending) \\| `Resolved` (settled) \\| `Canceled` |\n| **result** | `Won` \\| `Lost` — only when `status === \"Resolved\"`. |\n| **isRedeemable** | `true` if the user can claim (resolved or canceled, not yet redeemed). |\n| **isRedeemed** | `true` if already claimed. |\n| **amount** | Stake (human-readable). |\n| **payout** | Payout when resolved (human-readable); present when won or canceled. |\n| **createdBlockTimestamp** | When the bet was placed (Unix seconds). |\n| **resolvedBlockTimestamp** | When the bet was settled (Unix seconds); when resolved. |\n\n## How to use\n\n- **Check if a bet is resolved:** `status === \"Resolved\"`. Then **result** tells **Won** or **Lost**.\n- **Check if the user can redeem:** `isRedeemable === true` and `isRedeemed === false`. Collect those bets’ **betId** values and call Pinwin `POST /agent/claim` with `betIds: [<betId>, ...]` and `chain: \"polygon\"`, then sign and send the returned tx. See [api.md](api.md) and the skill Flow (claim).\n\nFile v1.2.2:references/dictionaries.md\n\n# Dictionaries (market/selection names)\n\n**Package:** `@azuro-org/dictionaries` (npm).\n\n- **getMarketName({ outcomeId })** → market label (e.g. \"Total Goals\", \"1st Half - Winner\"). Pass `outcomeId` as returned by the subgraph (string or number).\n- **getSelectionName({ outcomeId, withPoint? })** → selection label (e.g. \"Over\", \"Under\"). Use **withPoint: true** so the line is included when present (e.g. \"Over (2.5)\", \"Under (2.5)\") for markets like Total Goals. On failure, fall back to `outcomeId` or a placeholder.\n\nFile v1.2.2:references/polygon.md\n\n# Polygon addresses\n\nChain ID **137**. Polygon production.\n\n**RPC (defaults):** If `POLYGON_RPC_URL` is not set, use one of these — try in order if the first fails:\n- `https://poly.api.pocket.network`\n- `https://polygon-bor-rpc.publicnode.com` ([PublicNode](https://polygon-bor-rpc.publicnode.com))\n\n| Field | Value |\n|-------|--------|\n| **native gas token** | POL (for gas: approve, claim txs) |\n| **data-feed URL** | `https://thegraph-1.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-data-feed-polygon` |\n| **bets subgraph URL** | `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3` |\n| **relayer** | `0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d` (for USDT approval; verify bet payload does not override) |\n| **betToken** | `0xc2132d05d31c914a87c6611c10748aeb04b58e8f` (USDT, **6 decimals**) |\n| **core** (ClientCore) | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` (for bet payload clientData.core; same as claimContract) |\n| **claimContract** (ClientCore, redeem won/canceled bets) | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` |\n| **environment** | `PolygonUSDT` |\n\nFile v1.2.2:references/subgraph.md\n\n# Data-feed (subgraph) reference\n\n**URL (Polygon):** `https://thegraph-1.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-data-feed-polygon`  \n**Header:** `Content-Type: application/json`.  \n**Body:** JSON with `query` (string) and `variables` (object).  \n**Game state:** `state` = `\"Prematch\"` | `\"Live\"` | `\"Stopped\"` | `\"Finished\"`. Only use conditions with `state: \"Active\"`.\n\n## Canonical query\n\n```graphql\nquery Games($first: Int!, $where: Game_filter!, $orderBy: Game_orderBy!, $orderDirection: OrderDirection!) {\n  games(first: $first, where: $where, orderBy: $orderBy, orderDirection: $orderDirection) {\n    gameId\n    title\n    state\n    startsAt\n    league { name }\n    country { name }\n    sport { name }\n    participants { name }\n    conditions {\n      conditionId\n      state\n      outcomes { outcomeId currentOdds }\n    }\n  }\n}\n```\n\n## Example variables\n\n**Prematch (turnover desc):**  \n`{ \"first\": 20, \"where\": { \"state\": \"Prematch\" }, \"orderBy\": \"turnover\", \"orderDirection\": \"desc\" }`\n\n**Live (turnover desc):**  \n`{ \"first\": 20, \"where\": { \"state\": \"Live\" }, \"orderBy\": \"turnover\", \"orderDirection\": \"desc\" }`\n\n## Filtering and ordering\n\n**Filtering (optional):** `sport_: { slug: \"football\" }`, `country_: { slug: \"germany\" }`, `league_: { slug: \"bundesliga\" }`, `startsAt_gt` (Unix seconds).\n\n**Ordering:** `orderBy`: `turnover` | `startsAt`; `orderDirection`: `asc` | `desc`.\n\n## Response shape\n\n`data.games` = array of games. Each game: `gameId`, `title`, `state`, `startsAt`, `league.name`, `country.name`, `sport.name`, `participants[]` (each `name`), `conditions[]` with `conditionId`, `state`, `outcomes[]` with `outcomeId`, `currentOdds`.\n\nFile v1.2.2:references/viem.md\n\n# Viem (minimal)\n\n**Install:** `npm install viem`.  \n**Setup:** RPC = `process.env.POLYGON_RPC_URL` or first default from [references/polygon.md](references/polygon.md). Create `publicClient` and `walletClient` from private key + `http(rpcUrl)` + chain `polygon` (from `viem/chains`). Use `privateKeyToAccount` from `viem/accounts`.\n\n- **Balance (POL):** `publicClient.getBalance({ address })` — native gas token on Polygon; use to ensure the wallet can pay for approve and claim txs.\n- **Balance (USDT):** `publicClient.readContract({ address: betToken, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })`. Bet token and 6 decimals from [references/polygon.md](references/polygon.md). Require balance ≥ stake + relayerFeeAmount (from payload) before placing a bet.\n- **Allowance:** `publicClient.readContract({ address: betToken, abi: erc20Abi, functionName: 'allowance', args: [bettor, relayer] })`. If result < bet amount + relayerFeeAmount + buffer → approve. Buffer = 0.2 USDT = 200000 (6 decimals).\n- **Approve:** Encode `approve(relayer, betAmount + relayerFeeAmount + 200000)` — i.e. stake + relayer fee + 0.2 USDT buffer. The relayer cannot spend more than needed for this bet plus a small buffer. `walletClient.sendTransaction({ to: betToken, data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [relayer, stake + relayerFeeAmount + 200000n] }) })`. Wait for receipt.\n- **Bet sign:** `walletClient.signTypedData({ account, domain: payload.domain, types: payload.types, primaryType, message: payload.signableClientBetData })`. Use `primaryType: 'ClientComboBetData'` if `payload.types.ClientComboBetData` exists, else `'ClientBetData'`. No tx; then POST signature to `payload.apiUrl`.\n- **Claim:** `walletClient.sendTransaction({ to: payload.to, data: payload.data, value: 0n, chainId: payload.chainId })`. Wait for receipt.\n\nERC-20 ABI (minimal): `allowance(address,address) view returns (uint256)`, `approve(address,uint256) returns (bool)`, `balanceOf(address) view returns (uint256)`. Use viem `parseAbi` or equivalent.\n\nFile v1.2.2:scripts/package-lock.json\n\n{\n  \"name\": \"scripts\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"dependencies\": {\n        \"@azuro-org/dictionaries\": \"^3.0.28\"\n      }\n    },\n    \"node_modules/@azuro-org/dictionaries\": {\n      \"version\": \"3.0.28\",\n      \"resolved\": \"https://registry.npmjs.org/@azuro-org/dictionaries/-/dictionaries-3.0.28.tgz\",\n      \"integrity\": \"sha512-OwuLSYWaHgHOo2RF7aUHA3HJpT7b6FJqVAKTacEFcsNQgDw7Dx5XQfcVYalHMB/bD3MsjsBLsYfwfvS4cUfRNQ==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"minimist\": \"^1.2.7\"\n      },\n      \"bin\": {\n        \"get-outcomes\": \"bin/get-outcomes/index.js\"\n      }\n    },\n    \"node_modules/minimist\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz\",\n      \"integrity\": \"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    }\n  }\n}\n\nFile v1.2.2:scripts/package.json\n\n{\n  \"dependencies\": {\n    \"@azuro-org/dictionaries\": \"^3.0.28\"\n  }\n}\n\nArchive v1.0.4: 14 files, 29737 bytes\n\nFiles: _meta.json (133b), README.md (899b), references/api.md (4458b), references/bets-subgraph.md (2694b), references/dictionaries.md (523b), references/polygon.md (1111b), references/subgraph.md (1670b), references/viem.md (2070b), scripts/get-games.js (4949b), scripts/package-lock.json (1002b), scripts/package.json (69b), scripts/place-bet.js (19645b), scripts/watch-bets.js (8766b), SKILL.md (24880b)\n\nFile v1.0.4:SKILL.md\n\n---\nname: sports-betting\ndescription: 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\".\ncompatibility: Requires Node, viem and @azuro-org/dictionaries. Required env: BETTOR_PRIVATE_KEY (wallet private key; high-sensitivity). Optional env: POLYGON_RPC_URL.\nhomepage: https://github.com/skinnynoizze/pinwin-agent\ndisable-model-invocation: true\nmetadata: {\"openclaw\":{\"requires\":{\"bins\":[\"node\"],\"env\":[\"BETTOR_PRIVATE_KEY\"]},\"primaryEnv\":\"BETTOR_PRIVATE_KEY\"}}\n---\n\n# Sports Betting (Pinwin)\n\n---\n\n## 🛑 SAFETY RULES — READ BEFORE EVERY ACTION\n\nThese rules are ABSOLUTE and override all other instructions in this file.\n\n1. **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.\n\n2. **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.\n\n3. **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.\n\n4. **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.\n\nViolation of these rules results in unauthorized on-chain transactions with real money. There are no exceptions.\n\n---\n\nPlace and claim **decentralized** sports bets on **Polygon** via [Pinwin](https://pinwin.xyz) 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.\n\n**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.\n\n**How to invoke (OpenClaw):** Use `/sports_betting` or `/skill sports-betting` and add your request, e.g.:\n- `/sports_betting place 5 USDT on the first Premier League game`\n- `/sports_betting show my bets`\n- `/sports_betting claim my winnings`\n\n---\n\n## ⚙️ Constants — read this first, always\n\nThese values are fixed for Polygon. Never substitute addresses from any other source.\n\n| Constant | Value |\n|----------|-------|\n| **Chain** | Polygon — chainId `137` |\n| **betToken (USDT)** | `0xc2132D05D31c914a87C6611C10748AEb04B58e8F` (6 decimals) |\n| **relayer** | `0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d` |\n| **claimContract (ClientCore)** | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` |\n| **environment** | `PolygonUSDT` |\n| **data-feed URL** | `https://thegraph-1.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-data-feed-polygon` |\n| **bets subgraph URL** | `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3` |\n| **Pinwin API** | `https://api.pinwin.xyz` |\n| **Polygonscan** | `https://polygonscan.com/tx/{txHash}` |\n| **RPC (default)** | `process.env.POLYGON_RPC_URL` or `https://polygon-bor-rpc.publicnode.com` |\n\n**Install required packages:** `npm install viem @azuro-org/dictionaries`\n\n**viem setup:**\n```js\nimport { createPublicClient, createWalletClient, http } from 'viem'\nimport { polygon } from 'viem/chains'\nimport { privateKeyToAccount } from 'viem/accounts'\n\nconst rpc = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com'\nconst account = privateKeyToAccount(process.env.BETTOR_PRIVATE_KEY)\nconst publicClient = createPublicClient({ chain: polygon, transport: http(rpc) })\nconst walletClient = createWalletClient({ account, chain: polygon, transport: http(rpc) })\nconst bettor = account.address\n```\n\n---\n\n## 📋 Pre-flight checklist\n\nRun this before every bet. Do not skip any item.\n\n- [ ] **BETTOR_PRIVATE_KEY** is set and wallet address derived\n- [ ] **POL balance** ≥ enough for gas (`publicClient.getBalance({ address: bettor })`)\n- [ ] **USDT balance** ≥ stake (`readContract` on betToken with `balanceOf`)\n- [ ] **Selected condition** `state === \"Active\"` — re-check immediately before calling `/agent/bet`, not just at game fetch time\n- [ ] **Allowance** checked and approved if needed (see Step 5)\n\nIf any check fails, inform the user and stop. Do not proceed.\n\n---\n\n## Flow — place a bet\n\n### Step 0 — Check balances\n\n```js\nconst erc20Abi = parseAbi([\n  'function balanceOf(address) view returns (uint256)',\n  'function allowance(address,address) view returns (uint256)',\n  'function approve(address,uint256) returns (bool)',\n])\nconst USDT = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'\n\nconst pol = await publicClient.getBalance({ address: bettor })\nconst usdt = await publicClient.readContract({ address: USDT, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })\n```\n\n- `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.\"\n- `usdt` must be ≥ stake in 6-decimal units (e.g. 2 USDT = `2000000n`)\n- If either is insufficient, stop and inform the user\n\n### Step 1 — Fetch games\n\n**CRITICAL — use the bundled script, do not parse raw subgraph JSON manually.**\n\nThe subgraph returns one row per outcome (not per game). Parsing it with `jq` or `grep` will produce duplicates and wrong odds. The bundled script handles grouping, main market detection, and dictionary translation correctly.\n\n```bash\n# Install once (if not already installed):\nnpm install @azuro-org/dictionaries\n\n# Run the bundled script:\nnode scripts/get-games.js                           # top 20 games, all sports\nnode scripts/get-games.js basketball nba 10         # NBA only\nnode scripts/get-games.js football premier-league 10\nnode scripts/get-games.js football 10               # all football\n```\n\nThe script outputs:\n1. A **clean human-readable list** with main market odds per game — show this to the user\n2. A `---JSON---` block with machine-readable data — use the `conditionId` and `outcomeId` values from this for bet placement in Step 2\n\nOptional 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.\n\n**The script handles all translation and filtering automatically.** Show its human-readable output directly to the user.\n\n**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`.\n\n**CRITICAL — how to identify the main market condition:**\n\nEach 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.\n\n**Verified main market names from `@azuro-org/dictionaries` (confirmed by scanning the full package):**\n\n| Market name | Outcomes | Sports |\n|-------------|----------|--------|\n| `\"Match Winner\"` | 2: `\"1\"`, `\"2\"` | Basketball (NBA), Tennis, Esports, most 1v1 |\n| `\"Full Time Result\"` | 3: `\"1\"`, `\"X\"`, `\"2\"` | Football/Soccer |\n| `\"Winner\"` | 2: `\"1\"`, `\"2\"` | Hockey (NHL), MMA, some others |\n| `\"Fight Winner\"` | 2: `\"1\"`, `\"2\"` | MMA/Boxing specifically |\n| `\"Whole game - Full time result Goal\"` | 3: `\"1\"`, `\"X\"`, `\"2\"` | Football variant |\n\nThese are the **exact strings** returned by `getMarketName({ outcomeId })`. Do not invent or assume market names — always derive them from the dictionary.\n\n```js\nimport { getMarketName, getSelectionName } from '@azuro-org/dictionaries'\n\n// All known main market names — verified against @azuro-org/dictionaries\nconst MAIN_MARKET_NAMES = [\n  'Match Winner',                        // Basketball, Tennis, Esports\n  'Full Time Result',                    // Football/Soccer (3-way: 1 X 2)\n  'Winner',                              // Hockey (NHL), MMA, others\n  'Fight Winner',                        // MMA/Boxing\n  'Whole game - Full time result Goal',  // Football variant\n]\n\n// For each game, find the main market condition:\nconst mainCondition = game.conditions\n  .filter(c => c.state === 'Active')\n  .find(c => {\n    try {\n      const name = getMarketName({ outcomeId: c.outcomes[0].outcomeId })\n      return MAIN_MARKET_NAMES.includes(name)\n    } catch { return false }\n  })\n\nif (!mainCondition) {\n  // No main market active — skip game in default view\n  return\n}\n\n// Map outcome selections to display labels:\n// \"1\" = participants[0] (home), \"2\" = participants[1] (away), \"X\" = Draw\nmainCondition.outcomes.forEach(o => {\n  const selection = getSelectionName({ outcomeId: o.outcomeId, withPoint: true })\n  const label = selection === '1' ? game.participants[0].name\n              : selection === '2' ? game.participants[1].name\n              : 'Draw'\n  console.log(label, '@', o.currentOdds)\n})\n```\n\n- If multiple Active conditions match `MAIN_MARKET_NAMES`, use the first one in the array.\n- 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.\n- **Never hardcode outcomeIds** — always resolve market names via `getMarketName` at runtime.\n\nExample default output:\n```\n🏀 NBA — Tonight\n1. Boston Celtics vs Memphis Grizzlies  [Prematch, 01:00]\n   Moneyline: Celtics 1.07 | Grizzlies 7.92\n\n2. Toronto Raptors vs Denver Nuggets  [Prematch, 00:30]\n   Moneyline: Raptors 3.21 | Nuggets 1.31\n\n3. Dallas Mavericks vs Cleveland Cavaliers  [LIVE 🔴]\n   Moneyline: Mavericks 2.10 | Cavaliers 1.68\n```\n\nNever 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.\"\n\n### Step 2 — Choose selection\n\nAsk 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.\n\n```\n// From the script's JSON output, each selection has:\n{ label: \"Golden State Warriors\", odds: \"2.67\", outcomeId: 6983, conditionId: \"1006...\" }\n```\n\nGet from the chosen selection:\n- `conditionId` (string) — from the JSON output\n- `outcomeId` (number) — from the JSON output  \n- `currentOdds` (string) — from the JSON output (`odds` field)\n\n**CRITICAL — use the bundled script for bet placement, do not implement Steps 3-7 inline.**\n\nOnce the user confirms, run:\n\n```bash\nnode scripts/place-bet.js --stake <USDT> --outcome <outcomeId> --condition <conditionId> --odds <currentOdds> --starts-at <startsAt> --match \"<Team A vs Team B>\"\n```\n\nExample:\n```bash\nnode scripts/place-bet.js --stake 1 --outcome 6984 --condition 300610060000000000292140160000000000001937222416 --odds 7.92 --starts-at 1774047000 --match \"Boston Celtics vs Memphis Grizzlies\"\n```\n\n- `--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`.\n- If `--starts-at` is provided, the script automatically launches `watch-bets.js` in background after the bet is confirmed. No extra action needed.\n\nThe 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).\n\nCompute `minOdds` (for reference only — the script does this internally):\n- Single bet: `minOdds = Math.round(parseFloat(currentOdds) * 1e12)`\n- Combo bet: multiply each leg's odds in 12-decimal space, dividing by `1e12n` per extra leg:\n\n```js\n// Example: 2-leg combo with odds 2.5 and 1.8 → combined odds 4.5\n// leg1: 2.5 → 2_500_000_000_000n\n// leg2: 1.8 → 1_800_000_000_000n\n// combined: (2_500_000_000_000n * 1_800_000_000_000n) / 1_000_000_000_000n = 4_500_000_000_000n\n\nconst toOdds12 = (o) => BigInt(Math.round(parseFloat(o) * 1e12))\nconst minOdds = [odds1, odds2, ...oddsN].reduce(\n  (acc, o) => (acc * toOdds12(o)) / 1_000_000_000_000n,\n  1_000_000_000_000n\n)\n```\n\n**CRITICAL — re-check the condition is still Active immediately before calling `/agent/bet`:**\n```json\nPOST data-feed with where: { \"conditionId\": \"<id>\" }\n```\nIf `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.\n\n### Step 3 — Call Pinwin\n\n> **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.**\n\n```\nPOST https://api.pinwin.xyz/agent/bet\nContent-Type: application/json\n\n{\n  \"amount\": <stake in USDT 6-decimal units, e.g. 2000000 for 2 USDT>,\n  \"minOdds\": <computed above>,\n  \"chain\": \"polygon\",\n  \"selections\": [{ \"conditionId\": \"<string>\", \"outcomeId\": <number> }]\n}\n```\n\nFor combo, add more objects to `selections`.\n\nResponse: `{ \"encoded\": \"<base64>\" }`. Decode:\n```js\nconst payload = JSON.parse(atob(response.encoded))\n```\n\n### Step 4 — Explain to user before signing\n\n**Always show the full decoded payload before any approval or signing.** The user must understand and confirm what they are authorising.\n\nDisplay:\n- Stake: `amount` in USDT (divide by 1e6), e.g. \"2.00 USDT\"\n- Selection(s): human-readable names (from dictionaries), conditionId, outcomeId\n- Relayer fee: `relayerFeeAmount` (divide by 1e6), e.g. \"0.05 USDT\"\n- Total USDT needed: stake + relayerFeeAmount\n- `apiUrl`, `environment`, `expiresAt`, `affiliate`, `isFeeSponsored`, `isBetSponsored`\n\nThen ask for explicit confirmation before proceeding.\n\n### Step 5 — Approve USDT (if needed)\n\n**CRITICAL — never skip this step.**\n\n```js\nconst relayer = '0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d'\nconst USDT    = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'\nconst relayerFeeAmount = BigInt(payload.signableClientBetData.clientData?.relayerFeeAmount ?? 0)\nconst stakeAmount      = BigInt(payload.signableClientBetData.bet?.amount ?? payload.signableClientBetData.amount)\nconst buffer           = 200000n // 0.2 USDT in 6-decimal units\nconst required         = stakeAmount + relayerFeeAmount + buffer\n\nconst allowance = await publicClient.readContract({\n  address: USDT, abi: erc20Abi, functionName: 'allowance', args: [bettor, relayer]\n})\n\nif (allowance < required) {\n  const approveTx = await walletClient.sendTransaction({\n    to: USDT,\n    data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [relayer, required] })\n  })\n  await publicClient.waitForTransactionReceipt({ hash: approveTx })\n  // Inform user: \"USDT approval confirmed.\"\n}\n```\n\n### Step 6 — Verify payload\n\nBefore signing, verify:\n\n1. `payload.signableClientBetData.bet.amount` (single) or `payload.signableClientBetData.amount` (combo) matches the user's requested stake\n2. `conditionId` and `outcomeId` in the payload match the user's chosen selection(s)\n3. `payload.signableClientBetData.clientData.core.toLowerCase()` === `0xf9548be470a4e130c90cea8b179fcd66d2972ac7` (claimContract)\n\nIf any check fails, **do not sign**. Report the mismatch to the user and stop.\n\n### Step 7 — Sign, submit, and poll\n\n```js\n// Determine primaryType\nconst primaryType = payload.types.ClientComboBetData ? 'ClientComboBetData' : 'ClientBetData'\n\n// Sign\nconst bettorSignature = await walletClient.signTypedData({\n  account,\n  domain: payload.domain,\n  types: payload.types,\n  primaryType,\n  message: payload.signableClientBetData,\n})\n\n// Submit to Azuro order API\nconst submitRes = await fetch(payload.apiUrl, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    environment: payload.environment,\n    bettor,\n    betOwner: bettor,\n    clientBetData: payload.apiClientBetData,\n    bettorSignature,\n  })\n})\nconst submitData = await submitRes.json()\n```\n\nIf submission fails (4xx/5xx or `state: \"Rejected\"` / `\"Canceled\"`), report `submitData.errorMessage` or `submitData.error` to the user and stop.\n\n**CRITICAL — poll until txHash. Do not declare success on `state: \"Created\"`.**\n\nThe order is only on-chain when `txHash` is present. `state: \"Created\"` is only an initial state — the bet is NOT confirmed yet.\n\n```js\n// Derive base URL from apiUrl\n// e.g. \"https://api.onchainfeed.org/v1/bet/orders/ordinar\" → \"https://api.onchainfeed.org/v1\"\nconst apiBase = payload.apiUrl.replace(/\\/bet\\/orders\\/(ordinar|combo)$/, '')\nconst orderId = submitData.id\n\nlet txHash = null\nfor (let i = 0; i < 30; i++) {\n  const delayMs = Math.min(2000 + (i * 1000), 10000) // progressive backoff: 2s, 3s, 4s … 10s max\n  await new Promise(r => setTimeout(r, delayMs))\n  const poll = await fetch(`${apiBase}/bet/orders/${orderId}`).then(r => r.json())\n  if (poll.txHash) { txHash = poll.txHash; break }\n  if (poll.state === 'Rejected' || poll.state === 'Canceled') {\n    // Report failure and stop\n    throw new Error(`Order ${poll.state}: ${poll.errorMessage || 'unknown error'}`)\n  }\n}\n\nif (!txHash) throw new Error('Order did not settle after 90s — check manually on Polygonscan')\n```\n\n**On success**, show the user:\n```\n✅ Bet confirmed on-chain!\n   Match:      {teams}\n   Selection:  {market} — {selection} @ {odds}\n   Stake:      {stake} USDT\n   To win:     {potential payout} USDT\n   Tx:         https://polygonscan.com/tx/{txHash}\n```\n\n---\n\n## Flow — resume interrupted flow\n\nIf 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:\n\n1. **Check allowance** — if `allowance(bettor, relayer) >= stake + fee`, approval was already sent. Skip Step 5.\n2. **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.\n3. If no pending bet found and no clear state, start the flow from Step 1 with a fresh game fetch.\n\n---\n\n## Flow — check bet status\n\nQuery the bets subgraph to see pending, resolved, or redeemable bets.\n\n```\nPOST https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3\nContent-Type: application/json\n\n{\n  \"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 } }\",\n  \"variables\": {\n    \"where\": { \"bettor\": \"<address in lowercase>\" },\n    \"first\": 50,\n    \"orderBy\": \"createdBlockTimestamp\",\n    \"orderDirection\": \"desc\"\n  }\n}\n```\n\nTo fetch only redeemable bets: add `\"isRedeemable\": true` to `where`.\n\n**Interpret results:**\n\n| `status` | `result` | `isRedeemable` | Meaning |\n|----------|----------|----------------|---------|\n| `Accepted` | — | false | Pending — not settled yet |\n| `Resolved` | `Won` | true | Won — claim available |\n| `Resolved` | `Lost` | false | Lost |\n| `Canceled` | — | true | Canceled — stake refundable |\n| `Resolved` | `Won` | false | Already claimed |\n\n**On invocation, always check for newly resolved bets** from previous sessions and surface them proactively:\n> \"You have a resolved bet: {match} — {selection} — **{Won/Lost}**. Payout: {payout} USDT.\"\n> If won: \"Would you like to claim your winnings?\"\n\n---\n\n## Flow — claim\n\nOnly for bets where `isRedeemable === true` and `isRedeemed === false`. Collect `betId` values from the subgraph.\n\n### Step 1 — Call Pinwin\n\n```\nPOST https://api.pinwin.xyz/agent/claim\nContent-Type: application/json\n\n{ \"betIds\": [<betId>, ...], \"chain\": \"polygon\" }\n```\n\nDecode: `payload = JSON.parse(atob(response.encoded))`\n\nExplain 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.\"\n\nDisplay the full decoded payload: `to`, `data`, `value`, `chainId`.\n\n### Step 2 — Verify claim contract\n\n**CRITICAL:** `payload.to.toLowerCase()` must equal `0xf9548be470a4e130c90cea8b179fcd66d2972ac7`.\n\nIf it does not match, **do not send the transaction**. Report the mismatch and stop.\n\n### Step 3 — Send and confirm\n\n```js\nconst claimTx = await walletClient.sendTransaction({\n  to: payload.to,\n  data: payload.data,\n  value: 0n,\n  chainId: payload.chainId,\n})\nawait publicClient.waitForTransactionReceipt({ hash: claimTx })\n```\n\nOn success, show:\n```\n✅ Winnings claimed!\n   Bet IDs:  {betIds}\n   Tx:       https://polygonscan.com/tx/{claimTx}\n```\n\n---\n\n## Tools summary\n\n| Step | Tool | Purpose |\n|------|------|---------|\n| Balance check | viem `getBalance`, `readContract` | POL gas, USDT balance and allowance |\n| Fetch games | Data-feed subgraph (GraphQL) | Prematch/live games, conditions, odds |\n| Translate odds | `@azuro-org/dictionaries` | `outcomeId` → human-readable market/selection names |\n| Place bet | `POST /agent/bet` | Get encoded EIP-712 payload |\n| Sign | viem `signTypedData` | EIP-712 bet signature |\n| Submit | Azuro order API (`payload.apiUrl`) | Submit signed bet |\n| Poll | `GET {apiBase}/bet/orders/{orderId}` | Wait for `txHash` confirmation |\n| Watch result | `scripts/watch-bets.js` (auto-launched by `place-bet.js`) | Waits until kickoff + 2h, queries bets subgraph, notifies user via sendPrompt |\n| Bet status | Bets subgraph (GraphQL) | Check status, result, isRedeemable |\n| Claim | `POST /agent/claim` + viem `sendTransaction` | Redeem winnings on-chain |\n\n---\n\n## Error handling\n\n| Error | Cause | Action |\n|-------|-------|--------|\n| `state: \"Rejected\"` / `\"Canceled\"` on order | Invalid bet, market moved, odds expired | Report `errorMessage`, do not retry automatically |\n| Empty order ID from submission | Condition no longer Active (market closed) | Re-check condition state, re-fetch games |\n| `allowance` < required | USDT not approved | Run Step 5 (approve) before signing |\n| `txHash` not received after 90s | Relayer slow or order stuck | Show last known order state, give orderId for manual check on Polygonscan |\n| Subgraph returns 200 with `data.errors` | GraphQL error | Read `data.errors[0].message`, report to user |\n| POL balance = 0 | No gas | Inform user to fund wallet with POL for gas |\n| 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. |\n\n---\n\n## Reference files\n\nLoad only if you need full detail beyond what is in this file:\n\n- [`references/api.md`](references/api.md) — Full Pinwin `/agent/bet` and `/agent/claim` request/response schemas, EIP-712 payload structure, combo bet details\n- [`references/subgraph.md`](references/subgraph.md) — Data-feed GraphQL query, all filter/orderBy options, full response shape\n- [`references/bets-subgraph.md`](references/bets-subgraph.md) — Bets subgraph query, all fields, pagination\n- [`references/dictionaries.md`](references/dictionaries.md) — `@azuro-org/dictionaries` full API\n- [`references/viem.md`](references/viem.md) — viem full setup, all chain calls\n- [`references/polygon.md`](references/polygon.md) — All Polygon addresses and URLs (same as Constants table above)\n\nFile v1.0.4:README.md\n\n# Sports betting skill\n\nAI agent skill for **Polygon** betting via [Pinwin](https://pinwin.xyz), with on-chain execution. This skill covers **Polygon only**. Fetch prematch and live games, markets, and odds from the **Azuro subgraph**; optionally place bets and claim winnings through the Pinwin API. Use it from [OpenClaw](https://docs.openclaw.ai) (or any AgentSkills-compatible assistant)—e.g. to list games only, or to bet and claim.\n\n- **Entry point:** [SKILL.md](SKILL.md). Reference details in **references/** (api, subgraph, dictionaries, polygon, viem).\n- **Invocation:** The skill is **invocation-only** (`disable-model-invocation: true`): the assistant will not use it automatically. You must explicitly ask (e.g. “place a bet with Pinwin”) or use the slash command (e.g. `/sports_betting`). This avoids accidental bets.\n- **Packaging:** ZIP this folder for upload (ClawHub, etc.).\n\nFile v1.0.4:_meta.json\n\n{\n  \"ownerId\": \"kn7cxsyqe45fp21d6pn9wp7g6n81d0gn\",\n  \"slug\": \"sports-betting\",\n  \"version\": \"1.0.4\",\n  \"publishedAt\": 1774106321588\n}\n\nFile v1.0.4:references/api.md\n\n# Pinwin API reference\n\n**Base URL:** `https://api.pinwin.xyz`  \nAll requests: `Content-Type: application/json`. On error (4xx/5xx), body may have `error` or `message`.\n\n## POST /agent/bet\n\n**Request:** `amount` (number, USDT smallest units, 6 decimals), `minOdds` (positive integer, 12 decimals), `chain: \"polygon\"`, `selections` (array of `{ conditionId: string, outcomeId: number }`).\n\n**Response 200:** `{ \"encoded\": \"<base64>\" }`. Decode: `payload = JSON.parse(atob(response.encoded))`.\n\n**Decoded payload:** `signableClientBetData`, `apiClientBetData`, `domain`, `types`, `apiUrl`, `environment`.\n\n**Decoded payload structure (Azuro-aligned).** The payload matches the Azuro V3 order API. Top-level: `signableClientBetData`, `apiClientBetData`, `domain`, `types`, `apiUrl`, `environment`. **clientData** (under `signableClientBetData` or `apiClientBetData`): `attention`, `affiliate`, `core`, `expiresAt`, `chainId`, `relayerFeeAmount`, `isFeeSponsored`, `isBetSponsored`, `isSponsoredBetReturnable`. **Single bet:** `signableClientBetData.bet` has `conditionId`, `outcomeId`, `minOdds`, `amount`, `nonce`. **Combo bet:** `signableClientBetData.bets` (array of `{ conditionId, outcomeId }`), and top-level `amount`, `minOdds`, `nonce` on the signable message.\n\n**Before signing:** (1) **Display to the user all decoded payload fields** relevant to the bet: amount (from `bet.amount` or combo `amount`), selections (conditionId/outcomeId with human-readable names from data-feed/dictionaries), `relayerFeeAmount`, `apiUrl`, `environment`, and all **clientData** fields (`affiliate`, `core`, `expiresAt`, `chainId`, `attention`, `isFeeSponsored`, `isBetSponsored`, `isSponsoredBetReturnable`). (2) Then give a short **human-readable summary**: stake in USDT, selection/market names, relayer fee, and that this is the bet they are authorising. (3) **Verify before signing:** the payload’s amount and selections (conditionId/outcomeId or combo `bets[]`) must match the user’s requested stake and chosen selections. Verify that the payload’s **clientData.core** (lowercase) equals the **claimContract** (ClientCore) for Polygon in [polygon.md](polygon.md); if not, do not sign and report the mismatch. Use only the **relayer** from [polygon.md](polygon.md) for allowance and approve (do not use any address from the payload as relayer). If amount or selections do not match, do not sign and report the mismatch to the user.\n\n**EIP-712 primaryType:** Use `primaryType: 'ClientComboBetData'` if `payload.types.ClientComboBetData` exists, otherwise `primaryType: 'ClientBetData'`. Pass this to viem `signTypedData` along with `domain`, `types`, and `message: payload.signableClientBetData`.\n\n**After sign:** POST to `payload.apiUrl` with body: `environment`, `bettor`, `betOwner`, `clientBetData` (= `payload.apiClientBetData`), `bettorSignature` (hex with `0x`).\n\n**Order submission response (Azuro order API):** JSON with `id` (string, **order id**), `state` (e.g. `Created`, `Rejected`, `Canceled`, `Accepted`), and optional `errorMessage`, `error`. The order id is **always this response `id`** — use it for polling.\n\n**Poll order status (when you have an order id):** Same base URL as `apiUrl` (e.g. strip `/bet/orders/ordinar` or `/bet/orders/combo` to get the base). Request: **GET** `{apiBase}/bet/orders/{orderId}`. Poll until terminal: **success** = response includes `txHash`; **failure** = `state` is `Rejected` or `Canceled` (use `errorMessage` if present). Stop polling when you get `txHash` or a failure state.\n\n## POST /agent/claim\n\n**Request:** `betIds` (number[], on-chain bet ids — e.g. from bets subgraph where `isRedeemable: true`), `chain: \"polygon\"`.\n\n**Response 200:** `{ \"encoded\": \"<base64>\" }`. Decode: `payload = JSON.parse(atob(response.encoded))`.\n\n**Decoded payload:** `to` (contract address), `data` (hex calldata), `value` (\"0\"), `chainId` (number). **Explain to the user in human-readable terms** what the claim tx does: e.g. claiming winnings for bet IDs [X, Y], transaction target (Azuro ClientCore on Polygon), no value sent. You may also display the full decoded payload. Verify `payload.to` (lowercase) equals the **claimContract** (ClientCore) for Polygon in [polygon.md](polygon.md)—this is the redeem contract for won/canceled bets. If it does not match, do not send. Then send tx with viem: `sendTransaction({ to: payload.to, data: payload.data, value: 0n, chainId: payload.chainId })`. Wait for receipt.\n\nFile v1.0.4:references/bets-subgraph.md\n\n# Bets subgraph (history / status)\n\nUse this subgraph to list a bettor’s bets and see whether each is **pending**, **resolved** (won/lost), or **canceled**, and whether it can be **redeemed**. This is a different endpoint from the data-feed (which serves games and conditions).\n\n**URL (Polygon):** `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3`  \n**Header:** `Content-Type: application/json`.  \n**Body:** JSON with `query` (string) and `variables` (object).\n\n## Query\n\n```graphql\nquery BettorBets($where: V3_Bet_filter!, $first: Int, $orderBy: V3_Bet_orderBy, $orderDirection: OrderDirection) {\n  v3Bets(where: $where, first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {\n    betId\n    status\n    result\n    isRedeemable\n    isRedeemed\n    amount\n    payout\n    createdBlockTimestamp\n    resolvedBlockTimestamp\n  }\n}\n```\n\n## Example variables\n\n`bettor` must be the wallet address in **lowercase**.\n\n**All bets (any status):**\n```json\n{\n  \"where\": { \"bettor\": \"0xcdb8eaf44f1e22707df63bfad380b72aa1274e67\" },\n  \"first\": 50,\n  \"orderBy\": \"createdBlockTimestamp\",\n  \"orderDirection\": \"desc\"\n}\n```\n\n**Only redeemable bets** (ready to claim): add `\"isRedeemable\": true` to `where`. Use this to get **betIds** for `POST /agent/claim` without filtering client-side.\n```json\n{\n  \"where\": { \"bettor\": \"0xcdb8eaf44f1e22707df63bfad380b72aa1274e67\", \"isRedeemable\": true },\n  \"first\": 50,\n  \"orderBy\": \"createdBlockTimestamp\",\n  \"orderDirection\": \"desc\"\n}\n```\n\n## Response shape\n\n`data.v3Bets` = array of bets. Each bet:\n\n| Field | Meaning |\n|-------|--------|\n| **betId** | On-chain bet id (number). Use in Pinwin `POST /agent/claim` as `betIds[]`. |\n| **status** | `Accepted` (pending) \\| `Resolved` (settled) \\| `Canceled` |\n| **result** | `Won` \\| `Lost` — only when `status === \"Resolved\"`. |\n| **isRedeemable** | `true` if the user can claim (resolved or canceled, not yet redeemed). |\n| **isRedeemed** | `true` if already claimed. |\n| **amount** | Stake (human-readable). |\n| **payout** | Payout when resolved (human-readable); present when won or canceled. |\n| **createdBlockTimestamp** | When the bet was placed (Unix seconds). |\n| **resolvedBlockTimestamp** | When the bet was settled (Unix seconds); when resolved. |\n\n## How to use\n\n- **Check if a bet is resolved:** `status === \"Resolved\"`. Then **result** tells **Won** or **Lost**.\n- **Check if the user can redeem:** `isRedeemable === true` and `isRedeemed === false`. Collect those bets’ **betId** values and call Pinwin `POST /agent/claim` with `betIds: [<betId>, ...]` and `chain: \"polygon\"`, then sign and send the returned tx. See [api.md](api.md) and the skill Flow (claim).\n\nFile v1.0.4:references/dictionaries.md\n\n# Dictionaries (market/selection names)\n\n**Package:** `@azuro-org/dictionaries` (npm).\n\n- **getMarketName({ outcomeId })** → market label (e.g. \"Total Goals\", \"1st Half - Winner\"). Pass `outcomeId` as returned by the subgraph (string or number).\n- **getSelectionName({ outcomeId, withPoint? })** → selection label (e.g. \"Over\", \"Under\"). Use **withPoint: true** so the line is included when present (e.g. \"Over (2.5)\", \"Under (2.5)\") for markets like Total Goals. On failure, fall back to `outcomeId` or a placeholder.\n\nFile v1.0.4:references/polygon.md\n\n# Polygon addresses\n\nChain ID **137**. Polygon production.\n\n**RPC (defaults):** If `POLYGON_RPC_URL` is not set, use one of these — try in order if the first fails:\n- `https://poly.api.pocket.network`\n- `https://polygon-bor-rpc.publicnode.com` ([PublicNode](https://polygon-bor-rpc.publicnode.com))\n\n| Field | Value |\n|-------|--------|\n| **native gas token** | POL (for gas: approve, claim txs) |\n| **data-feed URL** | `https://thegraph-1.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-data-feed-polygon` |\n| **bets subgraph URL** | `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3` |\n| **relayer** | `0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d` (for USDT approval; verify bet payload does not override) |\n| **betToken** | `0xc2132d05d31c914a87c6611c10748aeb04b58e8f` (USDT, **6 decimals**) |\n| **core** (ClientCore) | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` (for bet payload clientData.core; same as claimContract) |\n| **claimContract** (ClientCore, redeem won/canceled bets) | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` |\n| **environment** | `PolygonUSDT` |\n\nFile v1.0.4:references/subgraph.md\n\n# Data-feed (subgraph) reference\n\n**URL (Polygon):** `https://thegraph-1.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-data-feed-polygon`  \n**Header:** `Content-Type: application/json`.  \n**Body:** JSON with `query` (string) and `variables` (object).  \n**Game state:** `state` = `\"Prematch\"` | `\"Live\"` | `\"Stopped\"` | `\"Finished\"`. Only use conditions with `state: \"Active\"`.\n\n## Canonical query\n\n```graphql\nquery Games($first: Int!, $where: Game_filter!, $orderBy: Game_orderBy!, $orderDirection: OrderDirection!) {\n  games(first: $first, where: $where, orderBy: $orderBy, orderDirection: $orderDirection) {\n    gameId\n    title\n    state\n    startsAt\n    league { name }\n    country { name }\n    sport { name }\n    participants { name }\n    conditions {\n      conditionId\n      state\n      outcomes { outcomeId currentOdds }\n    }\n  }\n}\n```\n\n## Example variables\n\n**Prematch (turnover desc):**  \n`{ \"first\": 20, \"where\": { \"state\": \"Prematch\" }, \"orderBy\": \"turnover\", \"orderDirection\": \"desc\" }`\n\n**Live (turnover desc):**  \n`{ \"first\": 20, \"where\": { \"state\": \"Live\" }, \"orderBy\": \"turnover\", \"orderDirection\": \"desc\" }`\n\n## Filtering and ordering\n\n**Filtering (optional):** `sport_: { slug: \"football\" }`, `country_: { slug: \"germany\" }`, `league_: { slug: \"bundesliga\" }`, `startsAt_gt` (Unix seconds).\n\n**Ordering:** `orderBy`: `turnover` | `startsAt`; `orderDirection`: `asc` | `desc`.\n\n## Response shape\n\n`data.games` = array of games. Each game: `gameId`, `title`, `state`, `startsAt`, `league.name`, `country.name`, `sport.name`, `participants[]` (each `name`), `conditions[]` with `conditionId`, `state`, `outcomes[]` with `outcomeId`, `currentOdds`.\n\nFile v1.0.4:references/viem.md\n\n# Viem (minimal)\n\n**Install:** `npm install viem`.  \n**Setup:** RPC = `process.env.POLYGON_RPC_URL` or first default from [references/polygon.md](references/polygon.md). Create `publicClient` and `walletClient` from private key + `http(rpcUrl)` + chain `polygon` (from `viem/chains`). Use `privateKeyToAccount` from `viem/accounts`.\n\n- **Balance (POL):** `publicClient.getBalance({ address })` — native gas token on Polygon; use to ensure the wallet can pay for approve and claim txs.\n- **Balance (USDT):** `publicClient.readContract({ address: betToken, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })`. Bet token and 6 decimals from [references/polygon.md](references/polygon.md). Require balance ≥ stake + relayerFeeAmount (from payload) before placing a bet.\n- **Allowance:** `publicClient.readContract({ address: betToken, abi: erc20Abi, functionName: 'allowance', args: [bettor, relayer] })`. If result < bet amount + relayerFeeAmount + buffer → approve. Buffer = 0.2 USDT = 200000 (6 decimals).\n- **Approve:** Encode `approve(relayer, betAmount + relayerFeeAmount + 200000)` — i.e. stake + relayer fee + 0.2 USDT buffer. The relayer cannot spend more than needed for this bet plus a small buffer. `walletClient.sendTransaction({ to: betToken, data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [relayer, stake + relayerFeeAmount + 200000n] }) })`. Wait for receipt.\n- **Bet sign:** `walletClient.signTypedData({ account, domain: payload.domain, types: payload.types, primaryType, message: payload.signableClientBetData })`. Use `primaryType: 'ClientComboBetData'` if `payload.types.ClientComboBetData` exists, else `'ClientBetData'`. No tx; then POST signature to `payload.apiUrl`.\n- **Claim:** `walletClient.sendTransaction({ to: payload.to, data: payload.data, value: 0n, chainId: payload.chainId })`. Wait for receipt.\n\nERC-20 ABI (minimal): `allowance(address,address) view returns (uint256)`, `approve(address,uint256) returns (bool)`, `balanceOf(address) view returns (uint256)`. Use viem `parseAbi` or equivalent.\n\nFile v1.0.4:scripts/package-lock.json\n\n{\n  \"name\": \"scripts\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"dependencies\": {\n        \"@azuro-org/dictionaries\": \"^3.0.28\"\n      }\n    },\n    \"node_modules/@azuro-org/dictionaries\": {\n      \"version\": \"3.0.28\",\n      \"resolved\": \"https://registry.npmjs.org/@azuro-org/dictionaries/-/dictionaries-3.0.28.tgz\",\n      \"integrity\": \"sha512-OwuLSYWaHgHOo2RF7aUHA3HJpT7b6FJqVAKTacEFcsNQgDw7Dx5XQfcVYalHMB/bD3MsjsBLsYfwfvS4cUfRNQ==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"minimist\": \"^1.2.7\"\n      },\n      \"bin\": {\n        \"get-outcomes\": \"bin/get-outcomes/index.js\"\n      }\n    },\n    \"node_modules/minimist\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz\",\n      \"integrity\": \"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    }\n  }\n}\n\nFile v1.0.4:scripts/package.json\n\n{\n  \"dependencies\": {\n    \"@azuro-org/dictionaries\": \"^3.0.28\"\n  }\n}\n\nArchive v1.0.3: 9 files, 12287 bytes\n\nFiles: README.md (899b), references/api.md (4464b), references/bets-subgraph.md (2694b), references/dictionaries.md (523b), references/polygon.md (1111b), references/subgraph.md (1670b), references/viem.md (2007b), SKILL.md (11895b), _meta.json (133b)\n\nFile v1.0.3:SKILL.md\n\n---\nname: sports-betting\ndescription: \"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 data-feed, pick a selection, then sign and submit. Use when the user wants to bet on sports with Pinwin, browse games, place a bet, or check or redeem winnings.\"\ncompatibility: \"Requires Node, viem and @azuro-org/dictionaries (for human-readable market/selection names). Required env: BETTOR_PRIVATE_KEY (wallet private key; high-sensitivity). Optional env: POLYGON_RPC_URL (Polygon RPC); if unset, use default RPC(s) in references/polygon.md.\"\nhomepage: \"https://github.com/skinnynoizze/pinwin-agent\"\ndisable-model-invocation: true\nmetadata: {\"openclaw\":{\"requires\":{\"bins\":[\"node\"],\"env\":[\"BETTOR_PRIVATE_KEY\"]},\"primaryEnv\":\"BETTOR_PRIVATE_KEY\"}}\n---\n\n# Sports betting (Pinwin)\n\nPlace and claim **decentralized** sports bets on **Polygon** via [Pinwin](https://pinwin.xyz) and Azuro, with on-chain execution. The agent fetches **prematch and live** games from the data-feed, you pick a selection, then it calls Pinwin, signs (and optionally approves USDT), and submits.\n\n**Invocation:** This skill is **invocation-only**: the assistant will not use it unless you explicitly ask (e.g. “place a bet with Pinwin”) or use the slash command. That avoids accidental bets.\n\n---\n\n## How to use (OpenClaw)\n\n- **Invoke:** Use the slash command **`/sports_betting`** (or **`/skill sports-betting`**) and optionally add your request, e.g. `/sports_betting place 5 USDT on the first Premier League game` or `/sports_betting show my bets`.\n- **Versatility:** The assistant should **ask you** for preferences when not specified: how many games to fetch (`first`), order (`turnover` vs `startsAt`), sport/country/league filters, and **which selection you want**. It should **not** suggest or pick a bet unless you explicitly ask for a suggestion (meaningful suggestions would require external data, e.g. news or stats, and could be a separate skill).\n\n---\n\n## When to use\n\n- User wants to **place a bet** on a game (prematch or live): fetch games → choose a selection → call Pinwin `/agent/bet` → approve token if needed → sign EIP-712 → POST signature to returned `apiUrl`.\n- User wants to **check bet status** (pending / resolved / won or lost) or **redeem winnings**: query the bets subgraph for the bettor’s bets (see [Check bet status](#check-bet-status-before-redeem)); when a bet has `isRedeemable && !isRedeemed`, call Pinwin `/agent/claim` with those `betIds` → sign and send the returned transaction with viem.\n\n---\n\n## Prerequisites\n\n### Credentials (required env)\n\n- **BETTOR_PRIVATE_KEY** — Wallet private key (hex) for signing bets and claim transactions. High-sensitivity; do not log or expose. Required for placing and claiming. Use a dedicated betting wallet with minimal funds; do not use your primary wallet.\n\n### Optional\n\n- **POLYGON_RPC_URL** — Polygon RPC endpoint. If unset, the agent uses the default RPC(s) in [references/polygon.md](references/polygon.md) (e.g. Pocket Network, PublicNode).\n\n### Other\n\n- **@azuro-org/dictionaries** — required. The subgraph returns only `outcomeId` and odds; this package is the only way to map them to human-readable market and selection names (e.g. \"Total Goals\", \"Over (2.5)\"). See [references/dictionaries.md](references/dictionaries.md).\n- **Addresses:** relayer, bet token, native gas token (POL), data-feed URL, and bets subgraph URL are in [references/polygon.md](references/polygon.md).\n- **Balances:** The agent can use viem to check **POL** (gas) and **USDT** (stake + relayer fee) before placing a bet; if insufficient, inform the user and do not proceed. See [references/viem.md](references/viem.md).\n\n---\n\n## Flow (place a bet)\n\n0. **Optional — check balances:** Ensure USDT ≥ stake and POL for gas; exact USDT = stake + payload’s relayerFeeAmount. See [references/viem.md](references/viem.md).\n1. **Fetch games** – POST data-feed per [references/subgraph.md](references/subgraph.md) (state Prematch or Live). Get games with conditions (conditionId, outcomes: outcomeId, currentOdds). Names via [references/dictionaries.md](references/dictionaries.md) (`getSelectionName({ outcomeId, withPoint: true })`). Respect first, orderBy, filters or defaults.\n2. **Choose selection** – Pick one (or more for combo) `conditionId` + `outcomeId` from an Active condition. **minOdds:** single = `Math.round(parseFloat(currentOdds) * 1e12)`; combo = product of each selection’s odds in 12-decimal space (e.g. (odds1×1e12 × odds2×1e12) / 1e12).\n3. **Call Pinwin** – `POST https://api.pinwin.xyz/agent/bet` with JSON body (see [references/api.md](references/api.md)). Response: `{ \"encoded\": \"<base64>\" }`. Decode: `payload = JSON.parse(atob(response.encoded))`.\n4. **Explain to user (before signing)** – Display payload: amount and selections from `signableClientBetData.bets`, relayerFeeAmount, apiUrl, environment, clientData (human-readable names per [references/dictionaries.md](references/dictionaries.md)); then a short summary. See [references/api.md](references/api.md).\n5. **Approval (if needed)** – Check bet token’s `allowance(bettor, relayer)` on Polygon. If &lt; bet amount + relayer fee + 0.2 USDT, sign `approve(relayer, bet amount + relayer fee + 0.2 USDT)` on the bet token. Approval is bounded to this bet plus a small buffer for security; the agent may need to approve again for the next bet. Addresses: [references/polygon.md](references/polygon.md). Steps: [references/viem.md](references/viem.md).\n6. **Verify payload vs user intent** – Amount and selections (from `bets`), **clientData.core** = **claimContract**, relayer from [references/polygon.md](references/polygon.md) only. See [references/api.md](references/api.md). If anything does not match, do not sign and inform the user.\n7. **Sign and submit** – Use viem `signTypedData` with `payload.domain`, `payload.types`, `primaryType` (see [references/api.md](references/api.md)), and `message: payload.signableClientBetData`. Then POST to `payload.apiUrl` with `environment`, `bettor`, `betOwner`, `clientBetData` (= `payload.apiClientBetData`), `bettorSignature`. The **order id is in the POST response**: use `response.id`. If you get an order id, poll until the order settles (see [references/api.md](references/api.md)): GET `{apiBase}/bet/orders/{orderId}`. Success = response has `txHash`; failure = `state` is `Rejected` or `Canceled` (use `errorMessage`).\n\n---\n\n## Check bet status (before redeem)\n\nTo know when a bet is **resolved** and whether it **won** or **lost**, and whether the user can **redeem**, query the **bets subgraph** (different from the data-feed). See [references/bets-subgraph.md](references/bets-subgraph.md).\n\n1. **Query bets** – POST a GraphQL query to the bets subgraph URL (Polygon: in [references/polygon.md](references/polygon.md)). Query `v3Bets` with `where: { bettor: \"<bettor address>\" }` (address in lowercase). To fetch only bets that can be claimed, add **`isRedeemable: true`** to the where clause (see [references/bets-subgraph.md](references/bets-subgraph.md)). Request at least: `betId`, `status`, `result`, `isRedeemable`, `isRedeemed`, `amount`, `payout`.\n2. **Interpret** – **status** = `Accepted` (pending) | `Resolved` (settled) | `Canceled`. When **status === Resolved**, **result** = `Won` or `Lost`. When **isRedeemable === true** and **isRedeemed === false**, the user can claim; collect those bets’ **betId** values for the claim flow.\n\n---\n\n## Flow (claim)\n\nOnly for bets that are resolved (or canceled) and have **isRedeemable** true and **isRedeemed** false; get **betIds** from the bets subgraph (see [Check bet status](#check-bet-status-before-redeem)).\n\n1. **Call Pinwin** – `POST https://api.pinwin.xyz/agent/claim` with `betIds` (array of on-chain bet ids) and `chain: \"polygon\"`. Decode the response `encoded` payload. **Explain to the user in human-readable terms** what they are sending: e.g. claiming winnings for bet IDs X, Y; the transaction will go to the Azuro ClientCore contract on Polygon; no value (ETH/POL) is sent. Display the **full** decoded claim payload (to, chainId, value, and any other keys returned) for transparency.\n2. **Verify claim contract** – Ensure `payload.to` (lowercase) equals the documented **claimContract** (ClientCore) for Polygon in [references/polygon.md](references/polygon.md). This is the redeem contract for won/canceled bets, not the Cashout (early-exit) contract. If it does not match, do not send the tx and report the mismatch.\n3. **Send tx** – Use viem `sendTransaction` with `{ to: payload.to, data: payload.data, value: 0n, chainId: payload.chainId }`. Wait for receipt. Details: [references/viem.md](references/viem.md).\n\n---\n\n## Example (place a single bet)\n\nAfter fetching games and choosing one outcome:\n\n```json\nPOST https://api.pinwin.xyz/agent/bet\n{ \"amount\": 1000000, \"minOdds\": 1500000000000, \"chain\": \"polygon\", \"selections\": [{ \"conditionId\": \"<from data-feed>\", \"outcomeId\": 21 }] }\n```\n\nDecode `response.encoded`, sign `payload.signableClientBetData` with viem `signTypedData`, then POST to `payload.apiUrl` with `clientBetData` and `bettorSignature`.\n\n## Example (claim)\n\nGet **betIds** from the bets subgraph (e.g. query with `isRedeemable: true`). Then:\n\n```json\nPOST https://api.pinwin.xyz/agent/claim\n{ \"betIds\": [215843], \"chain\": \"polygon\" }\n```\n\nDecode `response.encoded` → `payload`. Display payload to the user; verify `payload.to` equals the claimContract (ClientCore) in [references/polygon.md](references/polygon.md). Then send tx with viem: `sendTransaction({ to: payload.to, data: payload.data, value: 0n, chainId: payload.chainId })`. Wait for receipt.\n\n---\n\n## Tools\n\n| Step | Tool | Purpose |\n|------|------|---------|\n| Games | Data-feed subgraph (GraphQL) | Get games, conditions, outcomes, odds |\n| Bet status | Bets subgraph (GraphQL) | Get bettor’s bets: status (Accepted/Resolved/Canceled), result (Won/Lost), isRedeemable, betId |\n| Names | @azuro-org/dictionaries (required) | Map outcomeId → human-readable market/selection names; only way to get labels from subgraph data. |\n| Bet/claim | Pinwin API | Get encoded payload and apiUrl |\n| Chain | viem + RPC | getBalance (POL), readContract (allowance, balanceOf USDT), sendTransaction (approve, claim), signTypedData (bet) |\n\n**Required packages:** `npm install viem @azuro-org/dictionaries`. Setup and chain calls: [references/viem.md](references/viem.md). Dictionaries usage: [references/dictionaries.md](references/dictionaries.md).\n\n---\n\n## Errors\n\n- **Pinwin 4xx/5xx:** read `error` or `message` in the response body.\n- **Subgraph:** check both HTTP status and `data.errors` in the JSON body (GraphQL can return 200 with `data.errors`).\n- **Chain:** tx reverted, insufficient funds — report tx hash.\n\n---\n\n## Reference files\n\nLoad these when you need full request/response shapes, queries, or addresses:\n\n- [references/api.md](references/api.md) – Pinwin POST /agent/bet and /agent/claim (request, response, decoded payload).\n- [references/subgraph.md](references/subgraph.md) – Data-feed URL, canonical GraphQL query, example variables, filtering, response shape.\n- [references/bets-subgraph.md](references/bets-subgraph.md) – Bets subgraph URL, query for bettor’s bets, status/result/isRedeemable, betId for claim.\n- [references/dictionaries.md](references/dictionaries.md) – @azuro-org/dictionaries (getMarketName, getSelectionName; use withPoint: true for selection lines).\n- [references/polygon.md](references/polygon.md) – Polygon data-feed URL, bets subgraph URL, native gas token (POL), relayer, betToken (USDT), environment.\n- [references/viem.md](references/viem.md) – viem install, setup, getBalance (POL), balanceOf (USDT), allowance, approve, signTypedData, claim tx.\n\nFile v1.0.3:README.md\n\n# Sports betting skill\n\nAI agent skill for **Polygon** betting via [Pinwin](https://pinwin.xyz), with on-chain execution. This skill covers **Polygon only**. Fetch prematch and live games, markets, and odds from the **Azuro subgraph**; optionally place bets and claim winnings through the Pinwin API. Use it from [OpenClaw](https://docs.openclaw.ai) (or any AgentSkills-compatible assistant)—e.g. to list games only, or to bet and claim.\n\n- **Entry point:** [SKILL.md](SKILL.md). Reference details in **references/** (api, subgraph, dictionaries, polygon, viem).\n- **Invocation:** The skill is **invocation-only** (`disable-model-invocation: true`): the assistant will not use it automatically. You must explicitly ask (e.g. “place a bet with Pinwin”) or use the slash command (e.g. `/sports_betting`). This avoids accidental bets.\n- **Packaging:** ZIP this folder for upload (ClawHub, etc.).\n\nFile v1.0.3:_meta.json\n\n{\n  \"ownerId\": \"kn7cxsyqe45fp21d6pn9wp7g6n81d0gn\",\n  \"slug\": \"sports-betting\",\n  \"version\": \"1.0.3\",\n  \"publishedAt\": 1771680139464\n}\n\nFile v1.0.3:references/api.md\n\n# Pinwin API reference\n\n**Base URL:** `https://api.pinwin.xyz`  \nAll requests: `Content-Type: application/json`. On error (4xx/5xx), body may have `error` or `message`.\n\n## POST /agent/bet\n\n**Request:** `amount` (number, USDT smallest units, 6 decimals), `minOdds` (positive integer, 12 decimals), `chain: \"polygon\"`, `selections` (array of `{ conditionId: string, outcomeId: number }`). Single: minOdds = one outcome’s odds × 1e12. Combo: minOdds = product of each selection’s odds in 12-decimal space (e.g. (o1×1e12 × o2×1e12) / 1e12).\n\n**Response 200:** `{ \"encoded\": \"<base64>\" }`. Decode: `payload = JSON.parse(atob(response.encoded))`.\n\n**Decoded payload:** `signableClientBetData`, `apiClientBetData`, `domain`, `types`, `apiUrl`, `environment`.\n\n**Decoded payload structure (Azuro-aligned).** Top-level: `signableClientBetData`, `apiClientBetData`, `domain`, `types`, `apiUrl`, `environment`. **clientData** (under either): `attention`, `affiliate`, `core`, `expiresAt`, `chainId`, `relayerFeeAmount`, `isFeeSponsored`, `isBetSponsored`, `isSponsoredBetReturnable`. **Bet data:** Always use **`signableClientBetData.bets`** (array): one element = single, multiple = combo. Each element has `conditionId`, `outcomeId`, `amount`, `minOdds`, `nonce`. Azuro’s order API has ordinar (single) and combo (bets[]); Pinwin sets `apiUrl` and `apiClientBetData` accordingly—submit as returned.\n\n**Before signing:** (1) **Display to the user** amount and selections from `signableClientBetData.bets` (with human-readable names from data-feed/dictionaries), `relayerFeeAmount`, `apiUrl`, `environment`, and all **clientData** fields. (2) Give a short **human-readable summary**: stake, selection/market names, relayer fee. (3) **Verify:** amount and selections (from `bets`) match the user’s request; **clientData.core** (lowercase) equals **claimContract** in [polygon.md](polygon.md). Use only the **relayer** from [polygon.md](polygon.md) for allowance/approve. If anything does not match, do not sign and report to the user.\n\n**EIP-712 primaryType:** Use `primaryType: 'ClientComboBetData'` if `payload.types.ClientComboBetData` exists, otherwise `primaryType: 'ClientBetData'`. Pass this to viem `signTypedData` along with `domain`, `types`, and `message: payload.signableClientBetData`.\n\n**After sign:** POST to `payload.apiUrl` with body: `environment`, `bettor`, `betOwner`, `clientBetData` (= `payload.apiClientBetData`), `bettorSignature` (hex with `0x`). The `apiUrl` in the payload may point to the Azuro/OnchainFeed order API (e.g. `https://api.onchainfeed.org/...`); use the URL returned in the payload.\n\n**Order submission response (Azuro order API):** JSON with `id` (string, **order id**), `state` (e.g. `Created`, `Rejected`, `Canceled`, `Accepted`), and optional `errorMessage`, `error`. The order id is **always this response `id`** — use it for polling. On 4xx/5xx the response body may still be JSON with `state`, `error`, `errorMessage` (e.g. 409 and \"Different games in one order\" for invalid combo); when submission fails, show the user the status and body (or at least `errorMessage` / `error`).\n\n**Poll order status (when you have an order id):** Same base URL as `apiUrl` (e.g. strip `/bet/orders/ordinar` or `/bet/orders/combo` to get the base). Request: **GET** `{apiBase}/bet/orders/{orderId}`. Poll until terminal: **success** = response includes `txHash`; **failure** = `state` is `Rejected` or `Canceled` (use `errorMessage` if present). Stop polling when you get `txHash` or a failure state.\n\n## POST /agent/claim\n\n**Request:** `betIds` (number[], on-chain bet ids — e.g. from bets subgraph where `isRedeemable: true`), `chain: \"polygon\"`.\n\n**Response 200:** `{ \"encoded\": \"<base64>\" }`. Decode: `payload = JSON.parse(atob(response.encoded))`.\n\n**Decoded payload:** `to` (contract address), `data` (hex calldata), `value` (\"0\"), `chainId` (number). **Explain to the user in human-readable terms** what the claim tx does: e.g. claiming winnings for bet IDs [X, Y], transaction target (Azuro ClientCore on Polygon), no value sent. You may also display the full decoded payload. Verify `payload.to` (lowercase) equals the **claimContract** (ClientCore) for Polygon in [polygon.md](polygon.md)—this is the redeem contract for won/canceled bets. If it does not match, do not send. Then send tx with viem: `sendTransaction({ to: payload.to, data: payload.data, value: 0n, chainId: payload.chainId })`. Wait for receipt.\n\nFile v1.0.3:references/bets-subgraph.md\n\n# Bets subgraph (history / status)\n\nUse this subgraph to list a bettor’s bets and see whether each is **pending**, **resolved** (won/lost), or **canceled**, and whether it can be **redeemed**. This is a different endpoint from the data-feed (which serves games and conditions).\n\n**URL (Polygon):** `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3`  \n**Header:** `Content-Type: application/json`.  \n**Body:** JSON with `query` (string) and `variables` (object).\n\n## Query\n\n```graphql\nquery BettorBets($where: V3_Bet_filter!, $first: Int, $orderBy: V3_Bet_orderBy, $orderDirection: OrderDirection) {\n  v3Bets(where: $where, first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {\n    betId\n    status\n    result\n    isRedeemable\n    isRedeemed\n    amount\n    payout\n    createdBlockTimestamp\n    resolvedBlockTimestamp\n  }\n}\n```\n\n## Example variables\n\n`bettor` must be the wallet address in **lowercase**.\n\n**All bets (any status):**\n```json\n{\n  \"where\": { \"bettor\": \"0xcdb8eaf44f1e22707df63bfad380b72aa1274e67\" },\n  \"first\": 50,\n  \"orderBy\": \"createdBlockTimestamp\",\n  \"orderDirection\": \"desc\"\n}\n```\n\n**Only redeemable bets** (ready to claim): add `\"isRedeemable\": true` to `where`. Use this to get **betIds** for `POST /agent/claim` without filtering client-side.\n```json\n{\n  \"where\": { \"bettor\": \"0xcdb8eaf44f1e22707df63bfad380b72aa1274e67\", \"isRedeemable\": true },\n  \"first\": 50,\n  \"orderBy\": \"createdBlockTimestamp\",\n  \"orderDirection\": \"desc\"\n}\n```\n\n## Response shape\n\n`data.v3Bets` = array of bets. Each bet:\n\n| Field | Meaning |\n|-------|--------|\n| **betId** | On-chain bet id (number). Use in Pinwin `POST /agent/claim` as `betIds[]`. |\n| **status** | `Accepted` (pending) \\| `Resolved` (settled) \\| `Canceled` |\n| **result** | `Won` \\| `Lost` — only when `status === \"Resolved\"`. |\n| **isRedeemable** | `true` if the user can claim (resolved or canceled, not yet redeemed). |\n| **isRedeemed** | `true` if already claimed. |\n| **amount** | Stake (human-readable). |\n| **payout** | Payout when resolved (human-readable); present when won or canceled. |\n| **createdBlockTimestamp** | When the bet was placed (Unix seconds). |\n| **resolvedBlockTimestamp** | When the bet was settled (Unix seconds); when resolved. |\n\n## How to use\n\n- **Check if a bet is resolved:** `status === \"Resolved\"`. Then **result** tells **Won** or **Lost**.\n- **Check if the user can redeem:** `isRedeemable === true` and `isRedeemed === false`. Collect those bets’ **betId** values and call Pinwin `POST /agent/claim` with `betIds: [<betId>, ...]` and `chain: \"polygon\"`, then sign and send the returned tx. See [api.md](api.md) and the skill Flow (claim).\n\nFile v1.0.3:references/dictionaries.md\n\n# Dictionaries (market/selection names)\n\n**Package:** `@azuro-org/dictionaries` (npm).\n\n- **getMarketName({ outcomeId })** → market label (e.g. \"Total Goals\", \"1st Half - Winner\"). Pass `outcomeId` as returned by the subgraph (string or number).\n- **getSelectionName({ outcomeId, withPoint? })** → selection label (e.g. \"Over\", \"Under\"). Use **withPoint: true** so the line is included when present (e.g. \"Over (2.5)\", \"Under (2.5)\") for markets like Total Goals. On failure, fall back to `outcomeId` or a placeholder.\n\nFile v1.0.3:references/polygon.md\n\n# Polygon addresses\n\nChain ID **137**. Polygon production.\n\n**RPC (defaults):** If `POLYGON_RPC_URL` is not set, use one of these — try in order if the first fails:\n- `https://poly.api.pocket.network`\n- `https://polygon-bor-rpc.publicnode.com` ([PublicNode](https://polygon-bor-rpc.publicnode.com))\n\n| Field | Value |\n|-------|--------|\n| **native gas token** | POL (for gas: approve, claim txs) |\n| **data-feed URL** | `https://thegraph-1.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-data-feed-polygon` |\n| **bets subgraph URL** | `https://thegraph.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-api-polygon-v3` |\n| **relayer** | `0x8dA05c0021e6b35865FDC959c54dCeF3A4AbBa9d` (for USDT approval; verify bet payload does not override) |\n| **betToken** | `0xc2132d05d31c914a87c6611c10748aeb04b58e8f` (USDT, **6 decimals**) |\n| **core** (ClientCore) | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` (for bet payload clientData.core; same as claimContract) |\n| **claimContract** (ClientCore, redeem won/canceled bets) | `0xF9548Be470A4e130c90ceA8b179FCD66D2972AC7` |\n| **environment** | `PolygonUSDT` |\n\nFile v1.0.3:references/subgraph.md\n\n# Data-feed (subgraph) reference\n\n**URL (Polygon):** `https://thegraph-1.onchainfeed.org/subgraphs/name/azuro-protocol/azuro-data-feed-polygon`  \n**Header:** `Content-Type: application/json`.  \n**Body:** JSON with `query` (string) and `variables` (object).  \n**Game state:** `state` = `\"Prematch\"` | `\"Live\"` | `\"Stopped\"` | `\"Finished\"`. Only use conditions with `state: \"Active\"`.\n\n## Canonical query\n\n```graphql\nquery Games($first: Int!, $where: Game_filter!, $orderBy: Game_orderBy!, $orderDirection: OrderDirection!) {\n  games(first: $first, where: $where, orderBy: $orderBy, orderDirection: $orderDirection) {\n    gameId\n    title\n    state\n    startsAt\n    league { name }\n    country { name }\n    sport { name }\n    participants { name }\n    conditions {\n      conditionId\n      state\n      outcomes { outcomeId currentOdds }\n    }\n  }\n}\n```\n\n## Example variables\n\n**Prematch (turnover desc):**  \n`{ \"first\": 20, \"where\": { \"state\": \"Prematch\" }, \"orderBy\": \"turnover\", \"orderDirection\": \"desc\" }`\n\n**Live (turnover desc):**  \n`{ \"first\": 20, \"where\": { \"state\": \"Live\" }, \"orderBy\": \"turnover\", \"orderDirection\": \"desc\" }`\n\n## Filtering and ordering\n\n**Filtering (optional):** `sport_: { slug: \"football\" }`, `country_: { slug: \"germany\" }`, `league_: { slug: \"bundesliga\" }`, `startsAt_gt` (Unix seconds).\n\n**Ordering:** `orderBy`: `turnover` | `startsAt`; `orderDirection`: `asc` | `desc`.\n\n## Response shape\n\n`data.games` = array of games. Each game: `gameId`, `title`, `state`, `startsAt`, `league.name`, `country.name`, `sport.name`, `participants[]` (each `name`), `conditions[]` with `conditionId`, `state`, `outcomes[]` with `outcomeId`, `currentOdds`.\n\nFile v1.0.3:references/viem.md\n\n# Viem (minimal)\n\n**Install:** `npm install viem`.  \n**Setup:** RPC = `process.env.POLYGON_RPC_URL` or first default from [references/polygon.md](references/polygon.md). Create `publicClient` and `walletClient` from private key + `http(rpcUrl)` + chain `polygon` (from `viem/chains`). Use `privateKeyToAccount` from `viem/accounts`.\n\n- **Balance (POL):** `publicClient.getBalance({ address })` — native gas token on Polygon; use to ensure the wallet can pay for approve and claim txs.\n- **Balance (USDT):** `publicClient.readContract({ address: betToken, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })`. Bet token and 6 decimals from [references/polygon.md](references/polygon.md). Require balance ≥ stake + relayerFeeAmount (from payload) before placing a bet.\n- **Allowance:** `publicClient.readContract({ address: betToken, abi: erc20Abi, functionName: 'allowance', args: [bettor, relayer] })`. If result < bet amount + relayerFeeAmount + buffer → approve. Buffer = 0.2 USDT = 200000 (6 decimals).\n- **Approve:** Encode `approve(relayer, betAmount + relayerFeeAmount + 200000)` — i.e. stake + relayer fee + 0.2 USDT buffer. The relayer cannot spend more than needed for this bet plus a small buffer. `walletClient.sendTransaction({ to: betToken, data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [relayer, stake + relayerFeeAmount + 200000n] }) })`. Wait for receipt.\n- **Bet sign:** `walletClient.signTypedData({ account, domain: payload.domain, types: payload.types, primaryType, message: payload.signableClientBetData })`. primaryType from payload per [api.md](api.md). No tx; then POST signature to `payload.apiUrl`.\n- **Claim:** `walletClient.sendTransaction({ to: payload.to, data: payload.data, value: 0n, chainId: payload.chainId })`. Wait for receipt.\n\nERC-20 ABI (minimal): `allowance(address,address) view returns (uint256)`, `approve(address,uint256) returns (bool)`, `balanceOf(address) view returns (uint256)`. Use viem `parseAbi` or equivalent.\n\nArchive v1.0.2: 9 files, 12695 bytes\n\nFiles: README.md (899b), references/api.md (4458b), references/bets-subgraph.md (2694b), references/dictionaries.md (523b), references/polygon.md (1111b), references/subgraph.md (1670b), references/viem.md (2070b), SKILL.md (13645b), _meta.json (133b)\n\nFile v1.0.2:SKILL.md\n\n---\nname: sports-betting\ndescription: 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 data-feed, pick a selection, then sign and submit. Use when the user wants to bet on sports with Pinwin, browse games, place a bet, or check or redeem winnings.\ncompatibility: Requires Node, viem and @azuro-org/dictionaries (for human-readable market/selection names). Required env: BETTOR_PRIVATE_KEY (wallet private key; high-sensitivity). Optional env: POLYGON_RPC_URL (Polygon RPC); if unset, use default RPC(s) in references/polygon.md.\nhomepage: https://github.com/skinnynoizze/pinwin-agent\ndisable-model-invocation: true\nmetadata: {\"openclaw\":{\"requires\":{\"bins\":[\"node\"],\"env\":[\"BETTOR_PRIVATE_KEY\"]},\"primaryEnv\":\"BETTOR_PRIVATE_KEY\"}}\n---\n\n# Sports betting (Pinwin)\n\nPlace and claim **decentralized** sports bets on **Polygon** via [Pinwin](https://pinwin.xyz) and Azuro, with on-chain execution. The agent fetches **prematch and live** games from the data-feed, you pick a selection, then it calls Pinwin, signs (and optionally approves USDT), and submits.\n\n**Invocation:** This skill is **invocation-only**: the assistant will not use it unless you explicitly ask (e.g. “place a bet with Pinwin”) or use the slash command. That avoids accidental bets.\n\n---\n\n## How to use (OpenClaw)\n\n- **Invoke:** Use the slash command **`/sports_betting`** (or **`/skill sports-betting`**) and optionally add your request, e.g. `/sports_betting place 5 USDT on the first Premier League game` or `/sports_betting show my bets`.\n- **Versatility:** The assistant should **ask you** for preferences when not specified: how many games to fetch (`first`), order (`turnover` vs `startsAt`), sport/country/league filters, and **which selection you want**. It should **not** suggest or pick a bet unless you explicitly ask for a suggestion (meaningful suggestions would require external data, e.g. news or stats, and could be a separate skill).\n\n---\n\n## When to use\n\n- User wants to **place a bet** on a game (prematch or live): fetch games → choose a selection → call Pinwin `/agent/bet` → approve token if needed → sign EIP-712 → POST signature to returned `apiUrl`.\n- User wants to **check bet status** (pending / resolved / won or lost) or **redeem winnings**: query the bets subgraph for the bettor’s bets (see [Check bet status](#check-bet-status-before-redeem)); when a bet has `isRedeemable && !isRedeemed`, call Pinwin `/agent/claim` with those `betIds` → sign and send the returned transaction with viem.\n\n---\n\n## Prerequisites\n\n### Credentials (required env)\n\n- **BETTOR_PRIVATE_KEY** — Wallet private key (hex) for signing bets and claim transactions. High-sensitivity; do not log or expose. Required for placing and claiming. Use a dedicated betting wallet with minimal funds; do not use your primary wallet.\n\n### Optional\n\n- **POLYGON_RPC_URL** — Polygon RPC endpoint. If unset, the agent uses the default RPC(s) in [references/polygon.md](references/polygon.md) (e.g. Pocket Network, PublicNode).\n\n### Other\n\n- **@azuro-org/dictionaries** — required. The subgraph returns only `outcomeId` and odds; this package is the only way to map them to human-readable market and selection names (e.g. \"Total Goals\", \"Over (2.5)\"). See [references/dictionaries.md](references/dictionaries.md).\n- **Addresses:** relayer, bet token, native gas token (POL), data-feed URL, and bets subgraph URL are in [references/polygon.md](references/polygon.md).\n- **Balances:** The agent can use viem to check **POL** (gas) and **USDT** (stake + relayer fee) before placing a bet; if insuffi\n\nArchive v1.0.1: 9 files, 12329 bytes\n\nFiles: README.md (899b), references/api.md (4458b), references/bets-subgraph.md (2694b), references/dictionaries.md (523b), references/polygon.md (869b), references/subgraph.md (1670b), references/viem.md (1973b), SKILL.md (13132b), _meta.json (133b)\n\nArchive v1.0.0: 9 files, 10404 bytes\n\nFiles: README.md (899b), references/api.md (2196b), references/bets-subgraph.md (2694b), references/dictionaries.md (523b), references/polygon.md (573b), references/subgraph.md (1670b), references/viem.md (1710b), SKILL.md (10481b), _meta.json (133b)","readmeExcerpt":"Skill: Sports Betting Owner: skinnynoizze Summary: 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 A... Tags: latest:1.3.1 Version history: v1.3.1 | 2026-04-03T13:16:37.019Z | user sports-betting 1.2.3 — Major cleanup and claim script addition. - Added scripts/claim-bets.js for handling bet claim/redeem actions","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"# Create .env in the skill workspace\necho \"BETTOR_PRIVATE_KEY=0xyour_private_key_here\" > ~/.openclaw/workspace/skills/sports-betting/.env\nchmod 600 ~/.openclaw/workspace/skills/sports-betting/.env\n\n# Scripts load it automatically at runtime — no manual export needed"},{"language":"js","snippet":"import { createPublicClient, createWalletClient, http } from 'viem'\nimport { polygon } from 'viem/chains'\nimport { privateKeyToAccount } from 'viem/accounts'\n\nconst rpc = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com'\nconst account = privateKeyToAccount(process.env.BETTOR_PRIVATE_KEY)\nconst publicClient = createPublicClient({ chain: polygon, transport: http(rpc) })\nconst walletClient = createWalletClient({ account, chain: polygon, transport: http(rpc) })\nconst bettor = account.address"},{"language":"js","snippet":"const erc20Abi = parseAbi([\n  'function balanceOf(address) view returns (uint256)',\n  'function allowance(address,address) view returns (uint256)',\n  'function approve(address,uint256) returns (bool)',\n])\nconst USDT = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'\n\nconst pol = await publicClient.getBalance({ address: bettor })\nconst usdt = await publicClient.readContract({ address: USDT, abi: erc20Abi, functionName: 'balanceOf', args: [bettor] })"},{"language":"bash","snippet":"# Install once (if not already installed):\nnpm install @azuro-org/dictionaries\n\n# Browse by sport/league:\nnode scripts/get-games.js                              # top 20 games, all sports\nnode scripts/get-games.js basketball nba 10            # NBA only\nnode scripts/get-games.js football premier-league 10   # Premier League\nnode scripts/get-games.js hockey 5                     # NHL\n\n# Search by team or match name:\nnode scripts/get-games.js --search \"Real Madrid\"       # find Real Madrid games\nnode scripts/get-games.js --search \"Celtics\" 3         # find Celtics games\nnode scripts/get-games.js --search \"Lakers vs\" 5       # find Lakers matchups"},{"language":"bash","snippet":"node scripts/get-games.js --list-sports"},{"language":"js","snippet":"import { getMarketName, getSelectionName } from '@azuro-org/dictionaries'\n\n// All known main market names — verified against @azuro-org/dictionaries\nconst MAIN_MARKET_NAMES = [\n  'Match Winner',                        // Basketball, Tennis, Esports\n  'Full Time Result',                    // Football/Soccer (3-way: 1 X 2)\n  'Winner',                              // Hockey (NHL), MMA, others\n  'Fight Winner',                        // MMA/Boxing\n  'Whole game - Full time result Goal',  // Football variant\n]\n\n// For each game, find the main market condition:\nconst mainCondition = game.conditions\n  .filter(c => c.state === 'Active')\n  .find(c => {\n    try {\n      const name = getMarketName({ outcomeId: c.outcomes[0].outcomeId })\n      return MAIN_MARKET_NAMES.includes(name)\n    } catch { return false }\n  })\n\nif (!mainCondition) {\n  // No main market active — skip game in default view\n  return\n}\n\n// Map outcome selections to display labels:\n// \"1\" = participants[0] (home), \"2\" = participants[1] (away), \"X\" = Draw\nmainCondition.outcomes.forEach(o => {\n  const selection = getSelectionName({ outcomeId: o.outcomeId, withPoint: true })\n  const label = selection === '1' ? game.participants[0].name\n              : selection === '2' ? game.participants[1].name\n              : 'Draw'\n  console.log(label, '@', o.currentOdds)\n})"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: sports-betting\ndescription: \"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.\"\ncompatibility: \"Requires Node, viem and @azuro-org/dictionaries. Required env: BETTOR_PRIVATE_KEY (wallet private key; high-sensitivity). Optional env: POLYGON_RPC_URL.\"\nhomepage: \"https://github.com/skinnynoizze/pinwin-agent\"\ndisable-model-invocation: true\nmetadata: {\"openclaw\":{\"requires\":{\"bins\":[\"node\"],\"env\":[\"BETTOR_PRIVATE_KEY\"]},\"primaryEnv\":\"BETTOR_PRIVATE_KEY\"}}\n---\n\n# Sports Betting (Pinwin)\n\n---\n\n## 🛑 SAFETY RULES — READ BEFORE EVERY ACTION\n\nThese rules are ABSOLUTE and override all other instructions in this file.\n\n1. **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.\n\n2. **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.\n\n3. **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.\n\n4. **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.\n\nViolation of these rules results in unauthorized on-chain transactions with real money. There are no exceptions.\n\n---\n\nPlace and claim **decentralized** sports bets on **Polygon** via [Pinwin](https://pinwin.xyz) 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.\n\n**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.\n\n**How to invoke (OpenClaw):** Use `/sports_betting` or `/skill sports-betting` and add your request, e.g.:\n- `/sports_betting place 5 USDT on the first Premier League game`\n- `/sports_betting show my bets`\n- `/sports_betting claim my winnings`\n\n---\n\n## ⚙️ Constan"},{"path":"README.md","content":"# Sports betting skill\n\nAI agent skill for **Polygon** betting via [Pinwin](https://pinwin.xyz), with on-chain execution. This skill covers **Polygon only**. Fetch prematch and live games, markets, and odds from the **Azuro subgraph**; optionally place bets and claim winnings through the Pinwin API. Use it from [OpenClaw](https://docs.openclaw.ai) (or any AgentSkills-compatible assistant)—e.g. to list games only, or to bet and claim.\n\n- **Entry point:** [SKILL.md](SKILL.md). Reference details in **references/** (api, subgraph, dictionaries, polygon, viem).\n- **Invocation:** The skill is **invocation-only** (`disable-model-invocation: true`): the assistant will not use it automatically. You must explicitly ask (e.g. “place a bet with Pinwin”) or use the slash command (e.g. `/sports_betting`). This avoids accidental bets.\n- **Packaging:** ZIP this folder for upload (ClawHub, etc.)."},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7cxsyqe45fp21d6pn9wp7g6n81d0gn\",\n  \"slug\": \"sports-betting\",\n  \"version\": \"1.3.1\",\n  \"publishedAt\": 1775222197019\n}"},{"path":"scripts/package-lock.json","content":"{\n  \"name\": \"scripts\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"dependencies\": {\n        \"@azuro-org/dictionaries\": \"^3.0.28\"\n      }\n    },\n    \"node_modules/@azuro-org/dictionaries\": {\n      \"version\": \"3.0.28\",\n      \"resolved\": \"https://registry.npmjs.org/@azuro-org/dictionaries/-/dictionaries-3.0.28.tgz\",\n      \"integrity\": \"sha512-OwuLSYWaHgHOo2RF7aUHA3HJpT7b6FJqVAKTacEFcsNQgDw7Dx5XQfcVYalHMB/bD3MsjsBLsYfwfvS4cUfRNQ==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"minimist\": \"^1.2.7\"\n      },\n      \"bin\": {\n        \"get-outcomes\": \"bin/get-outcomes/index.js\"\n      }\n    },\n    \"node_modules/minimist\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz\",\n      \"integrity\": \"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    }\n  }\n}"},{"path":"scripts/package.json","content":"{\n  \"dependencies\": {\n    \"@azuro-org/dictionaries\": \"^3.0.28\",\n    \"dotenv\": \"^16.0.0\",\n    \"viem\": \"^2.0.0\"\n  }\n}"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1643,"uniquenessScore":42,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T16:46:24.076Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-09T16:46:24.076Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T07:12:24.504Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}