Project 8: Penny Saver with Venmo
June 30, 2015 |
One of the reasons why I enjoy building things quickly with APIs is started because of hackathons. Or rather, I’ve developed the interest in APIs because of hackathons. For those who aren’t familiar with hackathons, developers and others get together and build ideas they come up with over a short period of time, usually a weekend. They show off their concepts and prototypes and are judged, for either prizes or glory.
Having been to over a hundred hackathons in the past five years has certainly helped with building things quickly. In fact, the 15 projects in 30 days challenge has literally felt like 8 back-to-back hackathons for me thus far. With hackathons happening every weekend, it wasn’t long before another hackathon happened.
This summer, ChallengePost is hosting eight online hackathons that are part of their Summer Jam series. The Financial Literacy hack is sponsored by Venmo, a fun platform where you can send money to and receive money from friends in a social network type experience. Their API is really easy to use, just like their website and app. I submitted a project to this competition, and thought I would share the process that I took as my eighth project.
I wanted to show the power of saving money, and how saving money for unexpected situations or causes like donations can be taught with a simple automatic activity. Often times, we might find ourselves short of funds and just need a few extra dollars to spend on something. This project will show how to proactively create those extra few dollars (or more, if desired)?
For my eighth project in my 15 projects in 30 days challenge, I am building Penny Saver. When authorized by a Venmo user, Penny Saver will check the account balance each day. For example, if there are any pennies in the amount, like 83 cents in the balance $4.83, Penny Saver will automatically deduct 83 cents from the account and keep it in a separate account. It might be helpful to also have a threshold amount that prevents the account balance from dropping below a certain amount.
If you’re an active Venmo user where each day you have pennies left over, this could end up being an extra $15 to $30 at the end of the month. At anytime, you can charge the penny saver account an amount up to the amount saved, and get these funds back in your account immediately. However, the goal would be to never need to take this money back and instead use it for a good cause, such as donating to a charity.
Venmo
You’ll need to set up an app to get an app id and app secret. Change the IP address to point to where you host the app so that the login flow will redirect back to the app correctly.
You will also need an access token for the Venmo account that the funds will be saved into and charged to when funds are requested by a contributor. Change the IP address of the webhook that will be notified when payments and charges are made to this account.
Setup
There are three files for this project. The AngularJS app uses index.html and pennysaver.js in the public folder to explain how the process works. app.js is the Node.js app that handles the OAuth callback and webhook endpoint for Venmo, and also handles checking accounts and collecting amounts each day.
If you haven’t installed Node.js or MongoDB, you can check out my other projects for instructions on how to do so.
// Filename: app.js
var PAY_FROM_TOKEN = '';
var CLIENT_ID = '';
var CLIENT_SECRET = '';
var PAY_FROM_USER_ID = '';
var MONGODB_ADDRESS = 'mongodb://127.0.0.1:27017/test';
var PORT = 80;
var NEXT_CHECK_INTERVAL = 24*60*60; // time in seconds between Venmo checks (default 1 day)
var DB_CHECK_INTERVAL = 60; // how often in seconds to check db for users to check Venmo.
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var url = require('url');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
var request = require('request');
mongoose.connect(MONGODB_ADDRESS);
app.use(bodyParser.json());
app.listen(PORT);
app.use(cookieParser());
app.use(session({
store: new MongoStore({ mongooseConnection: mongoose.connection }),
secret: '1234567890QWERTY'
}))
var UserModel = mongoose.model('Users', {
userid: String,
balance: String,
accessToken: String,
tokenExpires: Number,
refreshToken: String,
nextCheck: Number,
threshold: String
});
function ignoreCharge(paymentId, callback) {
request({
url: 'https://api.venmo.com/v1/payments/'+paymentId,
qs: {
access_token: PAY_FROM_TOKEN,
action: 'deny'
},
method: 'PUT',
},
function(err, response, body) {
if(err || response.statusCode != 200) {
console.log(err);
} else {
console.log('payment ignored');
if(callback)
callback();
}
});
}
function approveCharge(paymentId, callback) {
request({
url: 'https://api.venmo.com/v1/payments/'+paymentId,
qs: {
access_token: PAY_FROM_TOKEN,
action: 'approve'
},
method: 'PUT',
},
function(err, response, body) {
if(err || response.statusCode != 200) {
console.log(err);
} else {
console.log('payment approved');
if(callback)
callback();
}
});
}
function pay(from_token, to, amount, reason, callback) {
request({
url: 'https://api.venmo.com/v1/payments?access_token='+from_token,
qs: {
user_id: to,
note: reason,
amount: amount,
audience: 'private'
},
method: 'POST',
},
function(err, response, body) {
if(err || response.statusCode != 200) {
console.log(err);
} else {
console.log('payment made');
if(callback)
callback();
}
});
}
// To setup Venmo app, use /api/venmo as the webhook verification URL.
app.get('/api/venmo', function(req, res) {
var url_parts = url.parse(req.url, true);
var query = url_parts.query;
res.status(200);
res.setHeader("Content-Type", "text/plain");
res.send(query.venmo_challenge);
});
// Listens for the charge and pay events.
app.post('/api/venmo', function(req, res) {
UserModel.findOne({userid: req.body.data.actor.id}, function(err, user) {
if(req.body.type == 'payment.created' && req.body.data.action == 'charge') {
if(!err) {
var newBalance = parseFloat(user.balance) - parseFloat(req.body.data.amount);
if(newBalance 0}));
}
});
}
});
app.post('/api/threshold', function(req, res) {
res.setHeader("Content-Type", "application/json");
if(!req.session.userid) {
res.status(401);
res.send('{"error":"Not authenticated"}');
} else {
UserModel.update({userid: req.session.userid}, {threshold: req.body.threshold}, function(err, numAffected) {
if(err) {
res.status(401);
res.send('{"error":"Not authenticated"}');
} else {
var response = {threshold: req.body.threshold};
res.send(JSON.stringify(response));
}
});
}
});
app.get('/oauth', function(req, res) {
res.redirect('https://api.venmo.com/v1/oauth/authorize?client_id='+CLIENT_ID+'&scope=make_payments%20access_profile%20access_balance&response_type=code');
});
app.get('/oauth_callback', function(req, res) {
var url_parts = url.parse(req.url, true);
var query = url_parts.query;
req.session.access_token = query.code;
var qs = {
client_id: CLIENT_ID,
code: query.code,
client_secret: CLIENT_SECRET
}
request({
url: 'https://api.venmo.com/v1/oauth/access_token',
qs: qs,
method: 'POST',
},
function(err, response, body) {
if(err || response.statusCode != 200) {
console.log(err);
res.send('Unable to authenticate with Venmo.');
return;
} else {
var js = JSON.parse(body);
UserModel.findOne({userid: js.user.id}, function(err, user) {
if(!user) {
var timeNow = new Date();
UserModel.create({
userid: js.user.id,
balance: '0.00',
accessToken: js.access_token,
tokenExpires: timeNow.getTime()+js.expires_in,
refreshToken: js.refresh_token,
nextCheck: 0,
threshold: '1.00'
}, function(err, user) {
console.log(user);
req.session.userid = js.user.id;
res.redirect('/');
});
} else {
console.log(user);
req.session.userid = js.user.id;
res.redirect('/');
}
});
}
});
});
app.get('/logout', function(req, res) {
req.session.destroy();
res.redirect('/');
});
setInterval(function() {
var timeNow = new Date();
UserModel.find({nextCheck: {$lt: timeNow.getTime(), $gt: 0}}, function(err, users) {
for(var i in users) {
request({
url: 'https://api.venmo.com/v1/me?access_token='+users[i].accessToken,
method: 'GET',
},
function(error, response, body) {
if(error || response.statusCode != 200) {
return;
} else {
var js = JSON.parse(body);
var threshold = users[i].threshold;
var balance = parseFloat(js.data.balance);
var contribution = balance%1.00;
if((balance-contribution) < threshold || contribution == 0) {
return;
}
var newBalance = (parseFloat(users[i].balance)+contribution).toFixed(2);
pay(users[i].accessToken, PAY_FROM_USER_ID, contribution, 'Contributing $'+contribution.toFixed(2)+' for a total $'+newBalance);
UserModel.update({_id: users[i]._id}, {
balance: newBalance,
nextCheck: users[i].nextCheck+(NEXT_CHECK_INTERVAL*1000)
});
}
});
}
});
}, DB_CHECK_INTERVAL*1000);
app.use(express.static(__dirname + '/public'));
console.log('App listening on port '+PORT);
<!-- Filename: public/index.html -->
<!DOCTYPE html>
<html ng-app="pennySaverApp">
<head>
<meta charset="utf-8">
<title>Penny Saver</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.min.js"></script>
<script src="pennysaver.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
body {
padding-top: 20px;
padding-bottom: 40px;
}
.container-narrow {
margin: 0 auto;
max-width: 700px;
}
.container-narrow > hr {
margin: 30px 0;
}
.marketing {
margin: 60px 0;
}
.marketing p + h4 {
margin-top: 28px;
}
.result {
padding-top: 5px;
}
</style>
</head>
<body ng-controller="PennySaverCtrl">
<div class="container-narrow">
<div class="masthead">
<h3 class="muted">Penny Saver</h3>
</div>
<hr>
<div class="row-fluid marketing" ng-hide="balance">
<a href="/oauth">Sign in with Venmo</a>
</div>
<div class="row-fluid marketing" ng-show="balance">
<p>You have saved ${{balance}}</p>
<p><input type="button" ng-show="active" ng-click="stopService()" value="Stop Penny Saver" /> <input type="button" ng-hide="active" ng-click="startService()" value="Start Penny Saver" /></p>
<p>Threshold: <input type="text" ng-model="threshold" /> <input type="button" ng-click="changeThreshold()" value="Change" /></p>
</div>
<h2>How Penny Saver works</h2>
<p>Everyday, Penny Saver will deduct any pennies you have in your Venmo account balance. For example, if you have $14.34 in your Venmo account, Penny Saver will take 34 cents and put it in a Penny Saver account. These pennies start to add up!</p>
<p>You can always come back to this page to see your balance, or check the amount when Penny Saver deducts any pennies.</p>
<p>You can choose at what threshold Penny Saver will not deduct pennies. For example, if you choose $10.00, and your balance is $9.68, Penny Saver won't take the 68 cents. When your Venmo account balance goes above $10.00, Penny Saver will reactivate.</p>
<p>Saving more is easy! If you want to manually add money to your Penny Saver account, you can pay <b>pennysaver</b> from your Venmo account. </p>
<p>Finally, when you want your money back, you can charge <b>pennysaver</b> up to the total amount. You should see the money back in your Venmo account shortly. Just remember, if you don't do this step, you'll be on the way to saving for a rainy day, when you really need those pennies.</p>
<p><a href="http://venmo.com">Go to your Venmo account</a> or <a href="/logout">Logout of Penny Saver</a></p>
</div>
<hr />
<div>
Powered by <a href="http://venmo.com" target="_blank">Venmo</a>.
</div>
</body>
</html>
// Filename: public/pennysaver.js
angular.module('pennySaverApp', [])
.controller('PennySaverCtrl', function($scope, $http) {
$scope.balance = null;
$scope.active = false;
$scope.threshold = '';
$http.get('/api/user').success(function(result) {
$scope.balance = result.balance;
$scope.active = result.active;
$scope.threshold = result.threshold;
}).catch(function(error) {
console.log(error);
});
$scope.changeThreshold = function() {
$http.post('/api/threshold', {threshold: $scope.threshold}).success(function(result) {
$scope.threshold = result.threshold;
}).catch(function(error) {
console.log(error);
});
}
$scope.stopService = function() {
$http.post('/api/active', {active: false}).success(function(result) {
$scope.active = result.active;
}).catch(function(error) {
console.log(error);
});
}
$scope.startService = function() {
$http.post('/api/active', {active: true}).success(function(result) {
$scope.active = result.active;
}).catch(function(error) {
console.log(error);
});
}
});
Filename: package.json
{
"name": "penny-saver",
"description": "Penny Saver application for Node.js",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "*",
"mongoose": "*",
"url": "*",
"body-parser": "*",
"cookie-parser": "*",
"express-session": "*",
"connect-mongo": "*",
"request": "*"
}
}
To install the dependencies:
npm install
And start the app
nodejs app.js
Penny Saver
The first step is to authorize Penny Saver to access the account balance and make payments. Sign in with a Venmo account.
In the backend, the service will check the account balance daily. If the account balance has pennies, it will deduct the cents from the account by making a payment to another designated account.
Here’s what happens the next day when the balance is $2.65.
A second option that can be done manually by the user entails paying the designated account directly. If the user has extra money to save, they can pay it to the designated account and it will be added to the money saved.
At anytime, the user can charge the designated account an amount up to the total amount saved. If the amount is valid, it will be paid to the user who contributed the funds originally.
The designated account that holds the funds is intentionally left ambiguous. An interesting approach that I thought of after implementing this project is centered around a community of people, such as a church group. If a group of people contribute amounts regularly to this general fund, this fund could then be used to help a less fortunate member, or for a cause the group supports (such as a softball group who needs funds to replace their aging equipment).
In this case, specific members may be promoted to a committee that decides how funds are distributed. In this case, individual saved amounts could be reduced and used for the desired causes. Hopefully this inspires you to find new ways to donations.
That’s it for this project. Some ways you could extend this project could be:
- It’s great to save for yourself. How about for a community fund? A homeowner’s association who could use the collected funds to help out a member that can’t afford home repairs or other expenses in life?
- Add more options to customize the amounts saved. Maybe it’s a minimum of $5 and the change (83 cents) to make it $5.83. Customize how frequently the balance is checked and change saved, maybe once a week or once a month.
- Add an option to enable an amount to be donated to another account if the saved balanced reaches a certain amount. This way you can also feel great about donating money you end up not needing.
- Add a progress bar and a goal in the app to promote reaching a certain goal amount. Never hurts to add some gamification.
My two cents (eh, Post-mortem)
Penny Saver seems like a great idea. But there are some serious things to be considered. Since we’re talking about money, and in particular, holding money, this approaches an issue that I didn’t quite anticipate. If this project was to be live, it may necessitate adherence to regulatory fiat among other requirements in your jurisdiction. I’m not a lawyer so you should definitely seek guidance from a professional about such implications.
User trust and expectations are aspects which must be established. Many organizations are honest in how they operate. But since we’re talking about money, there’s always dishonest people out there. I hate to bring this project down to such a level, since it is supposed to empower users with the ability to save money for when they find themselves in a sticky situation and need that extra money, or empower users to help others with money they would otherwise waste. This project could be misused very quickly.
One last thing I want to mention is that saving money is a hard habit for some. We may only save coins in a coin jar if we use cash. In this digital age, those pennies are now a digit that make a whole amount (you don’t see five dollars, you see $5.83), instead of pesky weight in the pocket that become a nuisance to use again. It is a interesting mentality that I didn’t realize until I completed this project. So in a way, Penny Saver brings back the coin jar concept in a digital form. And if anyone has had a coin jar, it’s always fun to count up the change and cash it in months later. Like recyclables, it feels like extra money that you’ve earned (even though it was yours all along). That was my approach to Financial Literacy, teaching and empowering the user to save money for a rainy day or for a good cause.
Source Code
You can find the repo on GitHub.
15 Projects in 30 Days Challenge
This blog post is part of my 15 projects in 30 days challenge. I’m hacking together 15 projects with different APIs, services, and technologies that I’ve had little to no exposure to. If my code isn’t completely efficient or accurate, please understand it isn’t meant to be complete and bulletproof. When something is left out, I try to mention it. Reach out to me and kindly teach me if I go towards the dark side.
This challenge serves a couple of purposes. First, I’ve always enjoyed hacking new things together and using APIs. And I haven’t had the chance (more like a reason) to dive in head first with things like AngularJS, NodeJS, and Venmo. This project demonstrated AngularJS, Node.js, and Venmo.