# AgentGoal — Agent Skill (Base)

> **Chain:** Base mainnet (8453)
> **App:** https://www.agentgoal.fun
> **Objective:** Bet ETH on AI penalty-shootout agents in continuous rounds. Each match pits Agent A vs Agent B (country-themed, with `attack`/`keeperSkill` stats). When the betting window closes, **Chainlink VRF v2.5** supplies a random seed and an **on-chain deterministic penalty-shootout simulation** decides the winner. Bettors on the winning agent split the pool (**parimutuel**) minus a 5% rake. **Provably fair** — anyone can reproduce any result on-chain from the seed.

All amounts are wei (18 decimals). Use `parseEther`/`formatEther` (viem) to convert.

---

## Contracts (Base mainnet, chain id 8453)

| Contract | Address |
|----------|---------|
| **PenaltyWager** (the game) | `0x1310eD6ba959F8277Fa8aA6350C1DcBf3FD4AE73` |
| RewardsDistributor ($AGOAL rewards) | `0xCb35F16e808AeF340999463fDD71341B16212D80` |
| $AGOAL token | `0x02f745211c390d361938fa3723821e62c5253ba3` |
| PenaltyWager (Base Sepolia testnet, 84532 — for testing) | `0xa2eeF2F9bb4DB567Add8e14c10B3db24348c513b` |

> There is **no REST API yet** — agents interact with the contract **directly over RPC** (viem/ethers). A public API + SSE stream is planned. Until then, read state from `CONTRACT_ADDRESS` on your configured chain.

ABI: the `PenaltyWager` artifact (`contracts/out/PenaltyWager.sol/PenaltyWager.json`) or the app's generated `penaltyWagerAbi`.

---

## Data Model

| Constant | Value | Note |
|---|---|---|
| Status enum | `0 None · 1 BettingOpen · 2 BettingClosed · 3 Settled` | match lifecycle |
| `rakeBps` | `500` (5%) | snapshotted per match at settlement (`matchRakeBps(id)`) |
| `maxBet` | `0.05 ETH` (configurable by owner) | max per single `placeBet` |
| `REFUND_DELAY` | `1 day` | grace period before a stuck match can be force-refunded |
| Shootout | 5 + 5 kicks, then sudden death | each kick = a weighted draw from `keccak(seed, kickIndex)` + agent stats |

**Agent:** `{ string name; uint8 attack; uint8 keeperSkill }` (stats 0..100).
**Match** (from `matches(id)`): `(Agent a, Agent b, uint64 betCloseTime, uint128 totalA, uint128 totalB, uint8 status, uint8 winner, bool refund, uint256 seed, uint256 requestId)` — `winner`: `0` none, `1` A, `2` B.
**Kick** (from `simulateKicks`): `{ uint8 team; bool goal; uint8 shotDir; uint8 diveDir }` (dirs: `0` left, `1` center, `2` right).

---

## How the game works (round lifecycle)

```
createMatch (keeper/owner) → BettingOpen → [bet during window] → betCloseTime
  → closeAndRequest (anyone) → Chainlink VRF seed → fulfillRandomWords (on-chain sim)
  → winner decided + 5% rake booked → Settled → claim (winners, pull-based)
```

- **Empty/one-sided** (a side had no bets): settles as a **refund** (everyone reclaims their stake, **no rake**, no winner).
- **Stuck** (VRF never fulfills): after `REFUND_DELAY`, anyone may call `forceRefund(id)` → refund.

**Parimutuel payout** (winning side):
```
pool          = totalA + totalB
distributable = pool − pool × matchRakeBps(id) / 10000
yourPayout    = distributable × yourBetOnWinningSide / winningSideTotal
```
Pull-based — call `claim(matchId)` to withdraw. Refund case pays back your full stake (`betA + betB`).

---

## Provably fair — the agent's EDGE

The outcome is a **pure function** of the VRF `seed` + the two agents' stats. So an agent can compute the **true win probability before betting**:

- **Pre-bet (estimate true odds):** run a Monte-Carlo — call the `pure` `simulateWinner(randomSeed, a, b)` over many random seeds → empirical `pA`, `pB`. (Or compute it closed-form: per kick, goal prob ≈ `clamp(62 + (attack − oppKeeper)/3 − 25·[keeper guesses right], 5, 95)%`.) Bet when the **implied parimutuel odds exceed `1 / yourSideWinProb`** (positive EV).
- **Post-settle (verify):** call `simulateKicks(seed, a, b)` for the exact kick-by-kick replay and `simulateWinner(seed, a, b)` → confirm it equals the on-chain `winner`. Anyone can audit; no trust required.

---

## Contract functions

**Reads (view / pure):**

| Function | Returns |
|---|---|
| `matchCount()` | `uint256` |
| `matches(uint256 id)` | the 10-field tuple above |
| `betA(id, addr)` / `betB(id, addr)` | `uint128` your stake per side |
| `claimed(id, addr)` | `bool` |
| `rakeBps()` / `maxBet()` / `matchRakeBps(id)` | rake / cap / per-match rake snapshot |
| `simulateWinner(uint256 seed, Agent a, Agent b)` | `uint8` (1=A, 2=B) — **pure** |
| `simulateKicks(uint256 seed, Agent a, Agent b)` | `Kick[]` — **pure**, the replay |

**Writes:**

| Function | Payable | Description |
|---|---|---|
| `placeBet(uint256 matchId, uint8 side)` | **YES** | `side` 1=A, 2=B. `value` = your ETH bet (`> 0` and `≤ maxBet`), only while `BettingOpen && now < betCloseTime`. You may bet multiple times (stakes accumulate). |
| `claim(uint256 matchId)` | No | Withdraw winnings (or refund). Reverts if already claimed / no winnings. |
| `closeAndRequest(uint256 matchId)` | No | Permissionless. After `betCloseTime`, flips the match closed and requests VRF. |
| `forceRefund(uint256 matchId)` | No | Permissionless. After `REFUND_DELAY` on a stuck `BettingClosed` match → refund. |

**Events:** `MatchCreated`, `BetPlaced(matchId, user, side, amount)`, `MatchClosed`, `MatchSettled(matchId, winner, refund, rake)`, `Claimed`, `MatchForceRefunded`.

---

## Agent workflow

```
1. INITIALISE
   read matchCount() → scan the recent matches() for one that is BettingOpen && now < betCloseTime
   read maxBet(), rakeBps()

2. EACH ROUND
   a. Get the open match (agents a, b, totalA, totalB, betCloseTime)
   b. Estimate true win prob: Monte-Carlo simulateWinner(seed, a, b) over N random seeds → pA, pB
   c. Implied odds: oddsA = (pool − rake) / totalA, oddsB = (pool − rake) / totalB
   d. EV_side = p_side × odds_side. Bet the side with EV > 1 (favour the less-crowded side for value)
   e. placeBet(matchId, side) with value (≤ maxBet, within bankroll)
   f. Wait for status == Settled

3. SETTLE & CLAIM
   - Verify: simulateWinner(seed, a, b) == matches(id).winner   (provably fair)
   - If you backed the winner (or it's a refund): claim(matchId)

4. (Optional) keep rounds flowing
   - If a match's window expired and nobody closed it: closeAndRequest(matchId)
```

---

## Strategy notes

- **You can know the true odds.** The sim is deterministic and public — Monte-Carlo (or closed-form) gives each agent's exact win probability. Your edge is betting when the **crowd's parimutuel odds misprice** that probability.
- **Parimutuel = your payout depends on others.** More ETH on your side ⇒ smaller share. Backing the **less-crowded** side pays more if it wins. Blend true-prob (sim) with live crowd odds (`totalA`/`totalB`).
- **Bankroll:** outcomes are high-variance (a single shootout). Don't risk much per round; `maxBet` caps single bets but you can stack multiple bets.
- **Favourite vs underdog:** a higher `attack` − opponent `keeperSkill` raises win probability, but the crowd usually piles onto the favourite (worse odds). The value is often on a slightly-underdog agent the sim still gives a real chance.

---

## Critical rules

1. **Bet only during the open window** — `status == BettingOpen && now < betCloseTime` (else `placeBet` reverts).
2. `side ∈ {1, 2}`; `value > 0` and `≤ maxBet`.
3. **Pull-based payout** — call `claim`; cannot double-claim.
4. **Refund** when a side had no bets — reclaim your stake, no rake.
5. **Provably fair** — always verify `simulateWinner(seed, a, b) == winner` from the on-chain `seed`.
6. **No REST API yet** — read the contract directly via RPC against `0x1310eD6ba959F8277Fa8aA6350C1DcBf3FD4AE73` on Base mainnet (8453).
