Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# AGENTS.md

Guidance for AI agents working in this repository.

## Repository Purpose

This repository contains Berachain metadata used by Berachain interfaces:

- Token lists in `src/tokens/`
- Reward vault lists in `src/vaults/`
- Validator lists in `src/validators/`
- Image assets in `src/assets/`

Keep changes scoped to the metadata item being added or fixed. Do not refactor unrelated entries, reformat whole files, or change dependency/workflow files unless the task explicitly requires it.

## Before Editing

1. Run `git status -sb` and preserve unrelated local changes.
2. Pull latest `main` before starting a new metadata PR.
3. Verify source-of-truth details from the relevant governance/forum/request link before editing JSON:
- Token name, symbol, decimals, and address
- Reward vault address
- Staking token address
- Protocol/location URL
- Vault name, preferably matching the request unless repo convention clearly requires otherwise

For reward vault requests, use the forum/request page as the primary source when available. Check the request sections that identify:

- Reward vault address
- Staking token address
- Token pair and token addresses
- Protocol/pool URL
- Requested vault name
- Any attached protocol, token, or vault images

If a forum image is low quality, prefer official project brand assets for token images. For generated vault pair images, compose from official token assets and keep the result compliant with the asset rules below.

## Image Assets

Assets are committed to `src/assets/**` and uploaded by CI to Cloudflare Images. Do not use Cloudinary for new metadata.

Current URL format:

```text
https://imagedelivery.net/qNj7Q3MCke89zoKzav7eDQ/<type>/<filename>/public
```

Where `<type>` is one of:

- `tokens`
- `vaults`
- `validators`
- `protocols`

Asset requirements:

- PNG, JPG, or JPEG only
- 1024x1024 pixels
- Under 5 MB
- PNG files must have no transparent pixels
- Use a solid background when converting SVG or transparent source art

Filename conventions:

- Token image: `src/assets/tokens/<token-address>.png`
- Vault image: `src/assets/vaults/<vault-address>.png`
- Validator image: `src/assets/validators/<validator-pubkey>.png`

For token and vault images, use the exact address casing accepted by `scripts/utils/_imageChecks.ts`. Existing metadata commonly uses lowercase Cloudflare URLs, but the upload script validates non-default asset filenames before upload.

Do not name a vault image after the staking token address. Current vault asset convention is `src/assets/vaults/<vault-address>.<ext>`, and the `logoURI` should use the same vault-address filename.

## Cloudflare Upload Flow

New or changed files under `src/assets/**` trigger the `upload-assets` CI job.

The upload job:

1. Waits for maintainer approval of the `cloudflare-uploads` GitHub Actions environment.
2. Uploads changed images to Cloudflare Images with IDs like `tokens/<filename>` or `vaults/<filename>`.
3. Allows the `images` validation check to run against the uploaded URLs.

Agents should commit the image files and set the expected Cloudflare `logoURI` in JSON before the URL exists. Do not attempt to upload manually unless explicitly asked and credentials are available.

Some older docs or examples may still mention Cloudinary. Treat those as stale for new metadata unless the repo workflow has changed again.

## Adding Token Metadata

Edit `src/tokens/mainnet.json` or the relevant network file.

Required fields:

- `chainId`
- `address`
- `symbol`
- `name`
- `decimals`

Usually include:

- `logoURI`
- `tags` when appropriate, for example `["stablecoin"]`
- `extensions.coingeckoId`, `extensions.pythPriceId`, or `extensions.beraPythPriceId` only when verified

## Adding Vault Metadata

Edit `src/vaults/mainnet.json` or the relevant network file.

Required fields:

- `stakingTokenAddress`
- `vaultAddress`
- `name`
- `protocol`
- `url`
- `categories`

Usually include:

- `logoURI`
- `description`
- `action`
- `owner` when it is already part of the local convention for that protocol/project

Vaults should only be added when they are whitelisted/passed through the relevant governance or forum process. Link the source in the PR body.

Use the forum/request vault name unless there is a clear, verified reason to adapt it to an existing naming convention. Do not add details such as fee tier, island type, or owner unless they are verified from the request, Hub, Kodiak, or another source of truth.

For Kodiak vaults, common fields are:

- `protocol`: `Kodiak`
- `categories`: `["defi/amm"]`
- `url`: Kodiak pool URL, including `farm=<vaultAddress>` when available
- `description`: `Acquired by depositing liquidity into the <PAIR> Pool on Kodiak`
- `action`: `Stake <TOKEN A> / <TOKEN B>`

## Validation

Run focused checks after editing:

```bash
pnpm biome check <changed-json-files>
pnpm validate:json .
pnpm validate:images .
```

`pnpm validate` also runs data, Pyth, and CoinGecko checks. Those may require network access or CI secrets, so local failures can be environmental. Record exactly what was run and any environmental limitation in the PR body.

If `tsx` fails locally with an IPC permission error under the system temp directory, rerun the same validator with appropriate sandbox approval instead of changing code.

## PR Expectations

PRs should include:

- A concise summary of metadata/assets added
- The governance/forum/source link used for verification
- The key addresses verified from that source
- Validation commands run
- Note that new assets require `cloudflare-uploads` approval if `src/assets/**` changed

Stage only the intended files. For asset PRs, this is usually:

- The relevant JSON metadata file(s)
- The corresponding file(s) under `src/assets/**`
62 changes: 62 additions & 0 deletions skills/generate-vault-pair-image/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
name: generate-vault-pair-image
description: Generate repo-ready split-token vault, pool, or LP images from two underlying token logos. Use when a user needs a 1024x1024 paired-token vault image, half-and-half token icon, LP pair logo, Berachain metadata vault asset, Cloudflare-ready token pair image, or asks to recreate a blurry forum vault image from cleaner underlying token assets.
---

# Generate Vault Pair Image

## Workflow

1. Identify the two underlying token images.
- Prefer official brand-kit assets or existing repository token assets.
- If the source is SVG or transparent PNG, render/flatten it onto a solid background.
- Do not upscale a blurry forum image when cleaner token assets exist.
2. Decide output path and naming.
- For Berachain metadata vaults, use `src/assets/vaults/<vault-address>.png`.
- Do not name vault images after the staking token/pool address.
3. Generate a 1024x1024 image.
- Left token fills the left half.
- Right token fills the right half.
- Apply an outer circle border and center divider.
- Flatten onto a solid background so PNG transparency checks pass.
4. Validate:
- Dimensions must be `1024x1024`.
- PNG must have no transparent pixels.
- File should be under 5 MB.

## Script

Use `scripts/generate_pair_image.mjs` for deterministic generation. It uses this repo's `sharp` dependency, so run it from the repository root.

Example:

```bash
node skills/generate-vault-pair-image/scripts/generate_pair_image.mjs \
--left src/assets/tokens/0xbca138DEd469F5063589bFdfdD4BC68EB1c3f252.png \
--right src/assets/tokens/0xFCBD14DC51f0A4d49d5E53C2E0950e0bC26d0Dce.png \
--out src/assets/vaults/0x181f3b1d55299f3744188f7ecd082c75a97d2d4f.png \
--border '#ffffff' \
--background '#ffffff'
```

Common options:

- `--left`: left token image, including SVG/PNG/JPG/WebP supported by `sharp`
- `--right`: right token image
- `--out`: output PNG path
- `--border`: outer circle and center divider color, default `#ffffff`
- `--background`: flattened background color, default `#ffffff`
- `--border-width`: outer border width, default `52`
- `--divider-width`: center divider width, default `46`
- `--fit`: token resize mode, default `cover`; use `contain` for logos that should not crop

## Berachain Metadata Notes

For metadata PRs:

- Set `logoURI` to the expected Cloudflare Images URL before CI uploads it.
- Use `https://imagedelivery.net/qNj7Q3MCke89zoKzav7eDQ/vaults/<vault-address>.png/public`.
- Commit the generated image under `src/assets/vaults/`.
- `pnpm validate:images .` should pass after the JSON entry references the image.

If the pair image is based on a forum/request asset, verify the request name and addresses from the forum, but generate the image from official token assets when the attached image is low quality.
112 changes: 112 additions & 0 deletions skills/generate-vault-pair-image/scripts/generate_pair_image.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env node
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";

const requireFromCwd = createRequire(path.join(process.cwd(), "package.json"));
const sharp = requireFromCwd("sharp");

const args = new Map();
for (let i = 2; i < process.argv.length; i += 1) {
const arg = process.argv[i];
if (!arg.startsWith("--")) continue;
const key = arg.slice(2);
const next = process.argv[i + 1];
if (!next || next.startsWith("--")) {
args.set(key, "true");
} else {
args.set(key, next);
i += 1;
}
}

const required = ["left", "right", "out"];
for (const key of required) {
if (!args.get(key)) {
console.error(`Missing --${key}`);
process.exit(1);
}
}

const size = Number(args.get("size") ?? 1024);
const border = args.get("border") ?? "#ffffff";
const background = args.get("background") ?? "#ffffff";
const borderWidth = Number(args.get("border-width") ?? 52);
const dividerWidth = Number(args.get("divider-width") ?? 46);
const fit = args.get("fit") ?? "cover";

const leftPath = args.get("left");
const rightPath = args.get("right");
const outPath = args.get("out");

for (const input of [leftPath, rightPath]) {
if (!fs.existsSync(input)) {
console.error(`Input not found: ${input}`);
process.exit(1);
}
}

if (!["cover", "contain", "fill", "inside", "outside"].includes(fit)) {
console.error(`Invalid --fit value: ${fit}`);
process.exit(1);
}

const renderToken = async (input) =>
sharp(input, { density: 2048 })
.resize(size, size, { fit, background })
.flatten({ background })
.png()
.toBuffer();

const halfMask = (side) => {
const x = side === "left" ? 0 : size / 2;
return Buffer.from(
`<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect x="${x}" y="0" width="${size / 2}" height="${size}" fill="white"/></svg>`,
);
};

const borderSvg = Buffer.from(`
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<circle cx="${size / 2}" cy="${size / 2}" r="${size / 2 - borderWidth / 2}" fill="none" stroke="${border}" stroke-width="${borderWidth}"/>
<line x1="${size / 2}" y1="${borderWidth / 2}" x2="${size / 2}" y2="${size - borderWidth / 2}" stroke="${border}" stroke-width="${dividerWidth}" stroke-linecap="round"/>
</svg>`);

const main = async () => {
const left = await sharp(await renderToken(leftPath))
.composite([{ input: halfMask("left"), blend: "dest-in" }])
.png()
.toBuffer();
const right = await sharp(await renderToken(rightPath))
.composite([{ input: halfMask("right"), blend: "dest-in" }])
.png()
.toBuffer();

fs.mkdirSync(path.dirname(outPath), { recursive: true });

await sharp({
create: {
width: size,
height: size,
channels: 4,
background,
},
})
.composite([
{ input: left, left: 0, top: 0 },
{ input: right, left: 0, top: 0 },
{ input: borderSvg, left: 0, top: 0 },
])
.flatten({ background })
.png()
.toFile(outPath);

const meta = await sharp(outPath).metadata();
const stat = fs.statSync(outPath);
console.log(`${outPath}`);
console.log(`${meta.width}x${meta.height}, ${stat.size} bytes`);
};

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading