How to Accept Bitcoin Payments with Electrum Using PHP and JSONRPC

Cockbox, my VPS provider, uses the thin wallet Electrum for Bitcoin payment processing. After 7 months of experience, I think I've found out what does and doesn't work for stable, secure bitcoin payments using PHP and Electrum's JSONRPC interface.

The Setup

Electrum can be installed with pip install electrum. The only configuration that needs to be done is to enable the JSONRPC interface, and a systemd service file (or sysvinit or whatever you use). In ~/.electrum/config of whatever user is accepting payments:

{
  "rpcport": 6969
}

And the service file, in /etc/systemd/system/electrum.service:

[Unit]
Description=Shuffles ur butt coins
After=network.target

[Service]
Type=forking
User={USER}
Restart=on-failure
ExecStart=/usr/local/bin/electrum daemon start

[Install]
WantedBy=multi-user.target

Enabling that with systemctl enable electrum will make sure Electrum always starts on boot.

Lesson #1: Ship your coins off-server

Don't keep your bitcoins on the server you're accepting payments on, or reuse the same wallet on your personal computer (since that is just as bad). Instead, ship your coins to a dedicated receiving address that is part of a seperate wallet that you control. If you don't need access to these coins immediately, use a cold wallet.

Lesson #2: Do not try to avoid user correlation

Don't try to avoid people correlating users together or trying to map out your revenue stream. Unless you use a 3rd-party mixing service for every transaction, you are not going to be able to do it. One idea I entertained when setting this up was to use a round-robin list of bitcoin addresses payments went to, but I quickly realized that this would only require someone to look one more address back to find the same information. Just don't bother.

Lesson #3: Do not rely on Electrum notify

Cockbox first launched using Electrum's notify functionality to process payments. The premise sounds nice: Via JSONRPC you request that electrum make an HTTP callback for every bitcoin transaction, and again when that transaction is confirmed. That would be great if you could rely on the notifications coming through! But you can't. You fucking can't. Notifications are missed often enough for it to fuck up your processing completely. Don't use it.

The Schema

You need to keep track of payments somehow. This is going to be custom and tied into your application somehow, but here's what Cockbox's looks like:

+---------------+------------------+------+-----+---------+----------------+
| Field         | Type             | Null | Key | Default | Extra          |
+---------------+------------------+------+-----+---------+----------------+
| id            | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| created_at    | timestamp        | YES  |     | NULL    |                |
| updated_at    | timestamp        | YES  |     | NULL    |                |
| service_id    | int(11)          | NO   |     | NULL    |                |
| amount_usd    | double           | NO   |     | 0       |                |
| amount_btc    | double           | NO   |     | 0       |                |
| btc_tx        | varchar(255)     | YES  |     | NULL    |                |
| expiry_before | datetime         | YES  |     | NULL    |                |
| expiry_after  | datetime         | YES  |     | NULL    |                |
+---------------+------------------+------+-----+---------+----------------+

The custom fields here being service_id, expiry_before, and expiry_after. updated_at is one of the timestamp fields in laravel, which Cockbox uses.

Using JSONRPC

I use this JSONRPC client, available as a composer package:

"require": {
    "fguillot/json-rpc": "@stable"
}

Basic usage is detailed on that page. Examples for the Electrum-specific stuff we're doing is below. I'm assuming you know how to handle the rest.

The Flow

Now we get into the meat of things.

  1. User wants to purchase something from you with BTC, and fills out a form or something
  2. You send an addrequest request via JSONRPC, which uses or creates a BTC address for you to use [1].
  3. User pays BTC to the address you give them
  4. A cronjob checks every minute the unspent transactions with listunspent
  5. For each unspent, find out if the btc_tx is already saved in the table detailed above
  6. If not, and height > 0 (meaning there is at least 1 confirmation), find the pending purchase, or in this case the user's service associated with the address
  7. If the BTC amount is >= what is due, complete the payment and do any processing you need to do before #7 if the payment is confirmed
  8. Save the payment into your payments table so it isn't processed again. If you don't need to do anything custom with your payment table (like expiry in my case) then you can do this
  9. Every day (or however often you want to get paid), run getbalance and check if there is a confirmed balance
  10. If so, payto your payment address with amount set to !. This will pay the max-fees [2]
  11. Take the response from that and broadcast it [3]

And that about does it. Anything more complicated than what's listed here is probably specific to your unique use case, so you should be able to take this to accept payments for whatever you're needing it for unless your needs are really specific.

[1]$client->execute('addrequest',['amount' => 99, 'force' => true]);
[2]$client->execute('payto',['destination' => '(address)', 'amount' => '!']);
[3]$client->execute('broadcast',['tx' => $response['hex']]);