diff --git a/notebooks/01_github_issues.ipynb b/notebooks/01_github_issues.ipynb
index 9b77e78..e6db440 100644
--- a/notebooks/01_github_issues.ipynb
+++ b/notebooks/01_github_issues.ipynb
@@ -24,14 +24,14 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "Fetched 42 issues from TheSoftwareDevGuild/TheGuildGenesis\n"
+ "Fetched 47 issues from TheSoftwareDevGuild/TheGuildGenesis\n"
]
},
{
@@ -69,122 +69,122 @@
"
\n",
" \n",
" | 0 | \n",
- " 126 | \n",
- " Use jupyter notebook to analyze github issues | \n",
+ " 146 | \n",
+ " Figure out how to admin delete profile | \n",
" open | \n",
- " 2025-10-30T13:17:36Z | \n",
- " 2025-10-30T13:17:36Z | \n",
+ " 2025-12-03T17:26:37Z | \n",
+ " 2025-12-03T17:26:37Z | \n",
" joelamouche | \n",
- " joelamouche | \n",
- " jupyter-notebook,python | \n",
+ " | \n",
+ " good first issue,back-end,planning | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 1 | \n",
- " 125 | \n",
- " Improve UX and design of theguild.dev | \n",
+ " 145 | \n",
+ " I think we should allow duplicate attestations | \n",
" open | \n",
- " 2025-10-28T11:23:19Z | \n",
- " 2025-10-28T11:23:19Z | \n",
+ " 2025-12-03T15:18:42Z | \n",
+ " 2025-12-03T15:18:42Z | \n",
" joelamouche | \n",
" | \n",
- " good first issue,ux,design,hacktoberfest | \n",
+ " solidity,planning | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 2 | \n",
- " 118 | \n",
- " Badge enhancements | \n",
+ " 144 | \n",
+ " Implement Upgradable pattern for TheGuildAttes... | \n",
" open | \n",
- " 2025-10-17T08:35:11Z | \n",
- " 2025-10-30T13:44:59Z | \n",
+ " 2025-12-03T10:28:34Z | \n",
+ " 2025-12-03T10:28:55Z | \n",
" joelamouche | \n",
- " joelamouche | \n",
- " enhancement,planning,hacktoberfest | \n",
+ " | \n",
+ " good first issue,solidity,foundry | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 3 | \n",
- " 110 | \n",
- " Discord bot: relay git activity on a discord c... | \n",
+ " 143 | \n",
+ " TheGuildAttestationResolver should have admin ... | \n",
" open | \n",
- " 2025-10-10T09:57:28Z | \n",
- " 2025-10-30T09:00:26Z | \n",
+ " 2025-12-03T10:27:07Z | \n",
+ " 2025-12-03T10:27:51Z | \n",
" joelamouche | \n",
" | \n",
- " good first issue,nodejs,typescript,discord-bot... | \n",
+ " good first issue,solidity,foundry | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 4 | \n",
- " 106 | \n",
- " Implement JWT for our profile api | \n",
+ " 142 | \n",
+ " Add contributor leaderboard | \n",
" open | \n",
- " 2025-10-09T13:18:18Z | \n",
- " 2025-10-27T08:48:29Z | \n",
+ " 2025-11-24T15:04:07Z | \n",
+ " 2025-11-24T15:04:42Z | \n",
" joelamouche | \n",
- " tusharshah21 | \n",
- " front end,rust,back-end,react,typescript,hackt... | \n",
+ " | \n",
+ " good first issue,front end,react,typescript,80pts | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 5 | \n",
- " 105 | \n",
- " Add backend endpoints to fetch attestations | \n",
+ " 141 | \n",
+ " Add a new leaderboard page, with link in the s... | \n",
" open | \n",
- " 2025-10-09T12:31:06Z | \n",
- " 2025-10-09T12:31:06Z | \n",
+ " 2025-11-24T15:01:00Z | \n",
+ " 2025-11-24T15:01:00Z | \n",
" joelamouche | \n",
" | \n",
- " rust,back-end,db | \n",
+ " good first issue,front end,react,typescript,20... | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 6 | \n",
- " 104 | \n",
- " Blockchain Indexer - Get blockchain data from ... | \n",
+ " 140 | \n",
+ " Create top badge owner leaderboard | \n",
" open | \n",
- " 2025-10-09T12:20:02Z | \n",
- " 2025-10-09T12:33:22Z | \n",
+ " 2025-11-24T14:59:35Z | \n",
+ " 2025-11-24T15:02:16Z | \n",
" joelamouche | \n",
- " oscarwroche | \n",
- " enhancement,planning | \n",
+ " | \n",
+ " good first issue,front end,react,typescript,40pts | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 7 | \n",
- " 103 | \n",
- " Improve auth logic for API | \n",
+ " 138 | \n",
+ " Public contributor leaderboard | \n",
" open | \n",
- " 2025-10-09T12:14:41Z | \n",
- " 2025-10-14T08:05:29Z | \n",
+ " 2025-11-17T13:58:58Z | \n",
+ " 2025-11-24T15:04:19Z | \n",
+ " joelamouche | \n",
" joelamouche | \n",
- " oscarwroche | \n",
- " enhancement,planning | \n",
+ " enhancement,front end,planning | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 8 | \n",
- " 102 | \n",
- " Add twitter handle to profiles in the backend | \n",
+ " 137 | \n",
+ " Figure out sharing new badges on X | \n",
" open | \n",
- " 2025-10-08T13:18:45Z | \n",
- " 2025-10-27T08:43:18Z | \n",
+ " 2025-11-17T13:58:23Z | \n",
+ " 2025-11-17T13:58:28Z | \n",
" joelamouche | \n",
- " ayushhh101 | \n",
- " good first issue,rust,back-end,db,hacktoberfest | \n",
+ " joelamouche | \n",
+ " planning | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
" | 9 | \n",
- " 101 | \n",
- " Add twitter account to profiles | \n",
+ " 135 | \n",
+ " Add projects to the Backend | \n",
" open | \n",
- " 2025-10-08T13:16:50Z | \n",
- " 2025-10-16T13:09:30Z | \n",
+ " 2025-11-17T12:38:30Z | \n",
+ " 2025-11-27T12:15:32Z | \n",
" joelamouche | \n",
- " | \n",
- " enhancement,planning | \n",
+ " pheobeayo | \n",
+ " good first issue,rust,back-end,160pts,db,hackt... | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
@@ -193,40 +193,40 @@
],
"text/plain": [
" number title state \\\n",
- "0 126 Use jupyter notebook to analyze github issues open \n",
- "1 125 Improve UX and design of theguild.dev open \n",
- "2 118 Badge enhancements open \n",
- "3 110 Discord bot: relay git activity on a discord c... open \n",
- "4 106 Implement JWT for our profile api open \n",
- "5 105 Add backend endpoints to fetch attestations open \n",
- "6 104 Blockchain Indexer - Get blockchain data from ... open \n",
- "7 103 Improve auth logic for API open \n",
- "8 102 Add twitter handle to profiles in the backend open \n",
- "9 101 Add twitter account to profiles open \n",
+ "0 146 Figure out how to admin delete profile open \n",
+ "1 145 I think we should allow duplicate attestations open \n",
+ "2 144 Implement Upgradable pattern for TheGuildAttes... open \n",
+ "3 143 TheGuildAttestationResolver should have admin ... open \n",
+ "4 142 Add contributor leaderboard open \n",
+ "5 141 Add a new leaderboard page, with link in the s... open \n",
+ "6 140 Create top badge owner leaderboard open \n",
+ "7 138 Public contributor leaderboard open \n",
+ "8 137 Figure out sharing new badges on X open \n",
+ "9 135 Add projects to the Backend open \n",
"\n",
- " created_at updated_at user assignees \\\n",
- "0 2025-10-30T13:17:36Z 2025-10-30T13:17:36Z joelamouche joelamouche \n",
- "1 2025-10-28T11:23:19Z 2025-10-28T11:23:19Z joelamouche \n",
- "2 2025-10-17T08:35:11Z 2025-10-30T13:44:59Z joelamouche joelamouche \n",
- "3 2025-10-10T09:57:28Z 2025-10-30T09:00:26Z joelamouche \n",
- "4 2025-10-09T13:18:18Z 2025-10-27T08:48:29Z joelamouche tusharshah21 \n",
- "5 2025-10-09T12:31:06Z 2025-10-09T12:31:06Z joelamouche \n",
- "6 2025-10-09T12:20:02Z 2025-10-09T12:33:22Z joelamouche oscarwroche \n",
- "7 2025-10-09T12:14:41Z 2025-10-14T08:05:29Z joelamouche oscarwroche \n",
- "8 2025-10-08T13:18:45Z 2025-10-27T08:43:18Z joelamouche ayushhh101 \n",
- "9 2025-10-08T13:16:50Z 2025-10-16T13:09:30Z joelamouche \n",
+ " created_at updated_at user assignees \\\n",
+ "0 2025-12-03T17:26:37Z 2025-12-03T17:26:37Z joelamouche \n",
+ "1 2025-12-03T15:18:42Z 2025-12-03T15:18:42Z joelamouche \n",
+ "2 2025-12-03T10:28:34Z 2025-12-03T10:28:55Z joelamouche \n",
+ "3 2025-12-03T10:27:07Z 2025-12-03T10:27:51Z joelamouche \n",
+ "4 2025-11-24T15:04:07Z 2025-11-24T15:04:42Z joelamouche \n",
+ "5 2025-11-24T15:01:00Z 2025-11-24T15:01:00Z joelamouche \n",
+ "6 2025-11-24T14:59:35Z 2025-11-24T15:02:16Z joelamouche \n",
+ "7 2025-11-17T13:58:58Z 2025-11-24T15:04:19Z joelamouche joelamouche \n",
+ "8 2025-11-17T13:58:23Z 2025-11-17T13:58:28Z joelamouche joelamouche \n",
+ "9 2025-11-17T12:38:30Z 2025-11-27T12:15:32Z joelamouche pheobeayo \n",
"\n",
" labels \\\n",
- "0 jupyter-notebook,python \n",
- "1 good first issue,ux,design,hacktoberfest \n",
- "2 enhancement,planning,hacktoberfest \n",
- "3 good first issue,nodejs,typescript,discord-bot... \n",
- "4 front end,rust,back-end,react,typescript,hackt... \n",
- "5 rust,back-end,db \n",
- "6 enhancement,planning \n",
- "7 enhancement,planning \n",
- "8 good first issue,rust,back-end,db,hacktoberfest \n",
- "9 enhancement,planning \n",
+ "0 good first issue,back-end,planning \n",
+ "1 solidity,planning \n",
+ "2 good first issue,solidity,foundry \n",
+ "3 good first issue,solidity,foundry \n",
+ "4 good first issue,front end,react,typescript,80pts \n",
+ "5 good first issue,front end,react,typescript,20... \n",
+ "6 good first issue,front end,react,typescript,40pts \n",
+ "7 enhancement,front end,planning \n",
+ "8 planning \n",
+ "9 good first issue,rust,back-end,160pts,db,hackt... \n",
"\n",
" url \n",
"0 https://github.com/TheSoftwareDevGuild/TheGuil... \n",
@@ -241,7 +241,7 @@
"9 https://github.com/TheSoftwareDevGuild/TheGuil... "
]
},
- "execution_count": 3,
+ "execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
@@ -330,7 +330,7 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 20,
"metadata": {},
"outputs": [],
"source": [
@@ -347,7 +347,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 21,
"metadata": {},
"outputs": [
{
@@ -356,9 +356,9 @@
"text": [
"{\n",
" \"contributors\": 4,\n",
- " \"tags\": 26,\n",
- " \"total_contributions\": 940,\n",
- " \"number_of_closed_tickets\": 34\n",
+ " \"tags\": 28,\n",
+ " \"total_contribution_tokens\": 1040,\n",
+ " \"number_of_closed_tickets\": 42\n",
"}\n"
]
}
@@ -415,6 +415,408 @@
"print(json.dumps(stats, indent=4))\n",
" "
]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Generate Attestations from Issues\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### fetch all issues"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Fetched 42 total issues (closed)\n"
+ ]
+ }
+ ],
+ "source": [
+ "import json\n",
+ "import re\n",
+ "from datetime import datetime\n",
+ "from collections import defaultdict\n",
+ "\n",
+ "# Fetch all issues (only closed) to get complete picture\n",
+ "all_issues = fetch_issues(state=\"closed\", per_page=100, max_pages=10)\n",
+ "print(f\"Fetched {len(all_issues)} total issues (closed)\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Extract Labels"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "============================================================\n",
+ "BADGES/LABELS THAT NEED TO BE CREATED (23):\n",
+ "============================================================\n",
+ " - back-end\n",
+ " - blockchain\n",
+ " - bug\n",
+ " - db\n",
+ " - design\n",
+ " - discord-bot\n",
+ " - documentation\n",
+ " - foundry\n",
+ " - front end\n",
+ " - good first issue\n",
+ " - hacktoberfest\n",
+ " - in progress\n",
+ " - jupyter-notebook\n",
+ " - nodejs\n",
+ " - onlydust-wave\n",
+ " - planning\n",
+ " - python\n",
+ " - react\n",
+ " - rust\n",
+ " - solidity\n",
+ " - typescript\n",
+ " - ux\n",
+ " - wagmi\n",
+ "============================================================\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Extract unique labels/badges from all issues\n",
+ "all_labels = set()\n",
+ "for issue in all_issues:\n",
+ " for label in issue.get(\"labels\", []):\n",
+ " label_name = label.get(\"name\", \"\")\n",
+ " # Filter out point labels (e.g., \"10pts\", \"5pts\") as they're not badges\n",
+ " if label_name and not re.match(r'^\\d+pts$', label_name):\n",
+ " all_labels.add(label_name)\n",
+ "\n",
+ "badges_to_create = sorted(list(all_labels))\n",
+ "print(f\"\\n{'='*60}\")\n",
+ "print(f\"BADGES/LABELS THAT NEED TO BE CREATED ({len(badges_to_create)}):\")\n",
+ "print(f\"{'='*60}\")\n",
+ "for badge in badges_to_create:\n",
+ " print(f\" - {badge}\")\n",
+ "print(f\"{'='*60}\\n\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "I'd rather dirrectly process this list into ai chat manually"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Non skill labels to skip"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "non_skill_badges = [\n",
+ " \"good first issue\",\n",
+ " \"hacktoberfest\",\n",
+ " \"in progress\",\n",
+ " \"bug\",\n",
+ " \"onlydust-wave\",\n",
+ " \"planning\",\n",
+ "]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### git_label_to_badge"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "git_label_to_badge = {\n",
+ " \"rust\": \"Rust\",\n",
+ " \"solidity\": \"Solidity\",\n",
+ " \"typescript\": \"TypeScript\",\n",
+ " \"python\": \"Python\",\n",
+ " \"apis\": \"APIs\",\n",
+ "\n",
+ " \"back-end\": \"Back-end\",\n",
+ " \"backend\": \"Back-end\",\n",
+ "\n",
+ " \"blockchain\": \"Blockchain\",\n",
+ "\n",
+ " \"db\": \"DB\",\n",
+ " \"database\": \"DB\",\n",
+ " \"databases\": \"DB\",\n",
+ "\n",
+ " \"design\": \"Design\",\n",
+ "\n",
+ " \"discord-bot\": \"Discord-bot\",\n",
+ " \"discord bot\": \"Discord-bot\",\n",
+ "\n",
+ " \"documentation\": \"Documentation\",\n",
+ " \"docs\": \"Documentation\",\n",
+ "\n",
+ " \"foundry\": \"Foundry\",\n",
+ "\n",
+ " \"front end\": \"Front end\",\n",
+ " \"frontend\": \"Front end\",\n",
+ " \"front-end\": \"Front end\",\n",
+ "\n",
+ " \"jupyter-notebook\": \"Jupyter-notebook\",\n",
+ " \"jupyter notebook\": \"Jupyter-notebook\",\n",
+ "\n",
+ " \"nodejs\": \"Nodejs\",\n",
+ " \"node.js\": \"Nodejs\",\n",
+ " \"node\": \"Nodejs\",\n",
+ "\n",
+ " \"onlydust-wave\": \"Onlydust-wave\",\n",
+ "\n",
+ " \"planning\": \"Planning\",\n",
+ "\n",
+ " \"react\": \"React\",\n",
+ "\n",
+ " \"ux\": \"UX\",\n",
+ " \"user-experience\": \"UX\",\n",
+ "\n",
+ " \"wagmi\": \"Wagmi\"\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Get github handles from API"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "https://theguild-backend-e2df290d177e.herokuapp.com/\n",
+ "Loaded 4 GitHub username -> address mappings from backend API\n",
+ "\n",
+ "Warning: 4 assignees without address mapping:\n",
+ " - oscarwroche\n",
+ " - peteroche\n",
+ " - rainwaters11\n",
+ " - yash-1104github\n",
+ "These will be skipped in attestations.\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Optionally fetch profiles from backend API to map GitHub usernames to addresses\n",
+ "BACKEND_API_URL = os.getenv(\"BACKEND_API_URL\", \"http://localhost:3000\")\n",
+ "print(BACKEND_API_URL)\n",
+ "github_to_address = {}\n",
+ "\n",
+ "try:\n",
+ " profiles_resp = requests.get(f\"{BACKEND_API_URL}/profiles\", timeout=5)\n",
+ " if profiles_resp.status_code == 200:\n",
+ " profiles = profiles_resp.json()\n",
+ " for profile in profiles:\n",
+ " if profile.get(\"github_login\"):\n",
+ " github_to_address[profile[\"github_login\"].lower()] = profile[\"address\"]\n",
+ " print(f\"Loaded {len(github_to_address)} GitHub username -> address mappings from backend API\")\n",
+ " else:\n",
+ " print(f\"Backend API not available (status {profiles_resp.status_code}), will need manual address mapping\")\n",
+ "except Exception as e:\n",
+ " print(f\"Could not fetch profiles from backend API ({e}), will need manual address mapping\")\n",
+ "\n",
+ "# Show unmapped assignees\n",
+ "all_assignees = set()\n",
+ "for issue in all_issues:\n",
+ " for assignee in issue.get(\"assignees\", []):\n",
+ " username = assignee.get(\"login\", \"\").lower()\n",
+ " if username:\n",
+ " all_assignees.add(username)\n",
+ "\n",
+ "unmapped = [u for u in all_assignees if u not in github_to_address]\n",
+ "if unmapped:\n",
+ " print(f\"\\nWarning: {len(unmapped)} assignees without address mapping:\")\n",
+ " for u in sorted(unmapped):\n",
+ " print(f\" - {u}\")\n",
+ " print(\"These will be skipped in attestations.\\n\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Generate all attestations"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Generated 55 attestations from 42 issues\n",
+ "Unique recipients: 4\n",
+ "Unique badges: 15\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Generate attestations from issues\n",
+ "# For each issue with assignees and labels, create attestations\n",
+ "\n",
+ "attestations = []\n",
+ "\n",
+ "# Ensure non_skill_badges is a lowercased set for quick lookup\n",
+ "_non_skill_badges_set = set(b.lower() for b in non_skill_badges)\n",
+ "\n",
+ "for issue in all_issues:\n",
+ " assignees = issue.get(\"assignees\", [])\n",
+ " labels = issue.get(\"labels\", [])\n",
+ " issue_number = issue.get(\"number\")\n",
+ " issue_title = issue.get(\"title\", \"\")\n",
+ " issue_url = issue.get(\"html_url\", \"\")\n",
+ "\n",
+ " # Skip if no assignees or no labels\n",
+ " if not assignees or not labels:\n",
+ " continue\n",
+ "\n",
+ " for assignee in assignees:\n",
+ " github_username = assignee.get(\"login\", \"\").lower()\n",
+ " if not github_username:\n",
+ " continue\n",
+ "\n",
+ " recipient_address = github_to_address.get(github_username)\n",
+ " if not recipient_address:\n",
+ " continue # Skip if no address mapping\n",
+ "\n",
+ " for label in labels:\n",
+ " label_name = label.get(\"name\", \"\")\n",
+ " # Skip point labels\n",
+ " if not label_name or re.match(r'^\\d+pts$', label_name):\n",
+ " continue\n",
+ " # Skip if label is in non_skill_badges (case-insensitive)\n",
+ " if label_name.lower() in _non_skill_badges_set:\n",
+ " continue\n",
+ " # Map label to badge, skip if mapping doesn't exist\n",
+ " badge_name = git_label_to_badge.get(label_name.lower())\n",
+ " if not badge_name:\n",
+ " continue\n",
+ "\n",
+ " justification = f\"Awarded for contributions to issue #{issue_number}: {issue_title}\"\n",
+ " if issue_url:\n",
+ " justification += f\" ({issue_url})\"\n",
+ "\n",
+ " attestations.append({\n",
+ " \"recipient\": recipient_address,\n",
+ " \"badgeName\": badge_name,\n",
+ " \"justification\": justification\n",
+ " })\n",
+ "\n",
+ "print(f\"Generated {len(attestations)} attestations from {len(all_issues)} issues\")\n",
+ "print(f\"Unique recipients: {len(set(a['recipient'] for a in attestations))}\")\n",
+ "print(f\"Unique badges: {len(set(a['badgeName'] for a in attestations))}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Save attestations to file"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "ā Saved 55 attestations to: ../the-guild-smart-contracts/attestations-2025-12-03.json\n",
+ " File size: 15808 bytes\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Save attestations to date-based file and also to attestations-latest.json\n",
+ "from pathlib import Path\n",
+ "\n",
+ "output_dir = Path(\"../the-guild-smart-contracts\")\n",
+ "output_dir.mkdir(parents=True, exist_ok=True)\n",
+ "\n",
+ "# Create filename with current date\n",
+ "date_str = datetime.now().strftime(\"%Y-%m-%d\")\n",
+ "output_file = output_dir / f\"attestations-{date_str}.json\"\n",
+ "\n",
+ "output_data = {\n",
+ " \"attestations\": attestations\n",
+ "}\n",
+ "\n",
+ "with open(output_file, \"w\") as f:\n",
+ " json.dump(output_data, f, indent=2)\n",
+ "\n",
+ "# Save latest attestations\n",
+ "latest_file = output_dir / \"attestations-latest.json\"\n",
+ "with open(latest_file, \"w\") as f:\n",
+ " json.dump(output_data, f, indent=2)\n",
+ "\n",
+ "print(f\"\\nā Saved {len(attestations)} attestations to: {output_file}\")\n",
+ "print(f\" File size: {output_file.stat().st_size} bytes\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally, run an ai prompt over the file to refactor duplicate attestations: \"Here we cannot have duplicate recipient/badgeName pairs. When it is the case, just condensate the multiple justifications into one justification for one badge.\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": []
}
],
"metadata": {
diff --git a/notebooks/env.example b/notebooks/env.example
index 243a69c..eca5436 100644
--- a/notebooks/env.example
+++ b/notebooks/env.example
@@ -1,4 +1,6 @@
GITHUB_TOKEN=
GITHUB_OWNER=
GITHUB_REPO=
-GITHUB_API_URL=
\ No newline at end of file
+GITHUB_API_URL=
+
+BACKEND_API_URL=
\ No newline at end of file
diff --git a/the-guild-smart-contracts/.env.example b/the-guild-smart-contracts/.env.example
index 393a5c3..ce15e2b 100644
--- a/the-guild-smart-contracts/.env.example
+++ b/the-guild-smart-contracts/.env.example
@@ -8,6 +8,8 @@ CREATE2_SALT=1
# Generic fallback for any other network (used if chain isn't matched)
EAS_ADDRESS=
+BADGE_REGISTRY_ADDRESS=
+
# RPC URLS
AMOY_RPC_URL=https://polygon-amoy.drpc.org
BASE_SEPOLIA_URL=https://base-sepolia.therpc.io
diff --git a/the-guild-smart-contracts/README.md b/the-guild-smart-contracts/README.md
index a238d9f..7071a21 100644
--- a/the-guild-smart-contracts/README.md
+++ b/the-guild-smart-contracts/README.md
@@ -203,9 +203,87 @@ forge script script/TheGuildBadgeRanking.s.sol:TheGuildBadgeRankingScript \
--broadcast
```
+### Batch Badge Creation
+
+The `CreateBadgesFromJson.s.sol` script allows batch creation of badges from JSON data.
+
+#### JSON Format
+
+Prepare your badges data in JSON format:
+```json
+{
+ "badges": [
+ {
+ "name": "Rust",
+ "description": "Know how to code in Rust"
+ },
+ {
+ "name": "Solidity",
+ "description": "Know how to code in Solidity"
+ }
+ ]
+}
+```
+
+- `name`: Name of the badge (max 32 characters, will be converted to bytes32)
+- `description`: Description of the badge (max 32 characters, will be converted to bytes32)
+
+#### Usage
+
+```shell
+# Using the helper script (recommended)
+export PRIVATE_KEY=your_private_key
+export RPC_URL=https://polygon-amoy.drpc.org
+export BADGE_REGISTRY_ADDRESS=0x8ac95734e778322684f1d318fb7633777baa8427
+
+# Dry run first (uses badges-latest.json by default)
+./run_batch_badges.sh true
+
+# Dry run with custom JSON file
+./run_batch_badges.sh badges.json true
+
+# Production run (uses badges-latest.json by default)
+./run_batch_badges.sh false
+
+# Production run with custom JSON file
+./run_batch_badges.sh badges.json false
+
+# Manual approach
+# Set environment variables
+export PRIVATE_KEY=your_private_key
+export JSON_PATH=./badges.json
+export BADGE_REGISTRY_ADDRESS=0x8ac95734e778322684f1d318fb7633777baa8427
+
+# Dry run (recommended first)
+export DRY_RUN=true
+forge script script/CreateBadgesFromJson.s.sol:CreateBadgesFromJson \
+ --rpc-url