Skip to content

docs(chains): generate agent-facing Supported Chains tables - #553

Closed
pedromcunha-uneven wants to merge 3 commits into
mainfrom
pedro/supported-chains-agent-table
Closed

pedromcunha-uneven wants to merge 3 commits into
mainfrom
pedro/supported-chains-agent-table

Conversation

@pedromcunha-uneven

@pedromcunha-uneven pedromcunha-uneven commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Supported Chains renders its chain tables in the browser (snippets/Chains.mdx fetches /chains client-side and returns null otherwise). As a result, the page's Markdown export, and llms-full.txt, show empty "Mainnets" and "Testnets" headings, so agents never see Bitcoin, Solana, Tron, TON or Hyperliquid. This keeps the live table for readers and gives agents a generated copy.

Changes / decisions to review

  • scripts/build-chains.mjs fetches https://api.relay.link/chains and https://api.testnets.relay.link/chains and writes one Markdown table per environment to snippets/supported-chains-{mainnets,testnets}.mdx. Each table has these columns: Chain, Chain ID, VM type, Supported Tokens. Rows are sorted by display name, like the live table. Both environments are fetched and rendered before either file is written, so if either API call errors or returns no chains, the script fails and writes nothing. Cell values have MDX characters (| { } < >) escaped and line breaks collapsed, so an unusual chain name can't break the page. The output is deterministic, and --check exits 1 when a snippet is stale.
  • VM type is an extra column that the human table doesn't have. It's what tells an agent which chains are non-EVM (bvm, svm, tvm, tonvm, hypevm, …).
  • Supported Chains includes each snippet inside <Visibility for="agents"> under the existing <Chains> component. Readers see only the live table, and the Markdown export shows only the generated one.
  • .github/workflows/sync-chains.yml runs hourly at :47, offset from Sync Changelog (:17) and Sync API Reference (:37), and on pushes to main that change the script. It copies Sync Changelog's flow: rebuild, then open or force-update one bot/chains-sync PR, and leave an open PR alone when its content already matches. Unlike the changelog, a chain list can revert, so the job deletes the sync branch (closing its PR) whenever the branch's own changes touch only the snippets and the snippets match main again. That check runs from the merge base, so unrelated merges to main don't block it. A branch carrying any other edit is left alone with a warning.
  • Freshness depends on merging. As with the changelog, the agent copy updates only when someone merges the sync PR. The live table is unaffected.
  • No markdown.instructions change. The page now carries the list itself.
  • AGENTS.md marks the two snippets as generated, next to the nav-stub note, so contributors and automation leave them alone.
  • An API outage makes the hourly run fail (red) and write nothing. I didn't add a retry, so the failure stays visible.

Verification

  • mint dev: the page's .md export lists all 57 mainnet chains and 2 testnet chains, with no leftover markup. The rendered page shows only the live table (57 + 2 rows), and there's no visible "VM type" column.
  • MDX escaping: with a hand-added row containing | < > { }, the page still compiles and renders, and the Markdown export shows those characters literally.
  • No partial writes: with the testnet URL pointed at a 404, the run fails and leaves the mainnet snippet untouched.
  • build-chains.mjs --check passes on fresh output and exits 1 after one row is edited by hand.
  • The workflow's shell step was run against a local bare repo with a stubbed gh in five cases: no change (no-op), change with no PR (opens one), same change with a PR open (left alone), new change with a PR open (force-updated), main caught up (branch deleted), /chains reverting before merge (stale branch deleted), a squash merge after unrelated commits landed on main (branch deleted), and a human edit on the sync branch followed by a revert (branch kept, with a warning).
  • mint validate still fails on main because of the existing <StatusWebsockets> error in calling-integration-guide.mdx. This PR doesn't touch that file.

Supported Chains renders its tables client-side, so its Markdown export and
llms-full.txt list no chains. scripts/build-chains.mjs writes Markdown tables
from /chains into snippets shown only to agents, and an hourly workflow opens
a sync PR when the list changes.
@mintlify

mintlify Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
unevenlabs 🟢 Ready View Preview Nov 6, 2026, 8:33 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @pedromcunha-uneven's task in 1m 3s —— View job


Code review

  • Read the PR diff and the files around it (snippets/Chains.mdx, .github/workflows/sync-changelog.yml)
  • Review scripts/build-chains.mjs
  • Review .github/workflows/sync-chains.yml
  • Review the page change and the generated snippets

I found no blocking bugs. The approach fits the problem, and the workflow does what Sync Changelog does. Notes below, most important first.

1. Chain names go into MDX with only | escaped (scripts/build-chains.mjs:21)
cell() escapes |, but the output is MDX, not plain Markdown. A displayName or tokenSupport value from the API containing {, }, < or > (for example Foo <Testnet> or Chain {v2}) would be read as a JSX expression or tag. That breaks the snippet, and with it Supported Chains, once the sync PR merges. The values are clean today, and the sync PR's preview would probably catch it, but the input comes from a live API. Escaping those characters is cheap. One way is to replace {, }, <, > with \{, \}, &lt;, &gt;.
Fix this →

2. The "already carries these chains" check is written differently from Sync Changelog (sync-chains.yml, lines 75–78)
Sync Changelog compares blob SHAs inside an explicit if [ "$state" = "OPEN" ] block with || true. This workflow uses git diff --quiet origin/$SYNC_BRANCH HEAD -- $SNIPPETS 2>/dev/null in the && chain. The behavior is the same: if the remote branch is missing, git exits 128, the check counts as "differs", and the workflow pushes, which is correct. The PR says it copies Sync Changelog's flow, though, so matching its form (or keeping its explanatory comments) would make the two easier to maintain together. The rationale comments from sync-changelog.yml were dropped here, including why the PR state is resolved before the content check and why branch deletion compares the whole tree. Those are the subtle parts.

3. An API outage turns the hourly job red
build-chains.mjs throws on a non-2xx response, an empty list, or the 30s timeout. That is the right choice, since it writes nothing, but any /chains outage will now fail a scheduled workflow every hour. If you'd rather it be quiet, one option is a single retry before failing. Otherwise this is fine as long as people know what the red runs mean.

4. Name casing differs slightly from the live table (cosmetic)
Chains.mdx applies text-transform: capitalize to names, and the generated copy uses the raw displayName. That is harmless, and the raw value is arguably more correct. The extra VM type column is a good addition for agents.

5. Docs and process gaps (non-blocking)

  • AGENTS.md §4.6 documents the generated changelog and says "never edit it by hand", but nothing documents the new generated snippets/supported-chains-*.mdx. It's worth adding a short paragraph, or a line in §1.1 next to the nav-stub note, so automation and contributors don't hand-edit them or flag them under §2 rules (they have no frontmatter, which is correct for snippets).
  • push.paths only covers scripts/build-chains.mjs. If someone hand-edits a snippet on main, the job won't run until the next :47. That's acceptable, just noting it.
  • Like the changelog sync, the PR is opened with GITHUB_TOKEN, so no CI or preview checks run on it automatically. Reviewers will need to trigger a preview by hand if they want one, which matters more given point 1.

Checked and fine

  • Sorting matches the live table: by displayName, with chain ID as a tie-breaker.
  • The output is deterministic, and --check exits 1 when a snippet is stale without writing anything.
  • Visibility is split as intended: <Chains> returns null server-side, so the Markdown export contains only the <Visibility for="agents"> copy, and readers see only the live table.
  • Branch deletion only happens when origin/main and origin/bot/chains-sync have identical trees. That protects human edits on the sync branch.
  • Actions are pinned by SHA, the same as the existing workflows.

I didn't run mint dev or the workflow myself. The verification steps listed in the PR description cover that ground.

@greptile-apps

greptile-apps Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

Adds generated Supported Chains tables for agents while keeping the live tables for readers.

  • Markdown exports now show generated mainnet and testnet chain tables.
  • A scheduled job proposes updated chain tables for review.

Reviews (3) · Last reviewed commit: "docs(chains): close stale sync PRs when ..." · Reviewed by Greptile

Comment thread scripts/build-chains.mjs Resolved
Comment thread scripts/build-chains.mjs Resolved
Escape MDX characters and collapse whitespace in generated cells, render both
environments before writing either snippet, document the generated snippets in
AGENTS.md, and restore the key rationale comments from Sync Changelog.
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @pedromcunha-uneven's task in 44s —— View job


Code review (re-review after 15a4637)

  • Read the new commit and the full PR diff
  • Re-check the earlier review points
  • Compare sync-chains.yml with sync-changelog.yml

Earlier findings are resolved

  • MDX escaping: cell() now collapses whitespace and backslash-escapes | { } < > (scripts/build-chains.mjs:21-25). CommonMark/MDX treat backslash before ASCII punctuation as a literal, so this works for table cells.
  • Partial writes: both environments are fetched and rendered through Promise.all before anything is written (scripts/build-chains.mjs:46). If either request fails, neither file changes.
  • AGENTS.md: §1.1 now marks the snippets as generated and not to be edited by hand.

One new finding, in the workflow

A stale sync PR never closes when /chains goes back to what main has (.github/workflows/sync-chains.yml, the git diff --quiet -- $SNIPPETS branch)
Here's how it happens:

  1. A chain is added to (or removed from) /chains.
  2. The job opens bot/chains-sync with that change.
  3. Before anyone merges the PR, the API goes back to its earlier state, for example a chain is briefly disabled and then re-enabled.

On the next run the regenerated snippets match main, so the job takes the "already match main" path. That path deletes the branch only when origin/main and origin/bot/chains-sync have identical trees. They don't, because the branch still carries the stale snippet change. The job logs a ::warning:: and exits green, and it never updates the PR again. Every later run does the same until a real chain change arrives. The open PR now shows a chain list that is wrong, and merging it would publish that list to agents.

Sync Changelog uses the same logic, but there it's harmless because the changelog only grows and doesn't revert. Chain lists can flip back and forth, so this case is realistic. One fix: in the "matches main" path, if the sync branch differs from main only in $SNIPPETS (for example git diff --quiet origin/main origin/$SYNC_BRANCH -- . ':(exclude)snippets/supported-chains-*.mdx'), delete the branch, which closes the PR. Branches that carry human edits are still left alone. I can't push workflow changes because the GitHub App has no workflows permission, so a maintainer would need to apply this.
Fix this →

Minor, non-blocking

  • a.displayName.localeCompare(...) (scripts/build-chains.mjs:36) throws a TypeError if a chain in the API response has no displayName. The job fails safely and writes nothing, but the error message won't say what went wrong. String(a.displayName ?? '') would avoid that.
  • The workflow's "already carries these chains" check uses git diff ... 2>/dev/null inside the && condition instead of Sync Changelog's blob-SHA comparison. The behavior is the same: a missing branch counts as different, so the job pushes. This was already raised; it's mentioned again only because the comment explaining the || true / set -e case was dropped.

Checked and fine

  • The rendering is deterministic, --check makes no writes, and rows are sorted the same way as the live table.
  • <Visibility for="agents"> correctly hides the generated copy from readers. <Chains> renders nothing server-side, so the Markdown export carries only the generated tables.
  • Actions are pinned by SHA, and the cron offset (:47) doesn't collide with the other syncs.

I didn't run mint dev or the script, because the runner can't reach the network APIs this needs. This review is based on reading the diff.

Delete the sync branch whenever its own changes touch only the chain snippets
and those snippets match main, measured from the merge base so unrelated
merges to main do not block it. Guard the sort against a missing displayName.
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @pedromcunha-uneven's task in 38s —— View job


Code review (re-review after 41f2cb4)

  • Read the new commit and the current state of sync-chains.yml, build-chains.mjs, the page, and the snippets
  • Re-check the earlier findings
  • Walk through the workflow's branch-retirement cases

I found no blocking issues. The last open finding is fixed, and the new logic holds up in the cases I traced.

Earlier findings are resolved

  • Stale sync PR when /chains reverts is fixed. The "already match main" path now deletes bot/chains-sync whenever the branch's own changes (merge-base..branch) touch only snippets/supported-chains-*.mdx. Measuring from the merge base is the right call, for two reasons:
    • A squash-merged sync PR still gets cleaned up, because the branch's own diff is snippets-only.
    • An unrelated merge to main, or someone clicking "Update branch" on the PR, doesn't block cleanup. A whole-tree comparison against origin/main would have.
  • Missing displayName: the sort now uses String(a.displayName ?? ''), so it can't throw a TypeError (scripts/build-chains.mjs:35).
  • MDX escaping, partial writes, and the AGENTS.md note were already resolved in 15a4637, and they're unchanged here.

Cases I traced through the new delete path

Situation Result
Sync PR merged (squash or merge), snippets match main Branch deleted ✅
/chains reverted before merge Branch deleted, so the stale PR closes ✅
Branch has a non-snippet human edit Warning only, branch kept ✅
No sync branch on the remote ls-remote fails, no-op ✅
main changed meanwhile, /chains still differs Force-update path runs, as before ✅

fetch-depth: 0 fetches all branches, so origin/$SYNC_BRANCH and the merge base are available to the script.

Minor, non-blocking

  • Human edits are protected in one path but not the other (sync-chains.yml, the git checkout -B + git push --force path). The delete path is careful to leave a branch that carries human edits, but whenever /chains differs from main, the update path rebuilds the branch as main plus one commit and force-pushes. That throws away the same edits the delete path protects. Sync Changelog has the same behavior, and the snippets are marked "never edit by hand", so this is consistent with the existing design. If you want the protection to be real, apply the same merge-base check before the force push and warn instead of pushing.
  • The comment on the delete path overstates the guarantee. It says the branch "may hold edits this job did not make", which suggests those edits are kept. Given the point above, it would help to say that only this path preserves them.
  • set -e and merge-base: if git merge-base ever finds no common ancestor (for example after a force-rewrite of main), the step exits red without a clear message. This is very unlikely, and failing loudly is acceptable.

Checked and fine

  • The page puts the generated tables inside <Visibility for="agents">. <Chains> returns null server-side, so readers see one table and the Markdown export shows the other.
  • The generated output is clean. The 57 mainnet rows have no escaped characters today, and the testnet snippet lists Base Sepolia and Sepolia.
  • Sorting matches the live table (displayName, then id), the output is deterministic, and --check doesn't write anything.
  • Actions are pinned by SHA, the cron runs at :47 (no collision), and concurrency blocks overlapping runs.

I couldn't run the script or mint dev because this runner can't reach api.relay.link, and the local clone has no merge base with origin/main. This review is based on the diff and the workflow logic.

Copy link
Copy Markdown
Collaborator Author

Closing in favor of a different approach: serving Supported Chains to agents from a small reverse proxy in front of docs.relay.link that builds the table live from /chains, instead of generated snippets and a sync workflow. Tracked in Linear.

This branch was successfully deployed

1 active deployment
staging — 41f2cb41 Deployed Nov 6, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant