Real-time auctions with HTML5, PayPal and Google App Engine - Part 1

By Peter Georgeson

Overview

This series of articles explores some interesting possibilities with PayPal's API, Google's App Engine and HTML5. These possibilities center around real-time mobile applications.

Specifically:

This is Part 1 and explains how to set up the basic application and describes some of the real-time possibilities provided by Google's new Channel API.

The Sample Application

A sample application containing all the code is available at GitHub. Download it and follow along.

The application demonstrates a real-time auction. An item will continue to be auctioned while new bids are received, but after a specified period of inactivity, the auction will end.

This implementation reflects "real" auctions and in certain circumstances could provide an exciting and compelling user experience. Potentially this application could also be used to augment real auctions by letting online bidders participate in such auctions.

Setting Up

Google App Engine (GAE)

To build this project, you will need an App Engine application. First, sign up for App Engine (requires a Google account).

You need at least Python 2.5 installed on your machine. GAE is not compatible with Python 3. Then download and install the App Engine SDK. Choose the Google App Engine SDK for your operating system.

This enables your application to be developed and run locally. The SDK also provides commands so you can upload your application to the App Engine servers.

Introducing the Real-Time Auction Application

Running Locally

To get a local copy of the application up and running:

All going well, you will see something like:

This display indicates that there are no items available for auction. To add items to be auctioned, browse to http://127.0.0.1:8080/add. This very simple interface is just for testing the application; it enables you to add some items to the database.

After one or more items have been added, return to http://127.0.0.1:8080/ where an auction should now be in progress.

In our real-time sample application, just one item is available for auction at a time. Any connected user may bid on this item. When the auction for that item ends, another item becomes available for auction. So although there is only ever one item for sale, all users are focussed on that one auction. Auctions run quickly, in "real-time" - before too long, there'll be a new item for sale.

You can now bid on the item being auctioned. Each new bid extends the length of the auction, however as soon as no bids are received for a specified amount of time, the auction closes and the highest bidder wins the auction. This encourages an exciting, fast-moving, real-time experience.

When the auction ends, the winner is notified and asked to confirm this before returning to the main auction screen.

Deploying Live

To deploy the application live:

About App Engine's Channel API

The Channel API provides a solution to the common problem of pushing notifications from the server to the client, sometimes referred to as Comet programming.

Implementing such solutions in a browser-compatible manner can be a headache. Typical solutions are:

With these issues, it's nice to be able to completely offload the implementation to Google's servers.

Using the Channel API

It's easy to include the Channel API in your application.

On the server...

1. Import the library


from google.appengine.api import channel

2. Create a channel

When a user first connects to the application, the server creates a token. This token must be made available to the HTML templates so that the client-side JavaScript can pick it up. In the sample application, we do this in main.py when the user requests the home page:


class Home(webapp.RequestHandler):
  def get(self):
    ...
    token = channel.create_channel(user.user_id())

    data = {
      'token': token,
    }
    path = os.path.join(os.path.dirname(__file__), 'templates/main.htm')
    self.response.out.write(template.render(path, data))
A channel requires a unique ID to identify it. In this application the user ID identifies the channel.

3. Push notifications

To send a message to a client, use the send_message method. In the application this is implemented in util.py:


  channel.send_message( user_id, message )

On the client...

1. Include the library


  <script src='/_ah/channel/jsapi'></script>

2. Create the channel

In main.htm, we take the server-provided token and use it to construct a Channel object. Then we register for the various events on the associated socket. Refer to static/main.js in the application.


  mgr.init( "{{ token }}" );
  ...
  init: function (token) {
    channel = new goog.appengine.Channel(token);
    socket = channel.open();
    socket.onopen = on_opened;
    socket.onmessage = on_message;
    socket.onerror = on_error;
    socket.onclose = on_close;

3. Receive messages

Messages can be processed by implementing the onmessage event. For example:


  on_message = function(m) {
    ...
    var parsed = JSON.parse( m.data );
    update_view( parsed );
  };

This section has covered the basics of using the Channel API for push notifications. Using this API, implementing push notification is impressively simple.

Implementing an auction process with the Channel API

Using this notification framework, a sample application was built with the following life-cycle:

When a user connects, open a channel

A channel is created using the user's ID as the channel key. The Channel API does not track open connections, or notify your application if a user disconnects. These must be managed by the application. The sample application uses the class model.Client to track connected clients. When a client connects, a new entry is generated in the Client table:


class Client( db.Model ):
  user = db.UserProperty()
  updated = db.DateTimeProperty(auto_now=True)

When a user bids, notify everyone

The client -> server communication mechanism uses a vanilla XMLHttpRequest object and does not use the Channel API. On the client:


  send = function(path, params) {
    if (params) {
      path += '?' + params;
    }
    var xhr = new XMLHttpRequest();
    xhr.open('POST', path, true);
    xhr.send();
  };

  send( "/bid", "key=" + key + "&amount=" + document.getElementById("new_bid").value );
This request is accepted by a normal webapp RequestHandler and is the mechanism used to send a bid to the server.

If the bid is legitimate, we want to notify all clients immediately. However, the Channel API does not include a broadcast mechanism - this must be managed by the application. In the sample app, Bid.post calls util.notify_all() to let all parties know the new auction state:


def notify_all( user, message ):
  ...
  default_state = simplejson.dumps( model.Item.state() )
  for client in model.Client.all():
    ...
    channel.send_message( client.user.user_id(), default_state )

The notify_all method creates a JSON representation of the auction state and sends it to all clients. On the client, the on_message method decodes the JSON object and uses it to update the view presented to the user.

Clients manage the countdown

As the remaining auction time counts down, we don't want to keep asking the server how long is left on the auction. The whole idea of the Channel API is to be able to write event-driven, rather than poll-driven, applications.

When a client receives a status update from the server, it calculates when the auction will expire. Based on this calculation, the client updates the on-screen countdown every second. The client will continue to do this until it receives further notification from the server. This keeps the application responsive and keeps load off the server.


update_view = function( state ) {
  ...
  expiry = new Date(new Date().getTime() + parseInt(state.remaining) * 1000 ); // calculate expiry time
  update_remaining();
}

update_remaining = function() {
  var remaining = Math.round( ( expiry.getTime() - new Date().getTime() ) / 1000 ); // calculate time remaining
  ...
  document.getElementById( "remaining" ).innerHTML = 
    "This auction will expire in " + Math.round( remaining ) + " second" + ( remaining == 1 ? "" : "s" );
  timer = setTimeout( update_remaining, 1000 ); // calculate again in 1s
  ...
}

When the auction ends, complete the transaction

The auction ends after a specified period of inactivity. On model.Item, the state is updated from INPROGRESS to FINISHED. If there was at least one bid, the item is considered sold and becomes SETTLED. This prevents the item from being re-auctioned.

In Part 2 we will enable users to settle transactions properly by integrating PayPal into the process.

Managing connected clients

The Channel API does not provide a way to track when a client has disconnected; this must be implemented by the application. The sample application has the client send a ping message at the end of each auction to signify that it is still connected. The server can then remove any client that has not "checked-in" with a ping.

In the sample application, we simply remove any client that has not sent a ping for 10 minutes:


  # remove old
  too_old = datetime.datetime.now() - datetime.timedelta( seconds=600 )
  items = Client.all().filter( "updated <", too_old )
  for item in items:
    item.delete()

Conclusion

This part of the tutorial concentrated on the interaction between browser and server using the Google App Engine Channel API and the notification model that this library enables. We demonstrated a "real-time" auction process and how this API could be utilized to provide an interesting purchase experience.

The next part will demonstrate how PayPal's adaptive payments API can be integrated into the sample application as the payment provider.

Further Details