Put money on a GitHub issue and get a tamper-evident record of it — certificate, verification, receipt. $0.01 to mint, $0.002 to verify. Non-custodial: the service never touches the bounty.
| Route | Price | What lands in the 200 body |
|---|---|---|
POST /bounties | $0.01 | A signed bounty certificate — issue URL, title, pledged amount, terms, expiry — minted only after the issue is confirmed real and open through the live GitHub API. Plus a one-time settleKey. |
GET /verify/:bountyId | $0.002 | A signed verification report from live GitHub state: is the issue closed, which merged PRs reference it, who wrote them, is the bounty eligible |
POST /settle/:bountyId | free | A signed payout receipt. Closing out costs nothing — you already paid to mint |
GET /bounties · POST /check-signature · GET / · /health · /skill.md · /.well-known/x402 · /openapi.json | free | Public board, signature validation, discovery |
base-sepolia / baseexact · EIP-3009 transferWithAuthorization0x40252CFDF8B20Ed757D61ff157719F33Ec332402solana / solana-devnetexact · SPL transferChecked, fee sponsoredWwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwWBoth rails appear in the accepts array of every 402. Settlement runs through
the x402 facilitator, and the 200 carries an
X-PAYMENT-RESPONSE receipt naming the rail and the transaction.
# 1. run it — .env.example already has working defaults
npm install && npm run dev # http://localhost:4027
# 2. mint without paying → the dual-rail challenge
curl -s -X POST localhost:4027/bounties -H 'content-type: application/json' \
-d '{"issueUrl":"https://github.com/nodejs/node/issues/1","amount":25}' \
| jq '.accepts[] | {network, payTo, maxAmountRequired}'
# { "network": "base-sepolia", "payTo": "0x40252CF…2402", "maxAmountRequired": "10000" }
# { "network": "solana", "payTo": "WwwuGbqH…T3WwW", "maxAmountRequired": "10000" }
# 3. pay $0.01 → the signed certificate lands in the 200 body
const pay = wrapFetchWithPayment(fetch, wallet);
const res = await pay(`${BASE}/bounties`, { method: "POST", body: … });
const { certificate, signature, settleKey } = await res.json();
# 4. later — buy proof that someone fixed it ($0.002)
await pay(`${BASE}/verify/${certificate.bountyId}`);
// { report: { issue: { state: "closed" }, mergedPrs: [{ number: 418, author: "…" }],
// eligible: true }, signature: "…" }
This service never holds your money. The $0.01 buys the certificate; the amount you declare is a pledge signed into it. You pay the claimant yourself and record it with POST /settle/:bountyId, which returns the signed receipt.
Custody is the hard part of every bounty platform — it is where the fees, the lock-ups, the disputes and the regulatory surface come from. Strip it out and what is actually scarce remains: a tamper-evident record that this issue was open, this amount was pledged, this PR merged, this payout happened. That record is the product, and it costs a cent.
Certificates, reports and receipts are HMAC-SHA256 over canonical JSON — keys sorted recursively, so the signature is stable regardless of field order. Validate any of them for free:
curl -s -X POST localhost:4027/check-signature \
-H 'content-type: application/json' \
-d '{"payload": <the signed object>, "signature": "<hex>"}'
{ "valid": true, "type": "x402-bounty-certificate", "checkedAt": "…" }
Set SIGNING_SECRET in production — with the default dev secret (the server warns at boot) anyone running this code can forge a certificate that looks like yours. And read a signature for what it is: proof that this deployment observed that GitHub state at that time, not proof that anyone escrowed funds.
Every certificate and every report is built from the GitHub REST API at request time. Minting checks the issue exists, is an issue rather than a PR, and is still open — otherwise you get a 404, 400 or 409 and no certificate. Verification reads the issue timeline for cross-referenced pull requests and confirms each one actually merged.
GITHUB_TOKEN is optional: without it you get GitHub's 60 requests/hour anonymous quota, with it 5,000/hour plus issues in private repos the token can read. There is no fixture mode.