diff --git a/bin/load_gen b/bin/load_gen
index b34ac3ff13c752f4c156000bffe3c1048ba25fd7..8acfdadefccd84350de509530fc36c0a6692e444 100755
--- a/bin/load_gen
+++ b/bin/load_gen
@@ -281,7 +281,7 @@ function poll() {
 }
 
 // always start out by creating a bunch of users
-var NUM_INITIAL_USERS = 5;
+var NUM_INITIAL_USERS = 30;
 
 // if an explicit target was specified, let's output what that means
 // in understandable terms
diff --git a/lib/load_gen/crypto.js b/lib/load_gen/crypto.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa4735336dfb153739f14587362631c65b9e9311
--- /dev/null
+++ b/lib/load_gen/crypto.js
@@ -0,0 +1,38 @@
+// a little tiny task focused wrapper around the excellent api exposed by
+// jwcrypto
+
+const
+userDB = require('./user_db.js'),
+jwk = require('jwcrypto/jwk.js'),
+jwt = require('jwcrypto/jwt.js'),
+vep = require('jwcrypto/vep.js');
+
+const NUM_KEYPAIRS = 5;
+
+process.stdout.write("generating " + NUM_KEYPAIRS +
+                     " keypairs to be (re)used during load generation: ");
+
+var keyPairs = [];
+
+while (keyPairs.length < NUM_KEYPAIRS)
+{
+  keyPairs.push(jwk.KeyPair.generate("DS", 256));
+  process.stdout.write(".");
+}
+
+process.stdout.write("\n");
+
+
+exports.getKeyPair = function() {
+  return userDB.any(keyPairs);
+};
+
+exports.getAssertion = function(obj) {
+  // XXX: we can memoize here at some point, returning existing assertions
+  // to reduce compute cost of loadgen client, to simulate more load
+  // on servers
+  var expirationDate = new Date(obj.now.getTime() + (2 * 60 * 1000));
+  var tok = new jwt.JWT(null, expirationDate, obj.audience);
+  var assertion = vep.bundleCertsAndAssertion([obj.cert], tok.sign(obj.secretKey));
+  return assertion;
+};
diff --git a/lib/load_gen/signin.js b/lib/load_gen/signin.js
index 49a0030e5677adccae74680ca47b9b157a639e0e..5695dddf2c7ba80eda0894b21f611ce5aa9d8914 100644
--- a/lib/load_gen/signin.js
+++ b/lib/load_gen/signin.js
@@ -41,7 +41,8 @@
 const
 wcli = require("../wsapi_client.js"),
 userdb = require("./user_db.js"),
-winston = require('winston');
+winston = require('winston'),
+crypto = require('./crypto');
 
 exports.startFunc = function(cfg, cb) {
 
@@ -54,12 +55,28 @@ exports.startFunc = function(cfg, cb) {
 
   var user = userdb.getExistingUser();
 
+  if (!user) {
+    winston.warn("can't achieve desired concurrency!  not enough users!");
+    return cb(false);
+  }
+
+  // unlock the user when we're done with them
+  cb = (function() {
+    var _cb = cb;
+    return function(x) {
+      userdb.releaseUser(user);
+      _cb(x);
+    };
+  })();
+
   // pick one of the user's emails that we'll use
   var email = userdb.any(user.emails);
 
   // pick one of the user's devices that we'll use
   var context = userdb.any(user.ctxs);
 
+  var origin = userdb.any(user.sites);
+
   // we want session_context to be called to simulate actual users.
   wcli.get(cfg, '/wsapi/session_context', context, undefined, function (r) {
     var serverTime;
@@ -83,10 +100,24 @@ exports.startFunc = function(cfg, cb) {
         return cb(false);
       }
 
-      // XXX: write me
-      process.exit(1);
+      var assertion = crypto.getAssertion({
+        now: serverTime,
+        secretKey: context.keys[email].keyPair.secretKey,
+        cert: context.keys[email].cert,
+        audience: origin,
+        email: email
+      });
+
+      wcli.post(cfg, '/verify', {}, {
+        audience: origin,
+        assertion: assertion
+      }, function (r) {
+        try {
+          cb(JSON.parse(r.body).status === 'okay');
+        } catch(e) {
+          return cb(false);
+        }
+      });
     });
   });
-
-  setTimeout(function() { cb(true); }, 10); 
 };
diff --git a/lib/load_gen/signup.js b/lib/load_gen/signup.js
index c508976254b5917deb98a1a074a181b72034eb74..ed402c6313ee37ca6e8fe29b319e9fcf9d1a6af7 100644
--- a/lib/load_gen/signup.js
+++ b/lib/load_gen/signup.js
@@ -38,7 +38,7 @@ const
 wcli = require("../wsapi_client.js"),
 userdb = require("./user_db.js"),
 winston = require('winston'),
-keys = require("./test_keys.js");
+crypto = require("./crypto.js");
 
 /* this file is the "signup" activity, which simulates the process of a new user
  * signing up for browserid. */
@@ -68,8 +68,8 @@ exports.startFunc = function(cfg, cb) {
   var user = userdb.getNewUser();
 
   if (!user) {
-    winston.warn("can't achieve desired concurrency!  not enough users!");
-    return cb(false);
+    winston.error(".getNewUser() should *never* return undefined!");
+    process.exit(1);
   }
 
   // unlock the user when we're done with them
@@ -109,8 +109,7 @@ exports.startFunc = function(cfg, cb) {
         r.body = JSON.parse(r.body);
         if (r.code !== 200 || r.body.success !== true) return cb(false);
 
-        // and now we should call registration status to complete the
-        // process
+        // and now let's certify the pubkey
         wcli.post(cfg, '/wsapi/cert_key', context, {
           email: email,
           pubkey: keypair.publicKey.serialize()
diff --git a/lib/load_gen/test_keys.js b/lib/load_gen/test_keys.js
deleted file mode 100644
index e6ccaf2c5338c8c7006914f472169c43fae216ec..0000000000000000000000000000000000000000
--- a/lib/load_gen/test_keys.js
+++ /dev/null
@@ -1,16 +0,0 @@
-var jwk = require('jwcrypto/jwk');
-
-const NUM_KEYPAIRS = 30;
-
-process.stdout.write("generating " + NUM_KEYPAIRS +
-                     " keypairs to be (re)used in load generation: ");
-
-exports.keyPairs = [];
-
-while (exports.keyPairs.length < NUM_KEYPAIRS)
-{
-  exports.keyPairs.push(jwk.KeyPair.generate("DS", 256));
-  process.stdout.write(".");
-}
-
-process.stdout.write("\n");
diff --git a/lib/load_gen/user_db.js b/lib/load_gen/user_db.js
index b7c09ce174ded5fc828f40f6daf3ed8f531c3bee..799e8a2435419e12ba4ceef7d8f45b853a6a9a90 100644
--- a/lib/load_gen/user_db.js
+++ b/lib/load_gen/user_db.js
@@ -40,7 +40,7 @@
 
 const
 secrets = require('../secrets.js'),
-keys = require("./test_keys.js");
+crypto = require("./crypto.js");
 
 // the grandiose database
 var users = [ ];
@@ -70,10 +70,10 @@ exports.getNewUser = function() {
     password: secrets.generate(10),
     // and four sites that they visit
     sites: [
-      secrets.generate(8) + "." + secrets.generate(3),
-      secrets.generate(8) + "." + secrets.generate(3),
-      secrets.generate(8) + "." + secrets.generate(3),
-      secrets.generate(8) + "." + secrets.generate(3)
+      'http://' + secrets.generate(8) + "." + secrets.generate(3),
+      'http://' + secrets.generate(8) + "." + secrets.generate(3),
+      'http://' + secrets.generate(8) + "." + secrets.generate(3),
+      'http://' + secrets.generate(8) + "." + secrets.generate(3)
     ],
     // and their device contexts (they have 2 devices on average)
     // key material is device specific
@@ -128,7 +128,7 @@ exports.addKeyToUserCtx = function(ctx, email) {
   // this is simulated.  it will need to be real to apply load to
   // the verifier, but that in turn will drastically increase the
   // cost of the application of load.  ho hum.
-  var k = exports.any(keys.keyPairs);
+  var k = crypto.getKeyPair();
   ctx.keys[email] = { keyPair: k };
   return k;
 };