It's like regular money, but with more math. |
This is how it works:
First, a resource is requested which requires payment. The middleware checks with the datastore to see if the requesting client has an existing record for this resource.
If no existing record is found, it means the client has never attempted to access this resource before. The middleware uses the address generator to create a new payment address record and gives it to the datastore. The client is returned the 402 response along with the special payment instruction headers.
Alternatively, the datastore might find a record for this resource and client. That means the client has been here before. The record returned by the datastore will indicate whether or not the payment is complete. If the payment has been made, the middleware allows normal access. If payment has not been made, the middleware returns the 402 response to the client with the special payment instruction headers. The data for those headers comes out of the payment record.
At any time, an external Bitcoin processing system can determine that payment has been made to one of the addresses generated by the address generator. It sends data on the payment transaction to the payment callback resource which looks up the record in the datastore and marks it as paid.
I used the Coinbase Merchant API as the external Bitcoin processing system. When I create addresses with Coinbase, I can specify a callback URL for each one and Coinbase will POST some data to it when payment is made. Each address gets a unique URL so my system can look up the payment record that matches it. I am using the official Coinbase Node module in my app.
Here is a basic outline of the app:
var express = require("express"); var app = express(); // mock purchase record var demoRecord = { recordId: "12345", code: "product-code", paid: false, cost: 123.45, address: "bitcoinaddress", secret: "itsasecret" }; // mock datastore object var datastore = { findRecord: function(req, productCode, callback) { callback("12345"); }, checkPaidStatus: function(recordId, callback) { callback(demoRecord); }, newRecord: function(data) { // store the record }, findRecordBySecret: function(secret, callback) { if (secret === demoRecord.secret) { callback(demoRecord); } else { callback(null); } } }; function newBitcoinAddress(productCode, callback) { callback(null, demoRecord); } function send402(res, data) { res.setHeader("X-Payment-Types-Accepted", "Bitcoin"); res.setHeader("X-Payment-Address-Bitcoin", data.address); res.setHeader("X-Payment-Amount-Bitcoin", data.cost); res.sendStatus(402); } function paywallMiddleware(req, res, next) { // let's use the URL as the product code, // since that's really what is being sold var productCode = req.path; datastore.findRecord(req, productCode, function(recordId) { if (recordId) { // found a record, which means that this client // has a payment address and purchase record already. // now check to see if it has been paid. datastore.checkPaidStatus(recordId, function(data) { if (data.paid) { next(); // all paid, move along... } else { // respond with the payment address that already exists send402(res, data); } }); } else { // no record found, which means a new Bitcoin // address and purchase record must be created. newBitcoinAddress(productCode, function(err, data) { if (err) { next(err); } else { datastore.newRecord(data); send402(res, data); } }); } }); } app.get("/free", function(req, res, next) { res.send("free content"); }); app.get("/paid", paywallMiddleware, function(req, res, next) { res.send("paid content"); }); app.get("/callback", function(req, res, next) { var secretCode = req.query.secret; datastore.findRecordBySecret(secretCode, function(record) { if (record) { record.paid = true; } }); res.sendStatus(200); }); // ------------ start listening var server = app.listen(3000, function() { console.log("listening on port %d", server.address().port); });
That looks like a lot of code, but it's not really that complicated. Near the top, you see I have some mock objects for the demo. In a real scenario, your datastore object would access an RDBMS or the file system to store and retrieve actual purchase records. Here, though, a single fake record and some stub functions will suffice. The newBitcoinAddress function is also a stub - in the real app, this is where I use the Coinbase API to create addresses and associated purchase records each with their own unique secret code as part of the callback URL. That secret code should never be returned to the user, but needs to be stored so the record can be retrieved later in the callback handler. The send402 function is just a utility function that sets the special response headers and sends the 402 code.
The paywallMiddleware function is the implementation of the algorithm I described above. It uses the mock datastore and address generation function.
The routes at the bottom allow testing the system. Launch the app and point your browser to http://localhost:3000/free. You should get the free content. Now try http://localhost:3000/paid. You should get a 402 response. This is because the /paid route includes the paywallMiddleware. To get access, you'll need to pay.
This demo doesn't require any real Bitcoin transactions to take place. To simulate the callback coming from Coinbase, I included a /callback route that is a GET, which makes it easy to test from your browser. In real life, this will be a POST. To simulate sending payment to the address, you just have to go to http://localhost:3000/callback?secret=itsasecret. When you do, you should get a simple OK response...but on the server side, the demo record was updated to paid because the secret code matched. Now you can go back to http://localhost:3000/paid and get the paid content. If you had used a different (incorrect) value for secret, the /paid resource would not have been unlocked.
Next week I'll go into a little more detail about how I use the Coinbase API. My hope is that someday more content creators such as myself can use functions like this for micropayments in place of running advertisements on their sites.
But that's just a dream right now. How long will it take to become reality? A few moths? I mean, months?
Amphibian.com comic for 22 May 2015 |
No comments:
Post a Comment