diff --git a/README.md b/README.md
index 716241d0e..a735b5887 100644
--- a/README.md
+++ b/README.md
@@ -328,15 +328,24 @@ Use nanobot for a real task, report what broke, and then pick a focused improvem
- Browse [open issues](https://github.com/HKUDS/nanobot/issues) for problems to investigate.
- Open a [pull request](https://github.com/HKUDS/nanobot/pulls) for a focused fix or integration.
-## Contact
+## Team
-Nanobot was started by [Xubin Ren](https://github.com/re-bin) as a personal open-source project and is now maintained collaboratively with contributors from the open-source community. Feel free to contact [xubinrencs@gmail.com](mailto:xubinrencs@gmail.com) for questions, ideas, or collaboration.
+Nanobot was started by [Xubin Ren](https://github.com/re-bin) as a personal open-source project. Today, Xubin, [Yongru Chen](https://github.com/chengyongru), and contributors from the open-source community maintain it voluntarily. Feel free to contact [xubinrencs@gmail.com](mailto:xubinrencs@gmail.com) for questions, ideas, or collaboration.
### Contributors
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
Thanks for visiting ✨ nanobot!
diff --git a/scripts/update_readme_contributors.py b/scripts/update_readme_contributors.py
new file mode 100644
index 000000000..644a8c4ed
--- /dev/null
+++ b/scripts/update_readme_contributors.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""Refresh the native contributor avatar wall in README.md."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+from pathlib import Path
+from urllib.request import Request, urlopen
+
+REPOSITORY = "HKUDS/nanobot"
+README = Path(__file__).resolve().parents[1] / "README.md"
+START = ""
+END = ""
+MAX_CONTRIBUTORS = 100
+AVATARS_PER_ROW = 10
+
+
+def fetch_contributors() -> list[dict[str, str]]:
+ headers = {
+ "Accept": "application/vnd.github+json",
+ "User-Agent": "nanobot-readme",
+ "X-GitHub-Api-Version": "2022-11-28",
+ }
+ if token := os.environ.get("GITHUB_TOKEN"):
+ headers["Authorization"] = f"Bearer {token}"
+
+ url = f"https://api.github.com/repos/{REPOSITORY}/contributors?per_page={MAX_CONTRIBUTORS}"
+ with urlopen(Request(url, headers=headers), timeout=30) as response: # noqa: S310
+ contributors = json.load(response)
+
+ return [
+ contributor
+ for contributor in contributors
+ if contributor.get("login")
+ and contributor.get("type") != "Bot"
+ and not contributor["login"].lower().endswith("[bot]")
+ ]
+
+
+def render_wall(contributors: list[dict[str, str]]) -> str:
+ avatars = [
+ (
+ f''
+ f''
+ )
+ for contributor in contributors
+ ]
+ rows = [
+ "".join(avatars[index : index + AVATARS_PER_ROW])
+ for index in range(0, len(avatars), AVATARS_PER_ROW)
+ ]
+ wall = "
\n".join(rows)
+ return f"{START}\n{wall}\n{END}"
+
+
+def update_readme(*, check: bool) -> bool:
+ current = README.read_text()
+ before, separator, tail = current.partition(START)
+ if not separator or END not in tail:
+ raise SystemExit("README contributor markers are missing")
+
+ _, _, after = tail.partition(END)
+ updated = f"{before}{render_wall(fetch_contributors())}{after}"
+ if updated == current:
+ return False
+ if check:
+ raise SystemExit("README contributor wall is out of date")
+ README.write_text(updated)
+ return True
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true", help="fail when README.md is out of date")
+ args = parser.parse_args()
+ print("Updated README.md" if update_readme(check=args.check) else "README.md is current")