# Axiom Protocol — User Agent Skill (XRPL EVM)

This skill enables an AI agent to interact with Axiom prediction markets as a **regular user** on XRPL EVM.

## Scope (Hard Rules)

The agent MUST only perform regular user operations:

1. Read market state and metadata
2. Buy market shares (`buy`)
3. Claim winnings/refunds (`claim`, `claimFor`)

The agent MUST NOT perform privileged or operator/bootstrap actions, including:

- Market creation (`MarketFactory.createMarket`)
- Admin cancellation (`adminCancel`)
- Role/bootstrap/operator actions
- Contract upgrades or implementation registration

These are not optional restrictions: these functions are role/whitelist-gated on-chain and will revert for non-authorized wallets (including normal agent wallets).

If asked to perform restricted actions, the agent must refuse and explain that the transaction would revert unless sent from an authorized bootstrap/operator address.

---

## Chain + Wallet Requirements

- Network: XRPL EVM
- Mainnet chain ID: `1440000`
- Testnet chain ID: `1449000`
- Mainnet RPC: `https://rpc.xrplevm.org`
- Testnet RPC: `https://rpc.testnet.xrplevm.org`
- Native XRP precompile token address: `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`

Agent prerequisites:

1. Have an EVM wallet with funded XRP (for gas + native-XRP market buys)
2. Use the correct chain ID and RPC
3. Simulate transactions before sending when possible

---

## User Flow Playbook

## Market Discovery (Critical)

If an agent scans only recent blocks, it can easily bias toward resolved/closed markets.

Why this happens:

1. Active markets may have been created earlier than your scan window
2. Recent logs are often dominated by resolution/claim activity
3. Some active markets may have low or zero recent on-chain events

Correct discovery strategy:

1. Build candidate market list from `MarketFactory` `MarketCreated` events, starting from factory deployment/start block (not just recent blocks)
2. Keep a persistent local registry of discovered market addresses
3. For each candidate, read `status()` and `betsClose()` directly from the market contract
4. Treat a market as actionable-for-buy only if `status == Active (0)` and current time is before `betsClose`

Do not infer current market status from recency of logs.

### Pseudo-code: discovery + market details

```typescript
// Inputs
// - rpcUrl, chainId
// - marketFactoryAddress
// - factoryDeploymentBlock (or a persisted first-scan block)
// - lastScannedBlock (persisted locally; optional for first run)

type MarketStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;

type MarketSummary = {
  address: `0x${string}`;
  status: MarketStatus;
  betsClose: bigint;
  settlementToken: `0x${string}`;
  outcomeCount: number;
  totalPool: bigint;
  resolvedOutcome: number;
  isBuyActionable: boolean;
};

const MARKET_CREATED_TOPIC0 = keccak256("MarketCreated(address,string,address)");

async function discoverMarketAddresses(client: PublicClient): Promise<`0x${string}`[]> {
  const latestBlock = await client.getBlockNumber();
  const fromBlock = lastScannedBlock ?? BigInt(factoryDeploymentBlock);

  const logs = await client.getLogs({
    address: marketFactoryAddress,
    fromBlock,
    toBlock: latestBlock,
    topics: [MARKET_CREATED_TOPIC0],
  });

  // Decode event and extract market address from args.market
  const discovered = logs
    .map((log) => decodeMarketCreated(log).market as `0x${string}`)
    .filter(Boolean);

  // Merge with persisted registry (important: do not keep only recent discoveries)
  const merged = unique([...loadPersistedMarketRegistry(), ...discovered]);
  persistMarketRegistry(merged);
  persistLastScannedBlock(latestBlock);

  return merged;
}

async function readMarketDetails(client: PublicClient, marketAddress: `0x${string}`): Promise<MarketSummary> {
  const [statusRaw, betsClose, settlementToken, outcomeCountRaw, totalPool, resolvedOutcomeRaw] = await Promise.all([
    client.readContract({ address: marketAddress, abi: TieredParimutuelMarketAbi, functionName: "status" }),
    client.readContract({ address: marketAddress, abi: TieredParimutuelMarketAbi, functionName: "betsClose" }),
    client.readContract({ address: marketAddress, abi: TieredParimutuelMarketAbi, functionName: "settlementToken" }),
    client.readContract({ address: marketAddress, abi: TieredParimutuelMarketAbi, functionName: "outcomeCount" }),
    client.readContract({ address: marketAddress, abi: TieredParimutuelMarketAbi, functionName: "totalPool" }),
    client.readContract({ address: marketAddress, abi: TieredParimutuelMarketAbi, functionName: "resolvedOutcome" }),
  ]);

  const now = BigInt(Math.floor(Date.now() / 1000));
  const status = Number(statusRaw) as MarketStatus;
  const outcomeCount = Number(outcomeCountRaw);
  const resolvedOutcome = Number(resolvedOutcomeRaw);

  return {
    address: marketAddress,
    status,
    betsClose,
    settlementToken,
    outcomeCount,
    totalPool,
    resolvedOutcome,
    isBuyActionable: status === 0 && now < betsClose,
  };
}

async function getActionableMarkets(client: PublicClient): Promise<MarketSummary[]> {
  const addresses = await discoverMarketAddresses(client);
  const details = await Promise.all(addresses.map((address) => readMarketDetails(client, address)));

  // Only these are valid targets for buy flow
  return details.filter((market) => market.isBuyActionable);
}
```

### Pseudo-code: buy preflight from market details

```typescript
async function preflightBuy(market: MarketSummary, outcomeIndex: number): Promise<void> {
  if (market.status !== 0) throw new Error("Market is not Active");

  const now = BigInt(Math.floor(Date.now() / 1000));
  if (now >= market.betsClose) throw new Error("Betting is closed");

  if (outcomeIndex < 0 || outcomeIndex >= market.outcomeCount) {
    throw new Error("Outcome index out of bounds");
  }
}
```

### 1) Read market state

Given a market contract address (`TieredParimutuelMarket` clone), read:

- `status()`
- `outcomeCount()`
- `resolvedOutcome()`
- `settlementToken()`
- `betsClose()`
- `totalPool()`
- `outcomePools(outcomeIndex)`
- `userWeightedShares(user, outcomeIndex)`

Status enum mapping:

- `0` Active
- `1` Paused
- `2` InResolution
- `3` Dispute
- `4` Escalation
- `5` Resolved
- `6` Cancelled

### 2) Buy shares (`buy`)

Function:

`buy(uint8 outcome, uint256 amount, uint256 minShares) payable`

Rules:

- Market must be `Active` (`status == 0`)
- `block.timestamp < betsClose`
- `outcome < outcomeCount`
- For native XRP markets (`settlementToken == 0x000...000`):
  - send value in `msg.value`
  - `amount` parameter is ignored by contract logic
- For ERC20 markets:
  - set `msg.value = 0`
  - pre-approve token spending to market address
  - pass bet amount in `amount`
- Always set nonzero `minShares` slippage guard (strategy-defined)

### 3) Claim payout/refund

Functions:

- `claim()` — claim for self
- `claimFor(address user)` — permissionless helper claim for another user

Rules:

- Claim allowed only when `status == Resolved (5)` or `Cancelled (6)`
- In `Resolved` state, payout depends on winning outcome shares
- In `Cancelled` state, refund returns principal

---

## Outcome Index Handling (Critical)

Never hardcode labels like `["Yes", "No"]` unless metadata explicitly confirms it.

Outcome indexes are authoritative on-chain, but labels are market-specific. Use your source of truth for labels and map by index.

For Axiom-integrated systems, use the outcomes library behavior:

1. Resolve labels from DB/IPFS metadata
2. Keep zero-based index mapping exact
3. Compare user index to `resolvedOutcome`

---

## Safety + Reliability Rules

Before buy:

1. Verify chain ID matches requested network
2. Verify market status is `Active`
3. Verify outcome index bounds
4. Verify token mode (native XRP vs ERC20)
5. Estimate gas / simulate call

Before claim:

1. Verify market is `Resolved` or `Cancelled`
2. For resolved markets, verify user has nonzero winning shares where possible

Error handling guidance:

- Retry transient RPC errors with short backoff
- Do not retry deterministic reverts (`InvalidOutcome`, `BettingClosed`, `NotResolved`, etc.)
- Surface revert reasons to user

---

## Embedded Machine-Readable Manifest (JSON)

Use the JSON below as structured skill metadata. This skill is intentionally **user-only** and excludes privileged actions.

```json
{
  "skill_name": "axiom-user-agent-skill",
  "version": "1.0.0",
  "last_updated": "2026-03-05",
  "scope": {
    "mode": "user-only",
    "enforcement": "onchain-access-control",
    "allowed_operations": [
      "read_market_state",
      "buy_shares",
      "claim_self",
      "claim_for_user"
    ],
    "forbidden_operations": [
      "create_market",
      "admin_cancel_market",
      "register_implementation",
      "upgrade_contracts",
      "bootstrap_or_operator_actions"
    ]
  },
  "networks": {
    "xrpl-mainnet": {
      "chain_id": 1440000,
      "rpc_urls": [
        "https://rpc.xrplevm.org"
      ],
      "explorer_base_url": "https://explorer.xrplevm.org"
    },
    "xrpl-testnet": {
      "chain_id": 1449000,
      "rpc_urls": [
        "https://rpc.testnet.xrplevm.org"
      ],
      "explorer_base_url": "https://explorer.testnet.xrplevm.org"
    }
  },
  "constants": {
    "native_xrp_precompile": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
    "zero_address": "0x0000000000000000000000000000000000000000",
    "decimals": {
      "token_amounts": 18,
      "usd_prices": 8,
      "share_math": 18
    },
    "market_status_enum": {
      "0": "Active",
      "1": "Paused",
      "2": "InResolution",
      "3": "Dispute",
      "4": "Escalation",
      "5": "Resolved",
      "6": "Cancelled"
    }
  },
  "canonical_contracts": {
    "xrpl-mainnet": {
      "AxiomAccessControl": "0x12a663bc4e434157bc0356a24fa63bade0af6b44",
      "AxiomRewards": "0x163cc681d60ca60ac47803eefd7df16a5b005d57",
      "BootstrapAuthority": "0xbfa23eb38ba681ef1d08370388e7ee2454267607",
      "FeeRouter": "0x603fc4e88926c4126e07a8a37001e2b6daba9f77",
      "MarketFactory": "0x8c639e0a6f77dbeec7706faa8d17972e545fe18b",
      "OptimisticSnapshotRegistry": "0x8f8394cf9ea205ac56d89c20c73649ed4458f46e",
      "PriceFeedAggregator": "0x97c9a2ce712abff386794bf91ca524a404a6659b",
      "ProtocolConfig": "0x05ca3f86f534ac495c5f8020c676b0feae74c788",
      "TieredParimutuelMarket": "0x2d762935c47042763bd45bc7116f15b00bca7cea",
      "VaultRegistry": "0xe788550ed96a10d85969077ba77cd6d4fdc404ca",
      "XRPVault": "0x9337051505436d20fdcf7e2ce5a733b49d1042bc"
    },
    "xrpl-testnet": {
      "AxiomAccessControl": "0xd507c67d8773c99007bd3e6b24636709fa2a3ee1",
      "BootstrapAuthority": "0xe9452afecc30a84b294bfe0a6296def485dcad24",
      "PriceFeedAggregator": "0x9d2473aa8353241b91d1de7b2dd8d89bfb3362eb",
      "VaultRegistry": "0xc7b6abc1e6ba0a4a1be7b4d0a10456e09bf597a6",
      "XRPVault": "0x741b7b0898b388f49f8afaa983019fa47d68a493"
    },
    "address_source": "Canonical deployments in database are authoritative. Refresh before high-value execution."
  },
  "contracts": {
    "TieredParimutuelMarket": {
      "role": "User-facing market contract (clones)",
      "allowed_functions": [
        "buy(uint8,uint256,uint256)",
        "claim()",
        "claimFor(address)"
      ],
      "read_functions": [
        "status()",
        "outcomeCount()",
        "resolvedOutcome()",
        "settlementToken()",
        "betsClose()",
        "totalPool()",
        "outcomePools(uint8)",
        "userWeightedShares(address,uint8)"
      ],
      "forbidden_functions": [
        "adminCancel(string)",
        "propose(uint8,string)",
        "challenge(uint8,string)",
        "vote(uint8)",
        "resolve()",
        "finalizeVote()"
      ],
      "artifact_path": "contracts/out/TieredParimutuelMarket.sol/TieredParimutuelMarket.json"
    },
    "MarketFactory": {
      "role": "Deployment factory",
      "forbidden_functions": [
        "createMarket(string,bytes)",
        "registerImplementation(string,address)"
      ],
      "artifact_path": "contracts/out/MarketFactory.sol/MarketFactory.json"
    }
  },
  "market_discovery": {
    "primary_source": "MarketFactory MarketCreated events",
    "event_signature": "MarketCreated(address,string,address)",
    "required_strategy": [
      "scan from factory deployment/start block",
      "persist discovered market addresses",
      "refresh status via onchain read (status, betsClose)",
      "do not use recent-block-only discovery"
    ],
    "actionable_buy_condition": "status == 0 AND now < betsClose"
  },
  "buy_recipe": {
    "prechecks": [
      "status == Active",
      "current_time < betsClose",
      "outcome < outcomeCount"
    ],
    "native_xrp": {
      "when": "settlementToken == zero_address",
      "call": "buy(outcome, amountIgnored, minShares)",
      "msg_value": "betAmount"
    },
    "erc20": {
      "when": "settlementToken != zero_address",
      "steps": [
        "approve(settlementToken, marketAddress, betAmount)",
        "buy(outcome, betAmount, minShares) with msg.value=0"
      ]
    }
  },
  "claim_recipe": {
    "prechecks": [
      "status in [Resolved, Cancelled]"
    ],
    "self": "claim()",
    "third_party": "claimFor(user)"
  },
  "outcomes": {
    "indexing": "zero-based",
    "rule": "Never assume binary labels. Resolve labels from metadata and map by index.",
    "winner_check": "userOutcomeIndex == resolvedOutcome"
  },
  "agent_runtime_policy": {
    "simulate_before_send": true,
    "max_retries_transient_rpc": 3,
    "retry_backoff_ms": [500, 1500, 3000],
    "never_retry_deterministic_revert": true
  }
}
```

---

## Practical Notes

- `TieredParimutuelMarket` markets are clone instances; each market has its own contract address.
- Canonical infra addresses can change over time; refresh from canonical deployment records when available.
- For user operations, market contract address + ABI + chain config are sufficient.
