+ + {item.title} + +
+ {item.excerpt ?{item.excerpt}
: null} +From aee191580440b73fcc30b83059aee1cd80fc766a Mon Sep 17 00:00:00 2001
From: Zhenvip <171159317+Zhenvip@users.noreply.github.com>
Date: Tue, 26 May 2026 03:16:58 +0800
Subject: [PATCH] feat: add bounty issue discovery homepage
---
package-lock.json | 417 +++++++---
.../migration.sql | 9 +
prisma/schema.prisma | 28 +-
src/app/deploy/page.tsx | 469 +++++++++++
src/app/layout.tsx | 4 +-
src/app/page.tsx | 733 +++++++++---------
src/lib/github/webhook-handler.ts | 65 +-
src/lib/issues/discovery.ts | 116 +++
tests/issue-discovery.test.ts | 134 ++++
9 files changed, 1495 insertions(+), 480 deletions(-)
create mode 100644 prisma/migrations/20260525190000_add_issue_discovery_metadata/migration.sql
create mode 100644 src/app/deploy/page.tsx
create mode 100644 src/lib/issues/discovery.ts
create mode 100644 tests/issue-discovery.test.ts
diff --git a/package-lock.json b/package-lock.json
index cd9f570..9b7045e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"@octokit/rest": "^21.1.1",
"@octokit/webhooks": "^13.9.0",
"@prisma/client": "^6.17.1",
- "@pvium/sdk": "file:../sdks/node",
+ "@pvium/sdk": "0.2.3",
"next": "^15.5.6",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@@ -28,20 +28,11 @@
"typescript": "^5.8.3"
}
},
- "../sdks/node": {
- "name": "@pvium/sdk",
- "version": "0.2.2",
- "license": "MIT",
- "dependencies": {
- "crypto-js": "^4.2.0",
- "ethers": "^6.16.0",
- "keccak256": "^1.0.6",
- "merkletreejs": "^0.6.0"
- },
- "devDependencies": {
- "@types/crypto-js": "^4.2.2",
- "typescript": "^5.8.2"
- }
+ "node_modules/@adraffy/ens-normalize": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
+ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
+ "license": "MIT"
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
@@ -379,9 +370,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -398,9 +386,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -417,9 +402,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -436,9 +418,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -455,9 +434,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -474,9 +450,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -493,9 +466,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -512,9 +482,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -531,9 +498,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -556,9 +520,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -581,9 +542,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -606,9 +564,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -631,9 +586,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -656,9 +608,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -681,9 +630,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -706,9 +652,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -868,9 +811,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -887,9 +827,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -906,9 +843,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -925,9 +859,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -969,6 +900,30 @@
"node": ">= 10"
}
},
+ "node_modules/@noble/curves": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
+ "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.3.2"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
+ "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1471,8 +1426,16 @@
}
},
"node_modules/@pvium/sdk": {
- "resolved": "../sdks/node",
- "link": true
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@pvium/sdk/-/sdk-0.2.3.tgz",
+ "integrity": "sha512-nsnUAma89tuhHZAg8eOLt4Qojeo+F6tTvQhwYOHauFzW2WUGfm+DttPXJKBWCPf+awWPmZzX3Vikfc5wo3B31A==",
+ "license": "MIT",
+ "dependencies": {
+ "crypto-js": "^4.2.0",
+ "ethers": "^6.16.0",
+ "keccak256": "^1.0.6",
+ "merkletreejs": "^0.6.0"
+ }
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
@@ -1966,9 +1929,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1983,9 +1943,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2000,9 +1957,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2017,9 +1971,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2034,9 +1985,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2051,9 +1999,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2068,9 +2013,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2085,9 +2027,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2176,6 +2115,12 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/aes-js": {
+ "version": "4.0.0-beta.5",
+ "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
+ "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
+ "license": "MIT"
+ },
"node_modules/ajv": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
@@ -2446,12 +2391,38 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/before-after-hook": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
"license": "Apache-2.0"
},
+ "node_modules/bn.js": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
+ "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
@@ -2476,6 +2447,36 @@
"node": ">=8"
}
},
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/buffer-reverse": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-reverse/-/buffer-reverse-1.0.1.tgz",
+ "integrity": "sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==",
+ "license": "MIT"
+ },
"node_modules/c12": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
@@ -2693,6 +2694,12 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+ "license": "MIT"
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -3522,6 +3529,49 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ethers": {
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
+ "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/ethers-io/"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.buymeacoffee.com/ricmoo"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@adraffy/ens-normalize": "1.10.1",
+ "@noble/curves": "1.2.0",
+ "@noble/hashes": "1.3.2",
+ "@types/node": "22.7.5",
+ "aes-js": "4.0.0-beta.5",
+ "tslib": "2.7.0",
+ "ws": "8.17.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/ethers/node_modules/@types/node": {
+ "version": "22.7.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
+ "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
+ "node_modules/ethers/node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "license": "MIT"
+ },
"node_modules/exsolve": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
@@ -3998,6 +4048,26 @@
"node": ">= 0.4"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4035,6 +4105,12 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4564,6 +4640,32 @@
"node": ">=4.0"
}
},
+ "node_modules/keccak": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz",
+ "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^2.0.0",
+ "node-gyp-build": "^4.2.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/keccak256": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/keccak256/-/keccak256-1.0.6.tgz",
+ "integrity": "sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^5.2.0",
+ "buffer": "^6.0.3",
+ "keccak": "^3.0.2"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -4664,6 +4766,20 @@
"node": ">= 8"
}
},
+ "node_modules/merkletreejs": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.6.0.tgz",
+ "integrity": "sha512-cyiratjG7fyHsa4DVfYVPxcoAh3zmUuOPItIfZex8f0pUVptNEmiiTOoeS0JnDDTWy+n3FKnI0K1gCzti7rGMg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-reverse": "^1.0.1",
+ "crypto-js": "^4.2.0",
+ "treeify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 7.6.0"
+ }
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -4801,6 +4917,12 @@
}
}
},
+ "node_modules/node-addon-api": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
+ "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==",
+ "license": "MIT"
+ },
"node_modules/node-exports-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -4837,6 +4959,17 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/nypm": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz",
@@ -5318,6 +5451,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -5475,6 +5622,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -5752,6 +5919,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -6017,6 +6193,15 @@
"node": ">=12"
}
},
+ "node_modules/treeify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz",
+ "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -6047,8 +6232,7 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
- "license": "0BSD",
- "optional": true
+ "license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -6238,6 +6422,12 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6353,6 +6543,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/prisma/migrations/20260525190000_add_issue_discovery_metadata/migration.sql b/prisma/migrations/20260525190000_add_issue_discovery_metadata/migration.sql
new file mode 100644
index 0000000..b39872e
--- /dev/null
+++ b/prisma/migrations/20260525190000_add_issue_discovery_metadata/migration.sql
@@ -0,0 +1,9 @@
+ALTER TABLE "Bounty" ADD COLUMN "issueTitle" TEXT;
+ALTER TABLE "Bounty" ADD COLUMN "issueUrl" TEXT;
+ALTER TABLE "Bounty" ADD COLUMN "issueState" TEXT;
+ALTER TABLE "Bounty" ADD COLUMN "issueBodyExcerpt" TEXT;
+ALTER TABLE "Bounty" ADD COLUMN "issueCreatedAt" TIMESTAMP(3);
+ALTER TABLE "Bounty" ADD COLUMN "issueUpdatedAt" TIMESTAMP(3);
+
+CREATE INDEX "Bounty_status_amount_idx" ON "Bounty"("status", "amount");
+CREATE INDEX "Bounty_issueUpdatedAt_idx" ON "Bounty"("issueUpdatedAt");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index df1171d..9f896d4 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -54,22 +54,30 @@ model GithubUserLink {
}
model Bounty {
- id String @id @default(cuid())
- repositoryId String
- issueNumber Int
- issueNodeId String?
- labelName String
- amount Decimal @db.Decimal(18, 6)
- currency String
- status BountyStatus @default(OPEN)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+ id String @id @default(cuid())
+ repositoryId String
+ issueNumber Int
+ issueNodeId String?
+ issueTitle String?
+ issueUrl String?
+ issueState String?
+ issueBodyExcerpt String? @db.Text
+ issueCreatedAt DateTime?
+ issueUpdatedAt DateTime?
+ labelName String
+ amount Decimal @db.Decimal(18, 6)
+ currency String
+ status BountyStatus @default(OPEN)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
repository RepositoryInstallation @relation(fields: [repositoryId], references: [id], onDelete: Cascade)
rewards RewardAttempt[]
@@unique([repositoryId, issueNumber, labelName])
@@index([repositoryId, issueNumber])
+ @@index([status, amount])
+ @@index([issueUpdatedAt])
}
model RewardAttempt {
diff --git a/src/app/deploy/page.tsx b/src/app/deploy/page.tsx
new file mode 100644
index 0000000..5c63fe6
--- /dev/null
+++ b/src/app/deploy/page.tsx
@@ -0,0 +1,469 @@
+import type { CSSProperties, ReactNode } from "react";
+
+const flowSteps = [
+ "A repository owner installs the GitHub App.",
+ "A maintainer labels an issue with a Pvium bounty label, for example pvium:20USDC or pvium:10.",
+ "When a pull request is merged into a configured reward target branch and closes that issue, the app checks the PR author.",
+ "If the PR author is already linked to a Pvium account, the app creates a Pvium payment link and comments a Pay reward link on the PR.",
+ "If the PR author is not linked, the app generates a signed Pvium OAuth invite for type: github and comments the invite link on the PR.",
+ "Once the user accepts the invite and connects the matching GitHub account in Pvium, the Pvium webhook creates the payment link and comments the Pay reward link on the PR.",
+ "When Pvium sends a paid or funded webhook, the app marks the reward and bounty as PAID.",
+];
+
+const localSetupCommands = [
+ "cd /Users/Projects/Javascript/paytrack/sdks/node",
+ "npm install",
+ "npm run build",
+ "",
+ "cd /Users/Projects/Javascript/paytrack/github-app",
+ "npm install",
+ "cp .env.example .env",
+ "npm run prisma:generate",
+ "npm run prisma:migrate",
+ "npm run dev",
+];
+
+const envExample = `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/pvium_github_app"
+
+GITHUB_APP_ID=""
+GITHUB_APP_PRIVATE_KEY=""
+GITHUB_WEBHOOK_SECRET=""
+GITHUB_REWARD_TARGET_BRANCHES="main,master"
+PVIUM_BOUNTY_LABEL_PREFIX="pvium:"
+
+PVIUM_ENVIRONMENT="sandbox"
+PVIUM_API_BASE_URL=""
+PVIUM_CONSENT_HOST=""
+PVIUM_SDK_LOG_REQUESTS="false"
+PVIUM_API_KEY=""
+PVIUM_CLIENT_ID=""
+PVIUM_WEBHOOK_SECRET=""
+PVIUM_INVITE_SIGNER_PRIVATE_KEY=""
+PVIUM_OAUTH_REDIRECT_URI="http://localhost:3000/api/pvium/oauth/callback"
+PVIUM_REWARD_PAYMENT_MODEL="instant-batch"
+PVIUM_REWARD_PAYMENT_SIGNER_PRIVATE_KEY=""
+PVIUM_REWARD_PAYMENT_CHAIN="base"
+PVIUM_REWARD_PAYMENT_CHAIN_ID="8453"
+PVIUM_REWARD_PAYMENT_CURRENCY="USDC"
+PVIUM_REWARD_PAYMENT_TOKEN_ADDRESS=""
+PVIUM_REWARD_PAYMENT_TOKEN_DECIMALS="6"
+PVIUM_REWARD_PLATFORM_FEE_WALLET=""
+PVIUM_REWARD_PLATFORM_FEE_BASIS_POINTS="0"
+PVIUM_REWARD_MAX_FEE_AMOUNT="0"
+PVIUM_INVOICE_REDIRECT_URI="http://localhost:3000/api/pvium/oauth/callback"
+
+APP_BASE_URL="http://localhost:3000"`;
+
+const githubPermissions = [
+ "Issues: read and write",
+ "Pull requests: read and write",
+ "Metadata: read-only",
+];
+
+const githubEvents = ["issues", "pull_request"];
+
+const configItems = [
+ "GITHUB_REWARD_TARGET_BRANCHES is a comma-separated list of base branches that can trigger reward processing when a PR is merged.",
+ "PVIUM_BOUNTY_LABEL_PREFIX controls the GitHub issue label prefix used to detect bounties. It defaults to pvium: when unset or empty.",
+ "PVIUM_ENVIRONMENT resolves Pvium hosts: test uses localhost, sandbox uses api-sandbox.pvium.com, and production uses api.pvium.com.",
+ "PVIUM_API_BASE_URL and PVIUM_CONSENT_HOST override the resolved Pvium hosts when needed.",
+ "PVIUM_SDK_LOG_REQUESTS=true logs SDK request method, host, path, status, duration, and network errors without logging full URLs or secrets.",
+ "PVIUM_REWARD_PAYMENT_MODEL controls the payment artifact. Use instant-batch for finalized instant batch payment links or invoice for the legacy invoice flow.",
+ "PVIUM_REWARD_PAYMENT_CHAIN is the chain used by both invoice payment channels and instant batch links.",
+ "PVIUM_REWARD_PAYMENT_CURRENCY is the invoice payment currency.",
+ "PVIUM_REWARD_PAYMENT_SIGNER_PRIVATE_KEY signs instant batches. If omitted, the invite signer is used.",
+ "PVIUM_REWARD_PAYMENT_CHAIN_ID is the chain id used to finalize instant batches.",
+ "PVIUM_REWARD_PAYMENT_TOKEN_ADDRESS and PVIUM_REWARD_PAYMENT_TOKEN_DECIMALS define the instant batch payout token.",
+ "PVIUM_REWARD_PLATFORM_FEE_WALLET receives the platform fee. If omitted, no platform fee payee is added.",
+ "PVIUM_REWARD_PLATFORM_FEE_BASIS_POINTS sets the fee. For example, 100 is 1% and 250 is 2.5%.",
+ "PVIUM_REWARD_MAX_FEE_AMOUNT caps the computed platform fee. Use 0 for no cap.",
+];
+
+const pviumEvents = [
+ "oauth.invite.accepted",
+ "invoice.paid",
+ "invoice.payment_completed",
+ "invoice.payment.succeeded",
+ "payment.attached",
+ "batch.funded",
+ "batch.payment_completed",
+ "batch.payment.succeeded",
+];
+
+const usageSteps = [
+ "Install the GitHub App on a repository.",
+ "Add a bounty label to an issue, such as pvium:20USDC or pvium:20. If PVIUM_BOUNTY_LABEL_PREFIX is changed, use that prefix instead.",
+ "Merge a PR into a configured reward target branch with a closing reference like Closes #123.",
+ "The app comments on the merged PR.",
+ "If the contributor needs to link Pvium, they use the invite link in the comment.",
+ "Pvium redirects back to /api/pvium/oauth/callback with an OAuth code.",
+ "The app exchanges the code through the local Pvium SDK, verifies the accepted GitHub handle, saves the OAuth token set, creates the payment link, and comments a Pay reward link.",
+ "The maintainer clicks Pay reward and completes payment in Pvium.",
+];
+
+export default function Home() {
+ return (
+ Pvium GitHub App
+ Turn merged pull requests into payable rewards. Maintainers label
+ bounty issues, contributors close them with PRs, and Pvium handles the
+ invite, payment link, funded webhook, and paid status updates.
+
+ The reward automation uses the local Pvium SDK at{" "}
+
+ Required values are documented in .env.example:
+
+ Generate
+ Configure the webhook URL as{" "}
+
+ Configure the Pvium webhook URL as{" "}
+
+ When{" "}
+
+ The app stores Pvium OAuth access and refresh tokens on the GitHub
+ user link so future merged PRs for the same contributor can create
+ rewards without asking the contributor to authorize again. Treat these
+ OAuth tokens as secrets; production deployments should encrypt them at
+ rest and restrict database access.
+
+
+
+ Reward GitHub contributors with Pvium payment links.
+
+
+ /Users/Projects/Javascript/paytrack/sdks/node
+
+ . The package points @pvium/sdk{" "}
+ at file:../sdks/node, so
+ rebuild the SDK after changing it.
+ GITHUB_APP_PRIVATE_KEY{" "}
+ from the GitHub App settings page under Private keys, then copy the
+ full PEM contents into the environment with line breaks replaced by{" "}
+ \n.
+
+ https://<your-host>/api/github/webhook
+
+ .
+
+ https://<your-host>/api/pvium/webhook
+
+ . Set PVIUM_WEBHOOK_SECRET to
+ the same secret configured on the Pvium client app.
+
+ PVIUM_REWARD_PLATFORM_FEE_WALLET
+ {" "}
+ is set and the fee basis points are greater than zero, instant batches
+ include the platform fee as the first payee with memo{" "}
+ platform fee. The contributor
+ reward amount is not reduced by the fee.
+ {title}
+ {children}
+
{value}
+ {value};
+}
+
+const styles: RecordPvium GitHub App
-- Turn merged pull requests into payable rewards. Maintainers label - bounty issues, contributors close them with PRs, and Pvium handles the - invite, payment link, funded webhook, and paid status updates. -
-
- The reward automation uses the local Pvium SDK at{" "}
-
- /Users/Projects/Javascript/paytrack/sdks/node
-
- . The package points @pvium/sdk{" "}
- at file:../sdks/node, so
- rebuild the SDK after changing it.
-
- Required values are documented in .env.example: -
-
- Generate GITHUB_APP_PRIVATE_KEY{" "}
- from the GitHub App settings page under Private keys, then copy the
- full PEM contents into the environment with line breaks replaced by{" "}
- \n.
-
- Configure the webhook URL as{" "}
-
- https://<your-host>/api/github/webhook
-
- .
-
- Configure the Pvium webhook URL as{" "}
-
- https://<your-host>/api/pvium/webhook
-
- . Set PVIUM_WEBHOOK_SECRET to
- the same secret configured on the Pvium client app.
-
- When{" "}
-
- PVIUM_REWARD_PLATFORM_FEE_WALLET
- {" "}
- is set and the fee basis points are greater than zero, instant batches
- include the platform fee as the first payee with memo{" "}
- platform fee. The contributor
- reward amount is not reduced by the fee.
-
- The app stores Pvium OAuth access and refresh tokens on the GitHub - user link so future merged PRs for the same contributor can create - rewards without asking the contributor to authorize again. Treat these - OAuth tokens as secrets; production deployments should encrypt them at - rest and restrict database access. -
-+ Adjust the minimum bounty or wait for connected repositories to + register Pvium bounty labels. +
+{value}
+ {item.excerpt}
: null} +{value};
+function formatAmount(value: number) {
+ return new Intl.NumberFormat("en", {
+ maximumFractionDigits: 2,
+ }).format(value);
}
const styles: Record