diff --git a/notebooks/01_github_issues.ipynb b/notebooks/01_github_issues.ipynb
index e6db440..d90c1e6 100644
--- a/notebooks/01_github_issues.ipynb
+++ b/notebooks/01_github_issues.ipynb
@@ -24,14 +24,14 @@
},
{
"cell_type": "code",
- "execution_count": 19,
+ "execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "Fetched 47 issues from TheSoftwareDevGuild/TheGuildGenesis\n"
+ "Fetched 49 issues from TheSoftwareDevGuild/TheGuildGenesis\n"
]
},
{
@@ -69,18 +69,54 @@
"
\n",
" \n",
" | 0 | \n",
+ " 154 | \n",
+ " Description field of badges should be bytes no... | \n",
+ " open | \n",
+ " 2025-12-14T10:45:06Z | \n",
+ " 2025-12-15T16:57:10Z | \n",
+ " joelamouche | \n",
+ " V-Vaal | \n",
+ " good first issue,solidity,40pts,foundry,web3 | \n",
+ " https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 153 | \n",
+ " Allow batch attestation creation in the front end | \n",
+ " open | \n",
+ " 2025-12-14T10:39:31Z | \n",
+ " 2025-12-14T10:39:31Z | \n",
+ " joelamouche | \n",
+ " | \n",
+ " good first issue,front end,react,ux,typescript | \n",
+ " https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " 150 | \n",
+ " Implement JWT - Front end flow | \n",
+ " open | \n",
+ " 2025-12-08T10:36:57Z | \n",
+ " 2025-12-08T12:40:22Z | \n",
+ " joelamouche | \n",
+ " tusharshah21 | \n",
+ " front end,react,typescript | \n",
+ " https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
" 146 | \n",
" Figure out how to admin delete profile | \n",
" open | \n",
" 2025-12-03T17:26:37Z | \n",
- " 2025-12-03T17:26:37Z | \n",
+ " 2025-12-07T06:52:27Z | \n",
" joelamouche | \n",
" | \n",
" good first issue,back-end,planning | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
- " | 1 | \n",
+ " 4 | \n",
" 145 | \n",
" I think we should allow duplicate attestations | \n",
" open | \n",
@@ -92,7 +128,7 @@
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
- " | 2 | \n",
+ " 5 | \n",
" 144 | \n",
" Implement Upgradable pattern for TheGuildAttes... | \n",
" open | \n",
@@ -104,7 +140,7 @@
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
- " | 3 | \n",
+ " 6 | \n",
" 143 | \n",
" TheGuildAttestationResolver should have admin ... | \n",
" open | \n",
@@ -116,31 +152,31 @@
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
- " | 4 | \n",
+ " 7 | \n",
" 142 | \n",
" Add contributor leaderboard | \n",
" open | \n",
" 2025-11-24T15:04:07Z | \n",
- " 2025-11-24T15:04:42Z | \n",
+ " 2025-12-13T17:03:24Z | \n",
" joelamouche | \n",
- " | \n",
+ " adityagupta0251 | \n",
" good first issue,front end,react,typescript,80pts | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
- " | 5 | \n",
+ " 8 | \n",
" 141 | \n",
" Add a new leaderboard page, with link in the s... | \n",
" open | \n",
" 2025-11-24T15:01:00Z | \n",
- " 2025-11-24T15:01:00Z | \n",
+ " 2025-12-14T10:34:19Z | \n",
" joelamouche | \n",
- " | \n",
+ " adityagupta0251 | \n",
" good first issue,front end,react,typescript,20... | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
" \n",
- " | 6 | \n",
+ " 9 | \n",
" 140 | \n",
" Create top badge owner leaderboard | \n",
" open | \n",
@@ -151,82 +187,46 @@
" good first issue,front end,react,typescript,40pts | \n",
" https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
"
\n",
- " \n",
- " | 7 | \n",
- " 138 | \n",
- " Public contributor leaderboard | \n",
- " open | \n",
- " 2025-11-17T13:58:58Z | \n",
- " 2025-11-24T15:04:19Z | \n",
- " joelamouche | \n",
- " joelamouche | \n",
- " enhancement,front end,planning | \n",
- " https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
- "
\n",
- " \n",
- " | 8 | \n",
- " 137 | \n",
- " Figure out sharing new badges on X | \n",
- " open | \n",
- " 2025-11-17T13:58:23Z | \n",
- " 2025-11-17T13:58:28Z | \n",
- " joelamouche | \n",
- " joelamouche | \n",
- " planning | \n",
- " https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
- "
\n",
- " \n",
- " | 9 | \n",
- " 135 | \n",
- " Add projects to the Backend | \n",
- " open | \n",
- " 2025-11-17T12:38:30Z | \n",
- " 2025-11-27T12:15:32Z | \n",
- " joelamouche | \n",
- " pheobeayo | \n",
- " good first issue,rust,back-end,160pts,db,hackt... | \n",
- " https://github.com/TheSoftwareDevGuild/TheGuil... | \n",
- "
\n",
" \n",
"\n",
""
],
"text/plain": [
" number title state \\\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",
+ "0 154 Description field of badges should be bytes no... open \n",
+ "1 153 Allow batch attestation creation in the front end open \n",
+ "2 150 Implement JWT - Front end flow open \n",
+ "3 146 Figure out how to admin delete profile open \n",
+ "4 145 I think we should allow duplicate attestations open \n",
+ "5 144 Implement Upgradable pattern for TheGuildAttes... open \n",
+ "6 143 TheGuildAttestationResolver should have admin ... open \n",
+ "7 142 Add contributor leaderboard open \n",
+ "8 141 Add a new leaderboard page, with link in the s... open \n",
+ "9 140 Create top badge owner leaderboard open \n",
"\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",
+ " created_at updated_at user assignees \\\n",
+ "0 2025-12-14T10:45:06Z 2025-12-15T16:57:10Z joelamouche V-Vaal \n",
+ "1 2025-12-14T10:39:31Z 2025-12-14T10:39:31Z joelamouche \n",
+ "2 2025-12-08T10:36:57Z 2025-12-08T12:40:22Z joelamouche tusharshah21 \n",
+ "3 2025-12-03T17:26:37Z 2025-12-07T06:52:27Z joelamouche \n",
+ "4 2025-12-03T15:18:42Z 2025-12-03T15:18:42Z joelamouche \n",
+ "5 2025-12-03T10:28:34Z 2025-12-03T10:28:55Z joelamouche \n",
+ "6 2025-12-03T10:27:07Z 2025-12-03T10:27:51Z joelamouche \n",
+ "7 2025-11-24T15:04:07Z 2025-12-13T17:03:24Z joelamouche adityagupta0251 \n",
+ "8 2025-11-24T15:01:00Z 2025-12-14T10:34:19Z joelamouche adityagupta0251 \n",
+ "9 2025-11-24T14:59:35Z 2025-11-24T15:02:16Z joelamouche \n",
"\n",
" labels \\\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",
+ "0 good first issue,solidity,40pts,foundry,web3 \n",
+ "1 good first issue,front end,react,ux,typescript \n",
+ "2 front end,react,typescript \n",
+ "3 good first issue,back-end,planning \n",
+ "4 solidity,planning \n",
+ "5 good first issue,solidity,foundry \n",
+ "6 good first issue,solidity,foundry \n",
+ "7 good first issue,front end,react,typescript,80pts \n",
+ "8 good first issue,front end,react,typescript,20... \n",
+ "9 good first issue,front end,react,typescript,40pts \n",
"\n",
" url \n",
"0 https://github.com/TheSoftwareDevGuild/TheGuil... \n",
@@ -241,7 +241,7 @@
"9 https://github.com/TheSoftwareDevGuild/TheGuil... "
]
},
- "execution_count": 19,
+ "execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
@@ -817,6 +817,229 @@
"cell_type": "markdown",
"metadata": {},
"source": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Generate contribution tokens from issues"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Fetch closed tickets"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Fetched 44 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 contribution labels"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "============================================================\n",
+ "ISSUE-BASED GITHUB CONTRIBUTION TOKENS (13):\n",
+ "============================================================\n",
+ "- tusharshah21: 40pts -- \"Github handle errors\"\n",
+ "- tusharshah21: 40pts -- \"Implement SIWE for the front end\"\n",
+ "- pheobeayo: 80pts -- \"Feature: add a github handle to profile in Front end\"\n",
+ "- pheobeayo: 40pts -- \"Add doc for indexer\"\n",
+ "- PeterOche: 80pts -- \"Discord Bot foundation\"\n",
+ "- rainwaters11: 80pts -- \"Add GitHub handles to backend database (associated with profiles)\"\n",
+ "- joelamouche: 160pts -- \"Add a backend for the contribution points (api, db)\"\n",
+ "- Teddy1792: 20pts -- \"Improve landing page design with animated background\"\n",
+ "- pheobeayo: 40pts -- \"On the profile, there should be a section with the badges issued by that profile\"\n",
+ "- tusharshah21: 20pts -- \"Add copy to clipboard icons next to all ethereum addresses\"\n",
+ "- tusharshah21: 40pts -- \"Add description on the profile page\"\n",
+ "- tusharshah21: 320pts -- \"Improve SIWE logic to use dynamic, user-specific nonce - backend\"\n",
+ "- yash-1104github: 80pts -- \"Improve loading UX for lists\"\n",
+ "============================================================\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Extract contribution labels that match the pattern \"xpts\"\n",
+ "# Extract contribution labels that match the pattern \"xpts\"\n",
+ "contributions = []\n",
+ "\n",
+ "for issue in all_issues:\n",
+ " issue_title = issue.get(\"title\", \"\")\n",
+ " for label in issue.get(\"labels\", []):\n",
+ " label_name = label.get(\"name\", \"\")\n",
+ " # Match labels like \"10pts\", \"5pts\", etc.\n",
+ " points_match = re.match(r\"^(\\d+)pts$\", label_name)\n",
+ " if points_match:\n",
+ " amount = int(points_match.group(1))\n",
+ " user = None\n",
+ " # Try to get GitHub handle from assignee or user\n",
+ " if issue.get(\"assignee\"):\n",
+ " user = issue[\"assignee\"].get(\"login\", \"\")\n",
+ " else:\n",
+ " # fallback: creator of the issue\n",
+ " user = issue.get(\"user\", {}).get(\"login\", \"\")\n",
+ " justification = issue_title\n",
+ " if user:\n",
+ " contributions.append({\n",
+ " \"amount\": amount,\n",
+ " \"githandle\": user,\n",
+ " \"justification\": justification,\n",
+ " })\n",
+ "\n",
+ "print(f\"\\n{'='*60}\")\n",
+ "print(f\"ISSUE-BASED GITHUB CONTRIBUTION TOKENS ({len(contributions)}):\")\n",
+ "print(f\"{'='*60}\")\n",
+ "for c in contributions:\n",
+ " print(f\"- {c['githandle']}: {c['amount']}pts -- \\\"{c['justification']}\\\"\")\n",
+ "print(f\"{'='*60}\\n\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### get github handles from API"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "https://theguild-backend-e2df290d177e.herokuapp.com/\n",
+ "Loaded 6 GitHub username -> address mappings from backend API\n",
+ "\n",
+ "Warning: 3 assignees without address mapping:\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 contributions"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Wrote 10 mints to contribution-tokens-latest.json\n"
+ ]
+ }
+ ],
+ "source": [
+ "import json\n",
+ "\n",
+ "# Prepare mints list in expected JSON format\n",
+ "mints = []\n",
+ "for c in contributions:\n",
+ " github_login = c['githandle'].lower()\n",
+ " address = github_to_address.get(github_login)\n",
+ " if not address:\n",
+ " continue # skip if we don't have an address\n",
+ " amount = c['amount']\n",
+ " justification = c['justification']\n",
+ " # Prepare reason as bytes and convert to hex string (\"0x...\")\n",
+ " # Handles possible utf-8 and encodes to hex\n",
+ " reason_bytes = justification.encode(\"utf-8\")\n",
+ " reason_hex = \"0x\" + reason_bytes.hex()\n",
+ " mints.append({\n",
+ " \"recipient\": address,\n",
+ " \"amount\": int(amount),\n",
+ " \"reason\": reason_hex,\n",
+ " })\n",
+ "\n",
+ "out_path = \"contribution-tokens-latest.json\"\n",
+ "with open(out_path, \"w\") as f:\n",
+ " json.dump({\"mints\": mints}, f, indent=2)\n",
+ "print(f\"Wrote {len(mints)} mints to {out_path}\")\n"
+ ]
}
],
"metadata": {
diff --git a/scripts/test_projects.sh b/scripts/test_projects.sh
new file mode 100755
index 0000000..7b20984
--- /dev/null
+++ b/scripts/test_projects.sh
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Test project API endpoints:
+# - List projects (public)
+# - Create a project (protected) using signature headers (no JWT)
+# Requirements: curl, node, npm. Installs ethers locally into /tmp by default.
+#
+# Inputs (env):
+# PUBLIC_ADDRESS (required) - wallet address
+# PRIVATE_KEY (required) - wallet private key (0x-prefixed)
+# API_URL (optional) - defaults to http://localhost:3001
+# PROJECT_NAME (optional) - defaults to "Shell Project"
+# PROJECT_DESC (optional) - defaults to "Created via script"
+# PROJECT_STATUS (optional) - defaults to "proposal" (proposal|ongoing|rejected)
+
+API_URL="${API_URL:-http://localhost:3001}"
+ADDRESS="${PUBLIC_ADDRESS:-}"
+PRIVATE_KEY="${PRIVATE_KEY:-}"
+
+# If not provided via env, prompt interactively (input hidden for private key).
+if [[ -z "${ADDRESS}" ]]; then
+ read -r -p "Enter PUBLIC_ADDRESS (0x...): " ADDRESS
+fi
+if [[ -z "${PRIVATE_KEY}" ]]; then
+ read -r -s -p "Enter PRIVATE_KEY (0x..., hidden): " PRIVATE_KEY
+ echo
+fi
+if [[ -z "${ADDRESS}" || -z "${PRIVATE_KEY}" ]]; then
+ echo "PUBLIC_ADDRESS and PRIVATE_KEY are required. Aborting."
+ exit 1
+fi
+
+PROJECT_NAME="${PROJECT_NAME:-Shell Project}"
+PROJECT_DESC="${PROJECT_DESC:-Created via script}"
+PROJECT_STATUS="${PROJECT_STATUS:-proposal}"
+
+# Ensure we have ethers available without polluting the repo.
+TOOLS_DIR="${TOOLS_DIR:-/tmp/theguildgenesis-login}"
+export NODE_PATH="${TOOLS_DIR}/node_modules${NODE_PATH:+:${NODE_PATH}}"
+export PATH="${TOOLS_DIR}/node_modules/.bin:${PATH}"
+if ! node -e "require('ethers')" >/dev/null 2>&1; then
+ echo "Installing ethers@6 to ${TOOLS_DIR}..."
+ mkdir -p "${TOOLS_DIR}"
+ npm install --prefix "${TOOLS_DIR}" ethers@6 >/dev/null
+fi
+
+echo "Fetching nonce for ${ADDRESS}..."
+nonce_resp="$(curl -sS "${API_URL}/auth/nonce/${ADDRESS}")"
+echo "Nonce response: ${nonce_resp}"
+# Parse nonce safely
+nonce="$(RESP="${nonce_resp}" python3 - <<'PY'
+import json, os
+data = json.loads(os.environ["RESP"])
+print(data["nonce"])
+PY
+)"
+if [[ -z "${nonce}" ]]; then
+ echo "Failed to parse nonce from response"
+ exit 1
+fi
+
+message=$'Sign this message to authenticate with The Guild.\n\nNonce: '"${nonce}"
+
+echo "Signing nonce..."
+signature="$(
+ ADDRESS="${ADDRESS}" PRIVATE_KEY="${PRIVATE_KEY}" MESSAGE="${message}" \
+ node - <<'NODE'
+const { Wallet } = require('ethers');
+
+const address = process.env.ADDRESS;
+const pk = process.env.PRIVATE_KEY;
+const message = process.env.MESSAGE;
+
+if (!address || !pk || !message) {
+ console.error("Missing ADDRESS, PRIVATE_KEY or MESSAGE");
+ process.exit(1);
+}
+
+const wallet = new Wallet(pk);
+if (wallet.address.toLowerCase() !== address.toLowerCase()) {
+ console.error(`Private key does not match address. Wallet: ${wallet.address}, Provided: ${address}`);
+ process.exit(1);
+}
+
+(async () => {
+ const sig = await wallet.signMessage(message);
+ console.log(sig);
+})();
+NODE
+)"
+
+echo "Signature: ${signature}"
+
+echo "Listing projects (public)..."
+list_tmp="$(mktemp)"
+list_status="$(curl -sS -o "${list_tmp}" -w "%{http_code}" \
+ "${API_URL}/projects")"
+list_resp="$(cat "${list_tmp}")"
+rm -f "${list_tmp}"
+echo "List HTTP ${list_status}: ${list_resp}"
+if [[ "${list_status}" != "200" ]]; then
+ echo "List projects failed with status ${list_status}"
+ exit 1
+fi
+
+create_payload=$(cat <