Skill: aaigotchi/aavegotchi-traits (version 1.1.0)

--- name: aavegotchi-traits description: Retrieve Aavegotchi NFT data by gotchi ID or name on Base. Returns traits, wearables, rarity scores, kinship, XP, level, and owner data. --- # Aavegotchi Traits Fetch Aavegotchi data on Base by ID or name. ## Usage ```bash cd scripts node get-gotchi.js 9638 node get-gotchi.js aaigotchi ./gotchi-info.sh 9638 ``` ## What it returns - Token ID, name, owner, haunt - BRS / modified BRS - Kinship, XP, level - Base + modified traits - Equipped wearables with names - Collateral, staked amount, last interaction - JSON output for automation ## Environment - `AAVEGOTCHI_RPC_URL` (optional) - `AAVEGOTCHI_SUBGRAPH_URL` (optional; default Goldsky Base subgraph) - `AAVEGOTCHI_SEARCH_BATCH_SIZE` (optional) - `AAVEGOTCHI_RPC_RETRIES` (optional) - `AAVEGOTCHI_RPC_RETRY_DELAY_MS` (optional) ## Reliability - Name lookup uses subgraph first for speed. - If subgraph fails, script scans on-chain with RPC retry/backoff to handle rate limits.

path: README.md
content: # Aavegotchi Traits Skill

Fetch Aavegotchi NFT stats on Base by gotchi ID or name.

## Features

- ID lookup via on-chain contract call
- Name lookup via subgraph first, with on-chain fallback
- Trait/BRS/Kinship/XP/level output
- Equipped wearable IDs and names
- Human output + JSON output

## Quick Start

```bash
cd scripts
npm install

# By ID
node get-gotchi.js 9638

# By name
node get-gotchi.js aaigotchi
```

## Environment Variables

- `AAVEGOTCHI_RPC_URL`
  - Optional Base RPC override
  - Default: `https://mainnet.base.org`
- `AAVEGOTCHI_SUBGRAPH_URL`
  - Optional subgraph override
  - Default: public Goldsky Base subgraph
  - Set to empty string to disable subgraph and force on-chain name scan
- `AAVEGOTCHI_SEARCH_BATCH_SIZE`
  - On-chain name scan batch size (default `25`)
- `AAVEGOTCHI_RPC_RETRIES`
  - Retry attempts for rate-limited RPC calls (default `4`)
- `AAVEGOTCHI_RPC_RETRY_DELAY_MS`
  - Base retry delay in ms (default `250`, exponential backoff)

## Notes

- Base chain only (`8453`)
- Name search is case-insensitive
- Partial names via subgraph may return closest match when exact match is unavailable


path: references/aavegotchi-data.md
content: # Aavegotchi Data Reference

## Contract Information

**Contract Address (Base Mainnet):** `0xa99c4b08201f2913db8d28e71d020c4298f29dbf`

**Network:** Base (Chain ID: 8453)

**RPC:** `https://mainnet.base.org`

## Gotchi Traits

Each Aavegotchi has 6 numeric traits that affect its rarity and appearance:

1. **Energy (NRG)** - Affects stamina and activity
2. **Aggression (AGG)** - Affects combat behavior
3. **Spookiness (SPK)** - Affects mysterious abilities
4. **Brain Size (BRN)** - Affects intelligence
5. **Eye Shape (EYS)** - Visual trait
6. **Eye Color (EYC)** - Visual trait

Each trait has:
- **Base Value**: The original rolled trait value
- **Modified Value**: Base value + wearable modifiers

Trait values range from 0-99, with extreme values (very low or very high) being rarer.

## Rarity Score (BRS)

**Base Rarity Score (BRS)** is calculated from trait rarity:
- Common traits: 50-50 (average)
- Uncommon traits: Further from 50
- Rare traits: Very close to 0 or 99

Higher BRS = Rarer Gotchi = More valuable

**Modified Rarity Score** includes wearable bonuses added to BRS.

## Kinship

**Kinship** represents the bond between Gotchi and owner:
- Starts at 50
- Increases by petting (max once per 12 hours)
- Decreases if not interacted with regularly
- Affects rewards in some games

## Haunt

**Haunt** is the generation/season when a Gotchi was summoned:
- Haunt 1: Original generation
- Haunt 2: Second generation
- Haunt 3: Current generation on Polygon (not Base yet)

On Base, Haunt numbers may differ from Polygon mainnet.

## Wearables

Aavegotchis can equip up to 16 wearables:
- Each wearable has an ID
- Wearables modify traits
- Wearables add to rarity score
- Some wearables are very rare and valuable

The `equippedWearables` array has 16 slots:
- 0 = body
- 1 = face
- 2-4 = eyes
- 5-7 = head
- 8-11 = hands (left/right)
- 12-15 = pet

Slots with value 0 = nothing equipped.

## Status Codes

- 0: Portal (not opened)
- 1: VRF Pending (being generated)
- 2: Open Portal (ready to choose)
- 3: Aavegotchi (summoned and alive)

## Age Calculation

Age is typically measured as:
- Days since summoning
- Or: Days since last interaction (as shown in the script)

## Level & Experience

- Gotchis gain XP through various activities
- XP threshold increases for each level
- Higher level = more wearable slots and abilities

## Collateral

Each Aavegotchi is staked with an ERC20 token (collateral):
- Common collaterals: GHST, DAI, USDC, WETH
- Collateral amount affects gotchi value
- Can be increased but not withdrawn while gotchi is alive


path: scripts/get-gotchi.js
content: #!/usr/bin/env node

import { ethers } from 'ethers';
import fetch from 'node-fetch';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const wearablesData = JSON.parse(
  readFileSync(join(__dirname, 'wearables-data.json'), 'utf-8')
);

const AAVEGOTCHI_CONTRACT = '0xa99c4b08201f2913db8d28e71d020c4298f29dbf';
const DEFAULT_BASE_RPC = 'https://mainnet.base.org';
const DEFAULT_SUBGRAPH_URL =
  'https://api.goldsky.com/api/public/project_cmh3flagm0001r4p25foufjtt/subgraphs/aavegotchi-core-base/prod/gn';

const BASE_RPC = process.env.AAVEGOTCHI_RPC_URL || DEFAULT_BASE_RPC;
const SUBGRAPH_URL = process.env.AAVEGOTCHI_SUBGRAPH_URL === ''
  ? null
  : (process.env.AAVEGOTCHI_SUBGRAPH_URL || DEFAULT_SUBGRAPH_URL);

function intEnv(name, fallback) {
  const raw = process.env[name];
  if (!raw) return fallback;
  const parsed = Number(raw);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

const SEARCH_BATCH_SIZE = intEnv('AAVEGOTCHI_SEARCH_BATCH_SIZE', 25);
const RPC_RETRIES = intEnv('AAVEGOTCHI_RPC_RETRIES', 4);
const RPC_RETRY_DELAY_MS = intEnv('AAVEGOTCHI_RPC_RETRY_DELAY_MS', 250);

const AAVEGOTCHI_ABI = [
  'function getAavegotchi(uint256 _tokenId) view returns (tuple(uint256 tokenId, string name, address owner, uint256 randomNumber, uint256 status, int16[6] numericTraits, int16[6] modifiedNumericTraits, uint16[16] equippedWearables, address collateral, address escrow, uint256 stakedAmount, uint256 minimumStake, uint256 kinship, uint256 lastInteracted, uint256 experience, uint256 toNextLevel, uint256 usedSkillPoints, uint256 level, uint256 hauntId, uint256 baseRarityScore, uint256 modifiedRarityScore, bool locked))',
  'function tokenByIndex(uint256 _index) view returns (uint256)',
  'function totalSupply() view returns (uint256)'
];

const ERC20_ABI = [
  'function decimals() view returns (uint8)',
  'function symbol() view returns (string)'
];

const TRAIT_NAMES = [
  'Energy',
  'Aggression',
  'Spookiness',
  'Brain Size',
  'Eye Shape',
  'Eye Color'
];

const TRAIT_EMOJIS = {
  Energy: '⚡',
  Aggression: '💥',
  Spookiness: '👻',
  'Brain Size': '🧠',
  'Eye Shape': '👁️',
  'Eye Color': '🎨'
};

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function normalizeName(name) {
  return String(name || '')
    .replace(/\u0000/g, '')
    .trim()
    .toLowerCase();
}

function isRateLimitError(error) {
  const primary = String(error?.shortMessage || error?.message || '').toLowerCase();
  const nested = String(error?.info?.error?.message || '').toLowerCase();
  const code = error?.info?.error?.code;

  return (
    primary.includes('rate limit') ||
    primary.includes('429') ||
    nested.includes('rate limit') ||
    nested.includes('over rate limit') ||
    code === -32016
  );
}

async function callWithRetry(label, fn) {
  let lastError = null;

  for (let attempt = 1; attempt <= RPC_RETRIES; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      if (!isRateLimitError(error) || attempt === RPC_RETRIES) {
        throw error;
      }

      const delay = RPC_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
      console.log(`Rate limited on ${label}, retry ${attempt}/${RPC_RETRIES} in ${delay}ms...`);
      await sleep(delay);
    }
  }

  throw lastError || new Error(`Unknown error calling ${label}`);
}

async function querySubgraphByName(name) {
  if (!SUBGRAPH_URL) {
    return null;
  }

  const normalizedSearch = normalizeName(name);
  const query = `
    query GetGotchiByName($name: String!) {
      aavegotchis(where: { name_contains_nocase: $name }, first: 20) {
        id
        name
      }
    }
  `;

  try {
    console.log(`Querying subgraph for "${name}"...`);

    const response = await fetch(SUBGRAPH_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, variables: { name } })
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const result = await response.json();
    if (result.errors?.length) {
      console.log(`Subgraph query returned errors: ${result.errors[0].message}`);
      return null;
    }

    const candidates = result.data?.aavegotchis || [];
    if (candidates.length === 0) {
      return null;
    }

    const exact = candidates.find((g) => normalizeName(g.name) === normalizedSearch);
    if (exact) {
      console.log(`✓ Found exact subgraph match: Gotchi #${exact.id} (${exact.name})`);
      return exact.id;
    }

    const best = candidates[0];
    console.log(`~ No exact subgraph match; using closest: Gotchi #${best.id} (${best.name})`);
    return best.id;
  } catch (error) {
    console.log('Subgraph unavailable, falling back to on-chain search...');
    return null;
  }
}

async function searchGotchiByName(contract, searchName) {
  console.log(`Searching on-chain for Gotchi name: "${searchName}" (case-insensitive exact match)...`);

  try {
    const totalSupply = await callWithRetry('totalSupply()', () => contract.totalSupply());
    const total = Number(totalSupply);
    console.log(`Scanning ${total} gotchis in batches of ${SEARCH_BATCH_SIZE} (RPC-safe mode)...`);

    const searchLower = normalizeName(searchName);
    let checked = 0;

    for (let i = 0; i < total; i += SEARCH_BATCH_SIZE) {
      const end = Math.min(i + SEARCH_BATCH_SIZE, total);

      const tokenIndexPromises = [];
      for (let j = i; j < end; j++) {
        tokenIndexPromises.push(
          callWithRetry(`tokenByIndex(${j})`, () => contract.tokenByIndex(j)).catch(() => null)
        );
      }

      const tokenIds = (await Promise.all(tokenIndexPromises)).filter((id) => id !== null);

      const gotchiPromises = tokenIds.map((tokenId) =>
        callWithRetry(`getAavegotchi(${tokenId.toString()})`, () => contract.getAavegotchi(tokenId)).catch(() => null)
      );

      const gotchis = await Promise.all(gotchiPromises);
      checked += end - i;

      if (checked % 500 === 0 || checked === total) {
        console.log(`Progress: ${checked}/${total} (${Math.round((checked / total) * 100)}%)`);
      }

      for (const gotchi of gotchis) {
        if (gotchi && normalizeName(gotchi.name) === searchLower) {
          console.log(`\nFound on-chain: Gotchi #${gotchi.tokenId} (${gotchi.name})\n`);
          return gotchi;
        }
      }
    }

    console.error(`\nNo gotchi found with name "${searchName}"`);
    return null;
  } catch (error) {
    console.error('Error searching for gotchi:', error.message);
    return null;
  }
}

async function getTokenDecimals(provider, tokenAddress) {
  try {
    const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
    const decimals = await callWithRetry(`decimals(${tokenAddress})`, () => tokenContract.decimals());
    return Number(decimals);
  } catch (error) {
    console.log(`Warning: Could not fetch decimals for ${tokenAddress}, assuming 18`);
    return 18;
  }
}

async function getGotchiInfo(identifier) {
  try {
    const idOrName = String(identifier || '').trim();
    const provider = new ethers.JsonRpcProvider(BASE_RPC);
    const contract = new ethers.Contract(AAVEGOTCHI_CONTRACT, AAVEGOTCHI_ABI, provider);

    let gotchi = null;
    let tokenId = null;

    if (/^\d+$/.test(idOrName)) {
      tokenId = idOrName;
    } else {
      tokenId = await querySubgraphByName(idOrName);

      if (!tokenId) {
        console.log('Subgraph not available or no match found. Falling back to on-chain search...');
        gotchi = await searchGotchiByName(contract, idOrName);
        if (!gotchi) {
          process.exit(1);
        }
      }
    }

    if (!gotchi) {
      gotchi = await callWithRetry(`getAavegotchi(${tokenId})`, () => contract.getAavegotchi(tokenId));
    }

    let stakedAmountFormatted = '0.0';
    if (gotchi.stakedAmount !== 0n) {
      const tokenDecimals = await getTokenDecimals(provider, gotchi.collateral);
      stakedAmountFormatted = ethers.formatUnits(gotchi.stakedAmount, tokenDecimals);
    }

    const ageDays = Math.max(
      0,
      Math.floor((Date.now() / 1000 - Number(gotchi.lastInteracted)) / 86400)
    );

    const data = {
      tokenId: gotchi.tokenId.toString(),
      name: gotchi.name,
      owner: gotchi.owner,
      status: gotchi.status.toString(),
      hauntId: gotchi.hauntId.toString(),
      level: gotchi.level.toString(),
      kinship: gotchi.kinship.toString(),
      experience: gotchi.experience.toString(),
      baseRarityScore: gotchi.baseRarityScore.toString(),
      modifiedRarityScore: gotchi.modifiedRarityScore.toString(),
      traits: {},
      modifiedTraits: {},
      equippedWearables: [],
      collateral: gotchi.collateral,
      stakedAmount: stakedAmountFormatted,
      lastInteracted: new Date(Number(gotchi.lastInteracted) * 1000).toISOString(),
      age: ageDays
    };

    for (let i = 0; i < 6; i++) {
      data.traits[TRAIT_NAMES[i]] = gotchi.numericTraits[i].toString();
      data.modifiedTraits[TRAIT_NAMES[i]] = gotchi.modifiedNumericTraits[i].toString();
    }

    data.equippedWearables = gotchi.equippedWearables
      .map((w) => w.toString())
      .filter((w) => w !== '0');

    data.wearableNames = data.equippedWearables.map((id) => ({
      id,
      name: wearablesData[id] || 'Unknown Item'
    }));

    console.log('='.repeat(60));
    console.log(`AAVEGOTCHI #${data.tokenId}: ${data.name || '(unnamed)'}`);
    console.log('='.repeat(60));
    console.log(`Owner: ${data.owner}`);
    console.log(`Haunt: ${data.hauntId}`);
    console.log(`Level: ${data.level}`);
    console.log(`Age: ${data.age} days since last interaction`);
    console.log('');
    console.log('SCORES:');
    console.log(`  Base Rarity Score (BRS): ${data.baseRarityScore}`);
    console.log(`  Modified Rarity Score: ${data.modifiedRarityScore}`);
    console.log(`  Kinship: ${data.kinship}`);
    console.log(`  Experience: ${data.experience}`);
    console.log('');
    console.log('TRAITS:');
    for (const [trait, value] of Object.entries(data.traits)) {
      const modified = data.modifiedTraits[trait];
      const modifier = modified !== value ? ` (modified: ${modified})` : '';
      const emoji = TRAIT_EMOJIS[trait] || '';
      console.log(`  ${emoji} ${trait}: ${value}${modifier}`);
    }
    console.log('');
    console.log('WEARABLES:');
    if (data.equippedWearables.length > 0) {
      console.log(`  Equipped (${data.equippedWearables.length}):`);
      for (const wearableId of data.equippedWearables) {
        const name = wearablesData[wearableId] || 'Unknown Item';
        console.log(`    ${wearableId}: ${name}`);
      }
    } else {
      console.log('  None equipped');
    }
    console.log('');
    console.log('STAKING:');
    console.log(`  Collateral: ${data.collateral}`);
    console.log(`  Staked Amount: ${data.stakedAmount} tokens`);
    console.log(`  Last Interacted: ${data.lastInteracted}`);
    console.log('='.repeat(60));

    console.log('\nJSON OUTPUT:');
    console.log(JSON.stringify(data, null, 2));
  } catch (error) {
    console.error('Error fetching Aavegotchi data:', error.message);
    const msg = String(error.message || '').toLowerCase();
    if (msg.includes('invalid token') || msg.includes('execution reverted') || msg.includes('missing revert data')) {
      console.error('This Gotchi ID may not exist on Base.');
    }
    process.exit(1);
  }
}

const identifier = process.argv.slice(2).join(' ').trim();
if (!identifier) {
  console.error('Usage: node get-gotchi.js <gotchi-id-or-name>');
  console.error('Examples: node get-gotchi.js 9638');
  console.error('          node get-gotchi.js "aaigotchi"');
  process.exit(1);
}

getGotchiInfo(identifier);


path: scripts/gotchi-info.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Wrapper script for get-gotchi.js

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

usage() {
  cat <<USAGE
Usage: ./gotchi-info.sh <gotchi-id-or-name>

Examples:
  ./gotchi-info.sh 9638
  ./gotchi-info.sh aaigotchi
  ./gotchi-info.sh "My Gotchi Name"
USAGE
}

if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
  usage
  exit 0
fi

if [ "$#" -lt 1 ]; then
  usage
  exit 1
fi

IDENTIFIER="$*"
cd "$SCRIPT_DIR" && node get-gotchi.js "$IDENTIFIER"


path: scripts/package-lock.json
content: {
  "name": "aavegotchi-traits-scripts",
  "version": "1.1.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "aavegotchi-traits-scripts",
      "version": "1.1.0",
      "dependencies": {
        "ethers": "^6.11.1",
        "node-fetch": "^3.3.2"
      }
    },
    "node_modules/@adraffy/ens-normalize": {
      "version": "1.10.1",
      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@noble/curves": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@noble/hashes": "1.3.2"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@noble/hashes": {
      "version": "1.3.2",
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      }
    },
    "node_modules/@types/node": {
      "version": "22.7.5",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.19.2"
      }
    },
    "node_modules/aes-js": {
      "version": "4.0.0-beta.5",
      "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/data-uri-to-buffer": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/ethers": {
      "version": "6.16.0",
      "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/ethers-io/"
        },
        {
          "type": "individual",
          "url": "https://www.buymeacoffee.com/ricmoo"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "@adraffy/ens-normalize": "1.10.1",
        "@noble/curves": "1.2.0",
        "@noble/hashes": "1.3.2",
        "@types/node": "22.7.5",
        "aes-js": "4.0.0-beta.5",
        "tslib": "2.7.0",
        "ws": "8.17.1"
      },
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/fetch-blob": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/jimmywarting"
        },
        {
          "type": "paypal",
          "url": "https://paypal.me/jimmywarting"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "node-domexception": "^1.0.0",
        "web-streams-polyfill": "^3.0.3"
      },
      "engines": {
        "node": "^12.20 || >= 14.13"
      }
    },
    "node_modules/formdata-polyfill": {
      "version": "4.0.10",
      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "fetch-blob": "^3.1.2"
      },
      "engines": {
        "node": ">=12.20.0"
      }
    },
    "node_modules/node-domexception": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Use your platform's native DOMException instead",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/jimmywarting"
        },
        {
          "type": "github",
          "url": "https://paypal.me/jimmywarting"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=10.5.0"
      }
    },
    "node_modules/node-fetch": {
      "version": "3.3.2",
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "data-uri-to-buffer": "^4.0.0",
        "fetch-blob": "^3.1.4",
        "formdata-polyfill": "^4.0.10"
      },
      "engines": {
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/node-fetch"
      }
    },
    "node_modules/tslib": {
      "version": "2.7.0",
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "0BSD"
    },
    "node_modules/undici-types": {
      "version": "6.19.8",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/web-streams-polyfill": {
      "version": "3.3.3",
      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/ws": {
      "version": "8.17.1",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    }
  }
}


path: scripts/package.json
content: {
  "name": "aavegotchi-traits-scripts",
  "version": "1.1.0",
  "description": "Scripts for fetching Aavegotchi traits data",
  "type": "module",
  "dependencies": {
    "ethers": "^6.11.1",
    "node-fetch": "^3.3.2"
  }
}


path: scripts/test-names.js
content: #!/usr/bin/env node

import { ethers } from 'ethers';

const AAVEGOTCHI_CONTRACT = '0xa99c4b08201f2913db8d28e71d020c4298f29dbf';
const BASE_RPC = 'https://mainnet.base.org';

const AAVEGOTCHI_ABI = [
  'function getAavegotchi(uint256 _tokenId) view returns (tuple(uint256 tokenId, string name, address owner, uint256 randomNumber, uint256 status, int16[6] numericTraits, int16[6] modifiedNumericTraits, uint16[16] equippedWearables, address collateral, address escrow, uint256 stakedAmount, uint256 minimumStake, uint256 kinship, uint256 lastInteracted, uint256 experience, uint256 toNextLevel, uint256 usedSkillPoints, uint256 level, uint256 hauntId, uint256 baseRarityScore, uint256 modifiedRarityScore, bool locked))',
  'function tokenByIndex(uint256 _index) view returns (uint256)',
  'function totalSupply() view returns (uint256)'
];

async function testNames() {
  const provider = new ethers.JsonRpcProvider(BASE_RPC);
  const contract = new ethers.Contract(AAVEGOTCHI_CONTRACT, AAVEGOTCHI_ABI, provider);

  console.log('Checking first 200 gotchis for named ones...\n');

  for (let i = 0; i < 200; i++) {
    try {
      const tokenId = await contract.tokenByIndex(i);
      const gotchi = await contract.getAavegotchi(tokenId);
      
      if (gotchi.name && gotchi.name.length > 0) {
        console.log(`Index ${i} => Token ID ${tokenId}: "${gotchi.name}" (length: ${gotchi.name.length}, bytes: ${Buffer.from(gotchi.name).toString('hex')})`);
      }
    } catch (e) {
      // Skip errors
    }
  }
}

testNames();


path: scripts/wearables-data.json
content: {
  "1": "Camo Hat",
  "2": "Camo Pants",
  "3": "MK2 Grenade",
  "4": "Snow Camo Hat",
  "5": "Snow Camo Pants",
  "6": "M67 Grenade",
  "7": "Marine Cap",
  "8": "Marine Jacket",
  "9": "Walkie Talkie",
  "10": "Link White Hat",
  "11": "Link Mess Dress",
  "12": "Link Bubbly",
  "13": "Sergey Beard",
  "14": "Sergey Eyes",
  "15": "Red Plaid",
  "16": "Blue Plaid",
  "17": "LINK Cube",
  "18": "Aave Hero Mask",
  "19": "Aave Hero Shirt",
  "20": "Aave Plush Toy",
  "21": "Captain Aave Mask",
  "22": "Captain Aave Suit",
  "23": "Captain Aave Shield",
  "24": "Thaave Helmet",
  "25": "Thaave Suit",
  "26": "Thaave Hammer",
  "27": "Marc Hair",
  "28": "Marc Outfit",
  "29": "REKT Sign",
  "30": "Jordan Hair",
  "31": "Jordan Suit",
  "32": "Aave Flag",
  "33": "Stani Hair",
  "34": "Stani Lifejacket",
  "35": "Aave Boat",
  "36": "ETH Logo Glasses",
  "37": "ETH TShirt",
  "38": "32 ETH Coin",
  "39": "Foxy Mask",
  "40": "Foxy Tail",
  "41": "Trezor Wallet",
  "42": "Eagle Mask",
  "43": "Nogara Armor",
  "44": "DAO Egg",
  "45": "Ape Mask",
  "46": "Half Rekt Shirt",
  "47": "Waifu Pillow",
  "48": "Xibot Mohawk",
  "49": "Coderdan Shades",
  "50": "GldnXross Robe",
  "51": "Mudgen Diamond",
  "52": "Galaxy Brain",
  "53": "All Seeing Eyes",
  "54": "Llamacorn Shirt",
  "55": "Aagent Headset",
  "56": "Aagent Shirt",
  "57": "Aagent Shades",
  "58": "Aagent Pistol",
  "59": "Aagent Fedora Hat",
  "60": "Common Wizard Hat",
  "61": "Legendary Wizard Hat",
  "62": "Mythical Wizard Hat",
  "63": "Godlike Wizard Hat",
  "64": "Common Wizard Staff",
  "65": "Legendary Wizard Staff",
  "66": "Wizard Visor",
  "67": "Straw Hat",
  "68": "Farmer Jeans",
  "69": "Pitchfork",
  "70": "Handsaw",
  "71": "Red Santa Hat",
  "72": "Jaay Hairpiece",
  "73": "Jaay Glasses",
  "74": "Jaay Suit",
  "75": "OKex Sign",
  "76": "Big GHST Token",
  "77": "Bitcoin Beanie",
  "78": "Black Jeans",
  "79": "Skateboard",
  "80": "Sushi Bandana",
  "81": "Sushi Coat",
  "82": "Sushi Piece",
  "83": "Sushi Knife",
  "84": "Gentleman Hat",
  "85": "Gentleman Coat",
  "86": "Monocle",
  "87": "Miner Helmet",
  "88": "Miner Jeans",
  "89": "Pickaxe",
  "90": "Pajama Hat",
  "91": "Pajama Shirt",
  "92": "Bedtime Milk",
  "93": "Fluffy Pillow",
  "94": "Sweatband",
  "95": "Track Shorts",
  "96": "Water bottle",
  "97": "Pillbox Hat",
  "98": "Day Dress",
  "99": "Parasol",
  "100": "Clutch",
  "101": "Witchy Hat",
  "102": "Witchy Cloak",
  "103": "Witchy Wand",
  "104": "Portal Mage Helmet",
  "105": "Portal Mage Armor",
  "106": "Portal Mage Axe",
  "107": "Portal Mage Black Axe",
  "108": "Rasta Hat",
  "109": "Rasta Shirt",
  "110": "Jamaican Flag",
  "111": "Hazmat Hood",
  "112": "Hazmat Suit",
  "113": "Uranium Rod",
  "114": "Red Hawaiian Shirt",
  "115": "Blue Hawaiian Shirt",
  "116": "Coconut",
  "117": "Cool Shades",
  "118": "Water Jug",
  "119": "Baby Bottle",
  "120": "Martini",
  "121": "Wine",
  "122": "Milkshake",
  "123": "Apple Juice",
  "124": "Beer Helmet",
  "125": "Track Suit",
  "130": "Fireball",
  "131": "Dragon Horns",
  "132": "Dragon Wings",
  "133": "Pointy Horns",
  "134": "L2 Sign",
  "135": "Polygon Shirt",
  "136": "Polygon Cap",
  "137": "Vote Sign",
  "138": "Snapshot Shirt",
  "139": "Snapshot Cap",
  "140": "Elf Ears",
  "141": "Gemstone Ring",
  "142": "Princess Tiara",
  "143": "Gold Necklace",
  "144": "Princess Hair",
  "145": "Godli Locks",
  "146": "Imperial Moustache",
  "147": "Tiny Crown",
  "148": "Royal Scepter",
  "149": "Royal Crown",
  "150": "Royal Robes",
  "151": "Common Rofl",
  "152": "Uncommon Rofl",
  "153": "Rare Rofl",
  "154": "Legendary Rofl",
  "155": "Mythical Rofl",
  "156": "Royal Rofl",
  "157": "Lil Pump Goatee",
  "158": "Lil Pump Drank",
  "159": "Lil Pump Shades",
  "160": "Lil Pump Threads",
  "161": "Lil Pump Dreads",
  "199": "Steampunk Goggles",
  "200": "Steampunk Trousers",
  "201": "Mechanical Claw",
  "202": "VR Headset",
  "203": "Gamer Jacket",
  "204": "Game Controller",
  "205": "Gotchi Mug",
  "206": "Biker Helmet",
  "207": "Biker Jacket",
  "208": "Aviators",
  "209": "Horseshoe Mustache",
  "210": "Haunt 1 Background",
  "211": "Guy Fawkes Mask",
  "212": "1337 Laptop",
  "213": "H4xx0r Shirt",
  "214": "Matrix Eyes",
  "215": "Cyborg Eye",
  "216": "Rainbow Vomit",
  "217": "Energy Gun",
  "218": "Mohawk",
  "219": "Mutton Chops",
  "220": "Punk Shirt",
  "221": "Pirate Hat",
  "222": "Pirate Coat",
  "223": "Hook Hand",
  "224": "Pirate Patch",
  "225": "Basketball",
  "226": "Red Headband",
  "227": "23 Jersey",
  "228": "10 Gallon Hat",
  "229": "Lasso",
  "230": "Wraangler Jeans",
  "231": "Comfy Poncho",
  "232": "Poncho Hoodie",
  "233": "Uncommon Cacti",
  "234": "Shaaman Poncho",
  "235": "Shaaman Hoodie",
  "236": "Blue Cacti",
  "237": "Mythical Cacti",
  "238": "Godlike Cacti",
  "239": "Wagie Cap",
  "240": "Headphones",
  "241": "WGMI Shirt",
  "242": "Yellow Manbun",
  "243": "Tinted Shades",
  "244": "V-Neck Shirt",
  "245": "Gecko Hat",
  "246": "APY Shades",
  "247": "Up Arrow",
  "248": "Up Only Shirt",
  "249": "Gecko Eyes",
  "250": "CoinGecko Tee",
  "251": "Candy Jaar",
  "252": "Aastronaut Helmet",
  "253": "Aastronaut Suit",
  "254": "uGOTCHI Token",
  "255": "Space Helmet",
  "256": "Lil Bubble Space Suit",
  "257": "Bitcoin Guitar",
  "258": "Taoist Robe",
  "259": "Bushy Eyebrows",
  "260": "Beard of Wisdom",
  "261": "Aantenna Bot",
  "262": "Radar Eyes",
  "263": "Signal Headset",
  "292": "Brunette Ponytail",
  "293": "Leather Tunic",
  "294": "Bow and Arrow",
  "295": "Forked Beard",
  "296": "Double-sided Axe",
  "297": "Animal Skins",
  "298": "Horned Helmet",
  "299": "Longbow",
  "300": "Feathered Cap",
  "301": "Alluring Eyes",
  "302": "Geisha Headpiece",
  "303": "Kimono",
  "304": "Paper Fan",
  "305": "Sus Butterfly",
  "306": "Flower Studs",
  "307": "Fairy Wings",
  "308": "Red Hair",
  "309": "Citaadel Helm",
  "310": "Plate Armor",
  "311": "Spirit Sword",
  "312": "Plate Shield",
  "313": "Kabuto Helmet",
  "314": "Yoroi Armor",
  "315": "Haanzo Katana",
  "350": "Pixelcraft Tee",
  "351": "3D Glasses",
  "352": "Pixelcraft Square",
  "353": "Nimbus",
  "354": "Alchemica Apron",
  "355": "Safety Glasses",
  "356": "Bandage",
  "357": "Nail Gun",
  "358": "Flaming Apron",
  "359": "Forge Goggles",
  "360": "Geode Smasher",
  "361": "Geo",
  "362": "FAKE Shirt",
  "363": "FAKE Beret",
  "364": "Paint Brush",
  "365": "Paint Palette",
  "366": "Heavenly Robes",
  "367": "Eyes of Devotion",
  "368": "Beard of Divinity",
  "369": "Staff of Creation",
  "370": "Wavy Hair",
  "371": "Plastic Earrings",
  "372": "Party Dress",
  "373": "Overalls",
  "374": "Lens n Frens Plant",
  "375": "GM Seeds",
  "376": "Lick Brain",
  "377": "Lick Eyes",
  "378": "Lick Tongue",
  "379": "Lick Tentacle",
  "380": "Sebastien Hair",
  "381": "Voxel Eyes",
  "382": "GOAT-ee",
  "383": "Sandbox Hoodie",
  "384": "Faangs",
  "385": "Block Scanners",
  "386": "Staff of Charming",
  "387": "Roflnoggin",
  "404": "Granny Glasses",
  "405": "Freckles",
  "406": "Common Stohn",
  "407": "Based Shades",
  "408": "Rasta Glasses",
  "409": "Braces",
  "410": "Uncommon Stohn",
  "411": "Aloha Flower",
  "412": "Baable Gum",
  "413": "Rare Stohn",
  "414": "Cheap Mask",
  "415": "Wild Fungi",
  "416": "Kawaii Mouth",
  "417": "Baby Licky",
  "418": "Based Shirt",
  "419": "Base App",
  "420": "Jesse Pollak Hair"
}


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: clean (LOW)