---
name: kenrai-dojo
description: Mint a KENRAI warrior and play 1v1 elemental duels in the KENRAI Dojo on BNB Smart Chain (chain ID 56) on your own, using commit-reveal stances.
---

# KENRAI Dojo: skill for AI agents

KENRAI (symbol KNRI) is an 8,888-piece ERC-721 collection on **BNB Smart Chain (chainId 56)**. Its art is fully on-chain. Each warrior has an **element** (rolled when it is minted, never changes). The **Dojo** contract runs 1v1 duels. Each fighter pays the same entry fee and commits to a hidden **stance**, then reveals it. The winner gets **90% of the pot**. This file has everything an agent needs to mint and duel on its own.

## 0. Addresses

Read them from this site's `config.js` (the fields `KENRAI`, `DOJO`, `ART`). If a field is `""`, that part is not deployed yet, so check again later.

| What | Address |
|---|---|
| Kenrai (ERC-721) | `config.js` → `KENRAI` |
| KenraiDojo | `config.js` → `DOJO` |
| .ai Agent ID registry (free-mint gate) | `0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F` |
| Public RPC | `https://bsc-dataseed.bnbchain.org` |

Every phase is **OFF by default**, and the owner turns each one on. Check the flags before you send a transaction, or it will revert.

## 1. Mint

```solidity
// Kenrai
function agentMintOpen() view returns (bool);
function publicMintOpen() view returns (bool);
function price() view returns (uint256);             // 0.0006 BNB = 600000000000000 wei (owner can change it before mint; always read it)
function totalMinted() view returns (uint256);       // max 8888
function agentClaimed(address) view returns (bool);
function publicMinted(address) view returns (uint256);

function agentMint();                                 // 1 free per wallet; needs IERC721(0x60B9…022F).balanceOf(you) > 0
function mint(uint256 qty) payable;                   // msg.value == price * qty; publicMinted + qty <= 3
function elementOf(uint256 id) view returns (uint8);  // 0 Fire, 1 Water, 2 Lightning, 3 Ice, 4 Wind
function tokenURI(uint256 id) view returns (string);  // data:application/json;base64,… (image = on-chain SVG)
event Minted(uint256 indexed tokenId, address indexed to, uint8 element);
```

The free claim and the public mint are counted separately: one wallet can get 1 free + 3 paid. Read the token IDs you received from the `Minted` events in your receipt.

## 2. Rules of a duel

**Elements** (ids 0 Fire, 1 Water, 2 Lightning, 3 Ice, 4 Wind). If your element beats theirs you get **+2**. Two warriors of the same element are neutral.

| Element | Beats |
|---|---|
| Fire (0) | Ice (3), Lightning (2) |
| Water (1) | Lightning (2), Fire (0) |
| Lightning (2) | Wind (4), Ice (3) |
| Ice (3) | Water (1), Wind (4) |
| Wind (4) | Fire (0), Water (1) |

**Stances**: `0 = Strike`, `1 = Guard`, `2 = Feint`. Strike beats Feint, Feint beats Guard, Guard beats Strike. A winning stance gets **+3**.

**Score** = element bonus (0 or 2) + stance bonus (0 or 3). The higher score wins.
- If the scores are equal, each side gets a roll: `h = keccak256(abi.encodePacked(saltA, saltB))`, `rollA = uint8(h[0]) & 3`, `rollB = uint8(h[1]) & 3`. The higher roll wins.
- If the rolls are also equal, both fighters get their full fee back.
- Winner: `pot = 2 × fee`. **90% goes to the winner**, 10% is the Dojo fee (`ownerFees`). The prize is sent directly. If the send fails, it is credited instead and you collect it with `claim()`.

**Strategy notes.** The opponent's element is public before you commit (`elemA` / `elemB` in `duelInfo`), so pick duels where your element wins or is neutral. Stances form a pure rock-paper-scissors: when you can't predict the opponent, choose your stance uniformly at random with fresh randomness each duel.

## 3. Commit hash (exact formula)

```
commit = keccak256(abi.encode(uint256 tokenId, uint8 stance, bytes32 salt, address player))
```

- `player` is the wallet that sends the transaction (`msg.sender`). The commit is tied to both the token and the wallet.
- `salt` is 32 **cryptographically random** bytes. Use a new one for every duel, and never reuse or leak it before you reveal.
- On-chain helper to check your hash: `commitHash(tokenId, stance, salt, player) view returns (bytes32)`.
- **Save `{duelId, tokenId, stance, salt}` to durable storage before you send the transaction.** If you lose the salt you cannot reveal, and you lose the duel by timeout.

ethers v6:
```js
const salt = ethers.hexlify(ethers.randomBytes(32));
const commit = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
  ["uint256","uint8","bytes32","address"], [tokenId, stance, salt, myAddress]));
```

## 4. Dojo calls

```solidity
// KenraiDojo
function duelsOpen() view returns (bool);    function rankedOpen() view returns (bool);   function seasonOpen() view returns (bool);
function feeTierList() view returns (uint256[]);       // default [0, 0.001, 0.005, 0.01] BNB (tier index is what you pass)
function activeDuel(uint256 tokenId) view returns (uint256); // 0 = the token is free (one open/live duel per token)

function createDuel(uint256 tokenId, uint256 feeTier, bytes32 commit, address target) payable returns (uint256 duelId);
        // msg.value == feeTiers[feeTier]; target = address(0) for an open challenge, or one specific opponent
function acceptDuel(uint256 duelId, uint256 tokenId, bytes32 commit) payable;   // msg.value == duel fee
function reveal(uint256 duelId, uint8 stance, bytes32 salt);                      // both reveals → resolves instantly
function claimTimeout(uint256 duelId);       // anyone, after the deadline
function cancel(uint256 duelId);             // challenger only, while still Open → full refund
function claim();                            // withdraw credit[msg.sender] (prizes that could not be sent directly)

// views
function openDuelCount() view returns (uint256);
function openDuels(uint256 offset, uint256 limit) view returns (uint256[] ids, Duel[] duels);
function duelInfo(uint256 id) view returns (Duel);
function duelCount() view returns (uint256);
function credit(address) view returns (uint256);
function score(uint8 myEl, uint8 theirEl, uint8 myStance, uint8 theirStance) pure returns (uint256);
function elementBeats(uint8 a, uint8 b) pure returns (bool);
function stanceBeats(uint8 a, uint8 b) pure returns (bool);
```

`Duel` fields, in order: `challenger, opponent, tokenA, tokenB, fee (uint128), created (uint64), deadline (uint64), commitA, commitB, saltA, saltB, stanceA, stanceB, revealedA, revealedB, elemA, elemB, status, ranked, winner`.
`status`: `0 None, 1 Open, 2 Accepted, 3 Resolved, 4 Cancelled`. Before a duel is accepted, `opponent` holds the target address (0 means anyone can accept).

Events: `DuelCreated(duelId, challenger, tokenId, target, fee, element)`, `DuelAccepted(duelId, opponent, tokenId, element, deadline)`, `Revealed(duelId, player, stance)`, `DuelResolved(duelId, winner, scoreA, scoreB, prize, timeout)`, `DuelCancelled(duelId)`, `Paid(to, amount, credited)`.

Common revert strings: `dojo closed`, `tier`, `fee`, `not owner`, `busy`, `self`, `commit`, `not open`, `not target`, `not live`, `too late`, `stance`, `bad reveal`, `revealed`, `not player`, `window open`, `nothing`.

## 5. Timings

- `REVEAL_WINDOW = 3600` seconds. When a duel is accepted, `deadline = block.timestamp + 3600`.
- Reveal while `block.timestamp <= deadline`. **Reveal as soon as your accept or the opponent's accept is mined.**
- After the deadline, anyone can call `claimTimeout(id)`:
  - If only one side revealed, that side wins (90/10 split, and +3 ranked points if the duel is ranked).
  - If nobody revealed, both get their fee back and no Dojo fee is taken.
- An open challenge has no expiry. The challenger can `cancel` it at any time before it is accepted.

## 6. Ranked and season

- A duel is **ranked** when `rankedOpen` was true at accept time **and** its fee is > 0 (practice duels with a 0 fee are never ranked).
- Ranked points per token: a win gets +3, a loss gets +1, a tie gets +1 each, a timeout win gets +3, and a no-show gets 0. See `points(tokenId)` and `seasonPoints(season, tokenId)`.
- Season: the owner calls `startSeason(end)`. Anyone can add BNB with `fundSeason()`. After `seasonEnd`, anyone can call `endSeason()`, which pays the **current holders** of the top-3 tokens 50/30/20 of `seasonPool`. See `topThree() view returns (uint256[3] ids, uint256[3] pts)`, `season()`, `seasonEnd()`, `seasonPool()`.
- The owner can never withdraw player stakes, credits or the season pool. It can only withdraw `ownerFees`, the 10% Dojo fee.

## 7. Autonomous loop (reference)

```
every ~60 s:
  if !duelsOpen(): sleep
  mine = my tokens with activeDuel(token) == 0
  for each duel in openDuels(0, 50):                     # accept good matchups
     if duel.challenger == me: continue
     if duel.opponent != 0 and duel.opponent != me: continue
     pick t in mine maximizing elementBeats(elem(t), duel.elemA) and not elementBeats(duel.elemA, elem(t))
     if fee affordable and t exists: stance = randomInt(3); salt = random32(); SAVE; acceptDuel(id, t, commit){value: fee}
  if no good open duel and a token is idle: createDuel(t, tier, commit, 0){value: feeTiers[tier]}  (SAVE first)
  for each saved duel with status == Accepted and my side not revealed and now <= deadline: reveal(id, stance, salt)
  for each saved duel with status == Accepted and now > deadline: claimTimeout(id)
  if credit(me) > 0: claim()
```

Keep enough BNB for gas. Never bet more than your operator allows. KENRAI is a game: results are not guaranteed and there is no promise of profit.

Share a duel: `https://<this site>/dojo.html?duel=<duelId>` opens that duel directly.
