Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,50 @@ This repository is the **GitHub Action MVP** (PRD Phase 1). It runs on `issue_co

| Command | Who | Description |
| --- | --- | --- |
| `/coinpay create @payer <amount> "<desc>"` | Anyone (opt-in, disabled by default) | Create **and publish** a CoinPayPortal invoice from the repository's configured business and reply with a live payment link. Add `--dry-run` to preview without creating anything. See [GitHub-published invoices](#github-published-invoices-coinpay-create-payer). |
| `/coinpay create $10 USD --wallet <address>` | Maintainer on a PR | Create an idempotent payment from the PR and up to five linked closing issues. Add `--dry-run` to preview without creating anything. |
| `/coinpay invoice <amount> USD --crypto <code> --for "<desc>"` | Maintainer (direct) / contributor (request) | Create or request a payment. |
| `/coinpay approve` | Maintainer | Approve the pending request in this thread. |
| `/coinpay cancel` | Maintainer | Cancel the pending request in this thread. |
| `/coinpay status` | Anyone | Payment status (pull-only in Action mode). |
| `/coinpay help` | Anyone | Show help. |

The first argument after `create` selects the flow: `@payer` runs the invoice flow below, while a numeric amount keeps the legacy PR-backed payment flow exactly as before (including its maintainer gate and `--wallet` requirement). Neither flow falls back to the other.

`<code>` is a CoinPayPortal crypto code: `usdc_pol`, `usdc_sol`, `usdc_base`, `usdt_pol`, `btc`, `eth`, `sol`, … Amounts are **USD-denominated decimal values** with up to two fractional digits and up to nine digits before the decimal point. The PR-backed `create` command requires an explicit maintainer-supplied wallet and derives its description and canonical links from GitHub rather than accepting free-form invoice text.

Direct-create vs. request is decided by the commenter's `author_association`: `OWNER`/`MEMBER`/`COLLABORATOR` create directly; everyone else creates a pending request a maintainer approves. Tune with [`.github/coinpay.yml`](examples/coinpay.yml).
Direct-create vs. request is decided by the commenter's `author_association`: `OWNER`/`MEMBER`/`COLLABORATOR` create directly; everyone else creates a pending request a maintainer approves. Tune with [`.github/coinpay.yml`](examples/coinpay.yml). The `minRoleToCreateInvoice` gate applies to the legacy payment flow only — the `@payer` invoice flow has its own safeguards below.

## GitHub-published invoices (`/coinpay create @payer …`)

```
/coinpay create @octocat 25 "Fix the settlement race"
/coinpay create @octocat $25 USD "Fix the settlement race" --dry-run
```

Creates a **draft invoice** on the repository's configured CoinPayPortal business via the idempotent `POST /api/invoices` contract, **publishes** it (live payment details, **no email is sent**), and replies with the payer mention, invoice number, USD amount, description, the platform fee taken from the API response, and the live `…/now/{invoice}` payment link. Payment happens inside CoinPayPortal so the platform fee applies; the bot never moves funds, never marks anything paid, and never calls send/paid/delete APIs.

**Honest limitation — who issues, who pays.** The invoice issuer is always the **repository's configured CoinPayPortal business** (`COINPAY_BUSINESS_ID` + `COINPAY_API_KEY` from repository secrets). It is *not* the commenting user's personal CoinPay account: no GitHub-to-CoinPay account mapping exists yet. Likewise `@payer` is only a GitHub mention used for notification and audit — it is not a verified CoinPay client, and no client record is created. Every bot reply states this. Personal account linking is a later, separate milestone.

**Who may run it.** Once enabled, any human commenter — deliberately no role allowlist. Bot-authored and edited comments are ignored. The mandatory safeguards are non-identity ones:

- **Feature flag, default off** (`githubInvoices.enabled`). Roll out in this order: **1)** deploy the CoinPayPortal idempotent invoice creation API **and its database migration**, **2)** verify a test call succeeds, **3)** only then set `githubInvoices.enabled: true`. Until then the command replies with a safe explanation (and the API would answer 503 anyway). The global `enabled: false` kill switch also covers this command.
- **Per-invoice cap** `githubInvoices.maxAmountUsd` (default 1000).
- **Per-repository hourly cap** `githubInvoices.repositoryHourlyCap` (default 20, API range 1–1000), enforced **atomically by CoinPayPortal** per business/repository, so parallel workflow runs cannot overshoot it. Idempotent replays don't consume the cap.
- **One invoice per source comment.** The `Idempotency-Key` uses the immutable repository ID + comment ID, so redeliveries, reruns, and process restarts return the *original* invoice instead of a new one; the same key with different terms is rejected (409). A repository rename can change the source notes and cause a 409, but cannot create another invoice under a new key. A deleted invoice is never recreated (410) and a closed (e.g. already paid) invoice is reported but never reopened or republished.
- **Strict parsing**: one valid GitHub `@login`, a positive USD amount with at most two decimals, a quoted plain-text description (≤200 chars, control characters stripped), optional literal `USD`, and `--dry-run` as the only flag. No wallet, client, email, or any other flag can be injected; the business's configured receiving wallet is always the payee.
- **Audit source data**: the immutable numeric GitHub actor id, actor login, payer login, repository, thread and comment id are recorded on the invoice (`source_reference`), and the notes carry the canonical thread URL.

If draft creation succeeds but publish fails (including a `409` while payment details are still being generated), the bot replies with a safe retry message and **posts no payment link**. A maintainer must **re-run the same GitHub Actions run**, preserving its original comment ID; posting a new command comment would request a separate invoice. Each invoice request has a 30-second network timeout; a lost response may mean creation already succeeded, so the same-run retry is important. Replies never contain raw API errors, keys, or wallet addresses. Duplicate *GitHub comments* are best-effort deduplicated (hidden `coinpay:handled` markers plus a post-publish re-check) — GitHub offers no atomic lock, so the hard uniqueness guarantee is on the invoice itself, not the reply.

`--dry-run` previews the exact invoice (amount, description, crypto, idempotency key) without calling CoinPayPortal, changing labels, or notifying the payer (the mention is rendered inert).

### Deployment checks before enabling

- Set the Action's `coinpay-base-url` to the same public origin as the portal's `NEXT_PUBLIC_APP_URL` (ignoring a trailing slash). The bot rejects unexpected payment-link origins instead of posting them.
- Explicitly accept that any human commenter can create invoices under the configured business and notify a payer. No funds move automatically, but open usage can generate unwanted invoices and mentions.
- The invoice publish path does not use the legacy payment-creation route's monthly quota check. The hourly cap is a repository integration safeguard, not a subscription entitlement or a substitute for platform-wide abuse controls. Keep the feature disabled until the business owner accepts that rollout boundary.
- A rejected request needs its terms/configuration checked before retrying. Do not repeatedly re-run a permanent validation failure or post replacement commands without checking whether an invoice already exists.

## How it works

Expand All @@ -41,8 +75,9 @@ If `github-token` is a PAT or another token that posts as a different login, set

## Limitations (Action MVP)

- **A receiving wallet (or `--wallet <address>`) is required.** Crypto payment creation returns an error otherwise.
- **No live webhook status sync.** A GitHub Action is ephemeral and cannot receive CoinPayPortal webhooks, so `coinpay:paid` / `coinpay:expired` labels and paid-status comments land with the hosted GitHub App (Phase 2), not here.
- **A receiving wallet (or `--wallet <address>`) is required.** Crypto payment creation returns an error otherwise. The `@payer` invoice flow uses only the business's configured wallet and fails safely when none exists.
- **No live webhook status sync.** A GitHub Action is ephemeral and cannot receive CoinPayPortal webhooks, so `coinpay:paid` / `coinpay:expired` labels and paid-status comments land with the hosted GitHub App (Phase 2), not here. In particular, `/coinpay status` never reports an invoice as paid just because it was created.
- **No personal CoinPay accounts.** GitHub-published invoices are issued by the repository's configured business; commenter/payer account linking, client records, status webhooks, PDF output, refunds/cancellations, and multi-business routing are out of scope for this slice.
- Card / `both` payment methods require Stripe Connect on the CoinPayPortal business.

## Development
Expand Down
4 changes: 3 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ inputs:
default: 'github-actions[bot]'
outputs:
action:
description: 'What the bot did (invoice_created, request_pending, help, error, ...).'
description: 'What the bot did (invoice_created, invoice_published, request_pending, help, error, ...).'
payment_id:
description: 'CoinPayPortal payment id, when a payment was created.'
invoice_id:
description: 'CoinPayPortal invoice id, when a GitHub-published invoice was created.'
runs:
using: 'node24'
main: 'dist/index.js'
Loading