GRCpay logoGRCpay
On this page

About Gridcoin Pay

GRCpay is a small, self-hosted service that turns the Gridcoin blockchain into a checkout settlement layer. This page walks through how it works.

Overview

GRCpay is a self-hosted checkout facilitator for the Gridcoin network. For each customer order it asks the wallet daemon to mint a one-shot Gridcoin address, polls the daemon every few seconds to see how much has landed there, and once the requested amount has arrived it forwards the payment to the merchant's wallet. No accounts, no custodial storage, no middlemen. It's a transparent on-chain settlement layer any merchant can run next to their existing checkout.

The whole flow is built around a small REST API. Drop it in next to your ecommerce backend and you can accept Gridcoin payments in minutes. Pair it with one of the upcoming plugins (WooCommerce first) and there's no integration work at all.

Why does GRCpay need to exist?

Bitcoin has BTCPay Server, a self-hosted, non-custodial payment processor you can drop in front of any checkout. Gridcoin doesn't. Gridcoin isn't on BTCPay's altcoin support list, and it doesn't appear on the awesome-bitcoin-payment-processors curated list either. The stock wallet daemon (gridcoinresearchd) is a full node with a JSON-RPC interface, and that's where it stops. A merchant who wants to accept GRC has to build the rest of the plumbing themselves before a customer can click “Pay with GRC” in a store. GRCpay is that rest-of-the-plumbing.

What you have to build on top of the daemon

The wallet daemon is exactly that: a wallet. It does its job well, managing keys, keeping its view of the chain in sync, tracking which addresses belong to it, and sending GRC when asked. It also stays cleanly within that remit, and that's the right design call. Running a checkout is a different layer of plumbing, and a wallet daemon shouldn't pretend otherwise.

What you get from gridcoinresearchd is a clean JSON-RPC surface: getnewaddress mints an address, getreceivedbyaddress reports how much has arrived at one, sendtoaddress sends a transaction. The daemon keeps its internal ledger up to date as new blocks come in, and then waits to be asked. Every action is initiated by an explicit RPC call from outside, which is exactly what you want from a wallet: predictable, auditable, no surprises. The flip side is that anything checkout-shaped (orders, expiry, forwarding, customer-facing webhooks) has to live somewhere else. The higher layer ends up owning:

  • Order ↔ address mapping. The daemon stores addresses but has no idea which order or customer they belong to. You need your own database to remember “address SXxx is order #1234, expects 10 GRC.”

  • A polling loop. You have to call getreceivedbyaddress for every open order, on a schedule, and decide when an order counts as “funded.” GRCpay runs this loop every 10 seconds against every wallet still in new status.

  • A lifecycle state machine. new → funded → processed, with expired → refunded / norefund / error for orders that age out. Refunds in particular need transaction-history walking. GRCpay calls listtransactions and getrawtransaction to find the original sender of an expired wallet's funds, and falls back to an error status for manual review when it can't.

  • A real HTTP API. The wallet only speaks JSON-RPC over HTTP basic auth, which is fine for sysadmins and awkward for plugging into a web checkout. No JSON:API conventions, no CORS, no rate limits, no QR endpoint, no audit log.

  • Settlement. If the merchant wants funds forwarded to a cold wallet, you need to fetch the balance, set the fee, and send a transaction, all wrapped in retry logic for transient RPC errors.

  • Keypool refills on demand. The daemon pre-generates a fixed buffer of keys (DEFAULT_KEYPOOL_SIZE = 100 in src/wallet/wallet.h). A busy merchant burns through that quickly and has to call keypoolrefill by hand. GRCpay handles the refill automatically every time getnewaddress fails.

What GRCpay actually adds

GRCpay is the thin layer that fills exactly that gap. It speaks JSON-RPC to gridcoinresearchd on one side and exposes a small JSON:API REST surface on the other, with the order-tracking database, polling loop, lifecycle state machine, refund flow, keypool refill, QR generation, and rate limiting all built in. It's the missing payment-processor layer that turns the wallet daemon into something a checkout can talk to.

It doesn't bypass any wallet limitations. There's no magic. It packages all the plumbing every merchant would otherwise have to write themselves into one open-source service that anyone can run against their own wallet.

A smaller attack surface

There's a quiet but real security win that comes for free with this architecture: putting GRCpay in front of gridcoinresearchd means you never expose the wallet's JSON-RPC interface to the internet. The full wallet RPC includes commands like dumpprivkey, walletpassphrase, and sendtoaddress. Anyone who reaches the RPC port with the right credentials can dump keys or drain the wallet outright.

GRCpay's public REST surface is deliberately narrow: create a payment wallet (validated input, no funds moved), look one up (read-only), fetch a QR code, list rates, query status. That's the entire set of operations a checkout actually needs. Nothing reachable from the internet can dump private keys, sign arbitrary transactions, or spend funds outside the lifecycle GRCpay itself controls.

  • The wallet daemon stays bound to localhost (or the internal Docker network) and is never reachable from outside.

  • Only GRCpay's small REST surface is exposed publicly.

  • Even if GRCpay's API is somehow compromised, the worst-case impact is the creation of empty payment wallets and the disclosure of already-public wallet state. None of the high-blast-radius wallet RPCs are reachable through it.

This is the same model BTCPay uses for Bitcoin: the wallet stays private, and the payment processor only speaks the limited subset of commands a checkout actually needs.

Sources

  • DEFAULT_KEYPOOL_SIZE = 100 and the wallet's key-management internals: src/wallet/wallet.h on the master branch of gridcoin-community/Gridcoin-Research.

  • JSON-RPC commands referenced in this chapter (getnewaddress, keypoolrefill, getreceivedbyaddress, sendtoaddress, listtransactions, getrawtransaction): Gridcoin RPC commands wiki.

Protocol & Lifecycle

Every payment wallet moves through a small state machine. The status is exposed on every wallet record returned by the API so your integration can react to changes without having to interpret raw blockchain data.

new ──▶ confirming ──▶ funded ──▶ processed
  │       │
  │       └──▶ new  (mempool drop / reorg)
  │
  └─▶ expired ──▶ refunded
              └─▶ norefund
  • new

    Wallet just created, or still waiting for enough funds to cover the invoice. GRCpay polls the wallet daemon for both the confirmed balance and the 0-conf mempool balance on each tick; anything still unconfirmed is tracked as amountPending.

  • confirming

    The customer has deposited enough GRC to cover the invoice when counting the confirmed balance AND the 0-conf pending balance, but the confirmed portion hasn't yet reached MIN_CONFIRMATIONS blocks. This is the right state to render a "payment detected, waiting for confirmations" banner to the customer so they don't re-send. Transitions forward to funded when the confirmed balance actually meets the invoice, or backward to new if a pending tx drops out of the mempool (reorg, low-fee replacement).

  • funded

    The required amount has been CONFIRMED (not just mempool-detected). If the customer overpaid, GRCpay refunds the excess to them first; then, if a recipient address was supplied at creation time, it forwards exactly the required amount to the merchant. Same-block reorgs can't flip a wallet into this state because amountRecieved only reflects the confirmed portion.

  • processed

    Payment has been forwarded to the recipient (or marked complete if no recipient was given). This is the success terminal state. If an overpayment was refunded along the way, the refund txid is in the wallet record's refundTx field and the amount in refundAmount.

  • expired

    The wallet aged out before being fully funded (default lifespan is 2 hours). GRCpay will attempt to refund any partial balance to the original sender.

  • refunded

    After the wallet expired, GRCpay walked the transaction history and successfully returned each sender their original contribution (minus the per-refund network fee). Wallets with multiple senders see one refund tx per sender.

  • norefund

    Wallet expired with no balance to return. Terminal state, nothing further to do.

  • error

    Something went wrong during processing. The wallet is parked for manual inspection.

Settlement

When a wallet's balance reaches the requested amount, GRCpay settles it. “Settle” means two things: if the customer paid more than was asked for, refund the excess to them; then forward exactly the required amount to the merchant's recipient address (minus the standard Gridcoin network fee, currently 0.001 GRC per transaction). The merchant always gets exactly what they asked for, not whatever happened to land in the wallet. Any overpayment goes back to the customer who caused it, which is how honest typos and stale fiat→GRC conversions get handled automatically instead of silently disappearing into the merchant's balance.

All amounts are tracked in halford precision (1 GRC = 100,000,000 halford) to avoid floating-point rounding surprises, and only converted to GRC for display and for RPC calls to the wallet daemon. The full refund flow section below has the details on how the refund side works, including the dust and sender-not-found edge cases.

If no recipient was supplied at creation time, the wallet is simply marked processed and the funds stay at the address, which is useful when the merchant prefers to sweep balances manually. Overpayment refunds still happen in this case; only the forward-to-merchant step is skipped.

Refunds

GRCpay refunds funds back to customers in three different situations, each with slightly different semantics. In all of them, the refund side is best-effort: GRCpay tries hard to do the right thing, and when it genuinely can't (sender unknown, dust amount, RPC error) it degrades gracefully rather than parking the wallet in limbo.

Overpayment refunds

When a customer sends more GRC than the wallet's required amount (a typo, a stale fiat→GRC conversion, or an off-by-one somewhere in their checkout) GRCpay detects the overpayment at settlement time and refunds the excess to the sender before forwarding the required amount to the merchant.

The refund goes to the latest contributor, the account whose payment pushed the wallet over the required amount. This is almost always the customer who caused the overpayment in the first place. Their net cost ends up being required + MIN_FEE (they pay for the refund tx themselves, which feels fair: the merchant shouldn't be penalised for their typo). The merchant gets exactly required - MIN_FEE, the same as on a clean payment.

Three corner cases where the refund doesn't happen and the merchant ends up absorbing the overpayment as a tip instead:

  • Dust overpayment. If the overpayment is smaller than the network fee (e.g. the customer paid 10.0005 GRC for a 10 GRC order), issuing a refund would cost more than it returns. Skipped. The merchant gets the tiny tip.

  • Sender can't be determined. If GRCpay can't walk the transaction history back to a usable sender address, it can't issue the refund. This is rare but possible with unusual wallet setups. The overpayment is absorbed into the merchant payout.

  • Refund tx itself fails, persistently. If the refund RPC call throws (wallet locked, network glitch, etc.) GRCpay does not immediately give up and forward the money to the merchant. Instead the wallet stays in funded and the failure is retried with exponential backoff (default intervals 30s, 1m, 2m, 4m) across the next few job-loop cycles. That window gives a real human operator time to actually unlock the daemon (the usual root cause) before we declare the refund hopeless. Only after the retry cap (default 5 attempts) is exhausted does GRCpay fall back to forwarding the full balance so the merchant payout is never blocked indefinitely.

When a refund does go through, the refund txid is recorded on the wallet record in the refundTx field, and the actual GRC amount returned to the customer is in refundAmount. Plugin authors can surface either one to the merchant in their order details screen.

Expired-wallet refunds

If a wallet expires with a non-zero balance (the customer paid late, or the order sat open too long without reaching its target) GRCpay walks the full transaction history and refunds each contributor the amount they originally sent, minus the per-refund network fee. A wallet that received contributions from multiple senders sees multiple refund transactions, one per sender.

Final wallet status depends on what happened:

  • refunded: at least one refund went out and none failed. The wallet record's tx_out holds the first refund txid; the total GRC returned is in refundAmount; full per-sender details are in the audit log.

  • norefund: every contribution was below the network fee threshold. Refunding any of them would net-negative the wallet, so nothing is attempted. Terminal state, operator doesn't need to do anything.

  • error: either no senders could be identified at all, or at least one refund RPC call threw. Any refunds that did succeed are still recorded, but the wallet is parked for an operator to check the audit log and decide what to do with the remainder.

Late-payment refunds

A trickier edge case: what happens when a customer sends GRC to a wallet that has already settled? Picture a shopper who opens a checkout page, walks away for an hour, and eventually clicks “pay” on a stale tab. By that point the order has either been completed by someone else, expired, or been cancelled by the merchant. Without special handling, that GRC would sit silently in GRCpay's hot wallet and the customer would lose it.

GRCpay runs a dedicated late-payment sweep on a separate slow timer (default once an hour, configurable via the LATE_PAYMENT_CHECK_INTERVAL env var; set it to 0 to turn the sweep off entirely). For every terminal-state wallet whose updated_at is within the last 7 days (the LATE_PAYMENT_WINDOW, also env-tunable), it asks the daemon for the current balance, compares it against the last amount GRCpay recorded, and refunds the difference to the latest sender, minus the per-tx fee, same economics as the overpayment flow.

Past the 7-day window the wallet is considered cold: any stale browser session or cached checkout page is long gone, and late deliveries into that address stay in the hot wallet for a human operator to sweep manually. Customers who have genuinely been sitting on a GRCpay address for a month should be reaching out to the merchant through normal channels anyway.

The rules are otherwise identical to overpayment refunds: dust amounts are absorbed into the hot wallet (not worth the fee), sender-not-found cases are logged and left alone, and refund RPC failures retry with the same exponential backoff before giving up. Every successful late refund appears in db_logs under the late_refund action, and the wallet's refundAmount is bumped cumulatively so merchants can always see the total GRC ever returned through that address.

Expiry & Background Jobs

Each wallet has a configurable lifespan (default: 2 hours). A background job loop runs every 10 seconds and walks every active wallet through five steps in sequence: refresh balances → mark funded → mark expired → forward funded payments → process expired refunds.

The single sequential loop keeps the moving parts auditable: every status transition is written to a db_logs audit table, so it's straightforward to reconstruct a wallet's timeline from the database alone — no extra event store required.

Self-host it (recommended)

GRCpay is open source and the preferred way to run it is on your own infrastructure, pointed at a Gridcoin wallet you control. You get a small, auditable Express service and a SQLite file. That's the whole stack.

Self-hosting is also what makes GRCpay genuinely non-custodial: your merchant funds never touch our wallet, our database, or our infrastructure. The only thing we'd ever see is the docs site you're reading right now.

Using the public instance

We also run a public copy at https://grcpay.gridcoin.club/api that anyone can point their plugin or integration at. It's a fast way to try the protocol without standing up your own stack first, and it's the same code you'd run yourself.

Privacy

GRCpay has no concept of accounts. There's no KYC, and no personal data is ever requested or stored. Each wallet is just a Gridcoin address, an expected amount, and an optional recipient. That's it.

The docs site you're reading uses Plausible for traffic analytics. No tracking pixels, no marketing cookies, no Google Analytics. The tracking script can be disabled at deploy time via the NEXT_PUBLIC_TRACK flag.


Self-hosted Gridcoin payment facilitator. Privacy-first by design.Terms · Legal overview

Made with by @gridcat · Part of Gridcoin Club ↗ · Testnet