9.2 KiB
Daily SMS Spending Summaries With Plaid and Twilio
Your bank lets you set up SMS alerts for various triggers; it might even give you the option of receiving periodic spending summaries (mine doesn't!). But what about a daily SMS summary of your spending across all your accounts? This is harder to come by, so let's roll our own:
Pre-work
- Nab a sandbox account from Plaid, and put those credentials into
PLAID_CLIENT_ID,PLAID_SECRET,PLAID_PUBLIC_KEYenvironment variables. While you're at it, ask for access the development API (it'll take a few days). For now, though, create an env varPLAID_ENVand set it to 'sandbox'.
export PLAID_CLIENT_ID='somechars1234'
export PLAID_PUBLIC_KEY='somemorechars1234'
export PLAID_SECRET='somesecretchars1234'
export PLAID_ENV='sandbox'
- Clone the quickstart for Plaid and reference those environment variables in the Python quickstart's
server.py.
PLAID_CLIENT_ID = os.getenv('PLAID_CLIENT_ID')
PLAID_SECRET = os.getenv('PLAID_SECRET')
PLAID_PUBLIC_KEY = os.getenv('PLAID_PUBLIC_KEY')
PLAID_ENV = os.getenv('PLAID_ENV')
-
Run
server.py(Python 2 only 😦) and log into Chase with the test credentials ("user_good" and "pass_good" as of 5/24/2017). It prints the access token to your terminal: Grab that and put it into aCHASE_ACCESS_TOKENenvironment variable. Repeat this for Bank of America and put that access token intoBOFA_ACCESS_TOKEN. -
Grab your Twilio credentials and Twilio incoming phone number and make sure those are available as environment variables too (see below).
-
Put your cell phone number in an environment variable as
MY_CELL.
export CHASE_ACCESS_TOKEN='access-sandbox-someprettysecretchars1234'
export BOFA_ACCESS_TOKEN='access-sandbox-somemoreprettysecretchars1234'
export TWILIO_SID='somechars1234'
export TWILIO_TOKEN='somesecretchars1234'
export MY_TWILIO_NUM='+11111111111'
export MY_CELL='+12222222222'
- Install just a couple of dependencies:
$ pip install python-plaid twilio
Get Some Transactions
Make yourself a plaid.Client instance and grab some transactions from Chase:
import os
from typing import List
# twilio.rest has a Client too, so let's avoid a namespace collision
from plaid import Client as PlaidClient
plaid_client = PlaidClient(client_id=os.getenv('PLAID_CLIENT_ID'), secret=os.getenv('PLAID_SECRET'),
public_key=os.getenv('PLAID_PUBLIC_KEY'), environment=os.getenv('PLAID_ENV'))
def get_some_transactions(access_token: str, start_date: str, end_date: str) -> List[dict]:
return plaid_client.Transactions.get(access_token, start_date, end_date)
Inspecting the output of get_some_transactions, we see that there are multiple accounts, 337 transactions among them, but only 100 transactions returned from this API call. Two of these accounts are for savings, so presumably they're only going to have transfers rather than purchases:
>>> from get_some_transactions_v1 import get_some_transactions
>>> import os
>>> some_transactions = get_some_transactions(os.getenv('CHASE_ACCESS_TOKEN'), '1972-01-01', '2017-05-26'))
>>> some_transactions['total_transactions']
337
>>> from pprint import pprint
>>> pprint(some_transactions['accounts'])
[{'account_id': 'qwp96Z11b5IBKVMl8XvLSkJXjgj6ZxIXX3o79',
'name': 'Plaid Checking',
'subtype': 'checking'
...},
{'account_id': 'Kk9ZL7NN4wSX3lR9evV8f9P4GVGk3BF33QnAM',
'name': 'Plaid Saving',
'subtype': 'savings'
...},
{'account_id': 'rEy96MWWgXukrnBW4yVphv7yl3lznosBBzo6n',
'name': 'Plaid CD',
'subtype': 'cd'
...}
{'account_id': '9rNKomMMdWTvVL4X9RP6UKb4qEqng1uJJ6nQw',
'name': 'Plaid Credit Card',
'subtype': 'credit'
...}]
>>> len(some_transactions['transactions'])
100
Get The Right Transactions
Looking at the transactions themselves, we see that there is a category field which sometimes has a list of values, sometimes None. What are the categories?
>>> some_transactions['transactions'].keys()
dict_keys(['account_id', 'account_owner', 'amount', 'category', 'category_id', 'date', 'location', 'name', 'payment_meta', 'pending', 'pending_transaction_id', 'transaction_id', 'transaction_type'])
>>> {category for transaction in some_transactions['transactions'] if some_transactions['category'] for category in trans['category']}
{'Airlines and Aviation Services',
'Coffee Shop',
'Credit Card',
'Deposit',
'Fast Food',
'Food and Drink',
'Payment',
'Restaurants',
'Transfer',
'Travel'}
Among the categories there are "Transfer", "Credit Card", and "Deposit": These aren't going to be useful in gleaning spending activity, so we'll refactor our get_some_transactions function to
- skip transactions with those categories.
- skip accounts with a subtype of "savings" or "cd".
Let's also
- make sure to get all available transactions by using pagination.
- just return transactions.
import math
...
# https://plaid.com/docs/api/#transactions
MAX_TRANSACTIONS_PER_PAGE = 500
OMIT_CATEGORIES = ["Transfer", "Credit Card", "Deposit"]
def get_some_transactions(access_token: str, start_date: str, end_date: str) -> List[dict]:
account_ids = [account['account_id'] for account in plaid_client.Accounts.get(access_token)['accounts']
if account['subtype'] not in ['cd', 'savings']]
num_available_transactions = plaid_client.Transactions.get(access_token, start_date, end_date,
account_ids=account_ids)['total_transactions']
num_pages = math.ceil(num_available_transactions / MAX_TRANSACTIONS_PER_PAGE)
transactions = []
for page_num in range(num_pages):
transactions += [transaction
for transaction in plaid_client.Transactions.get(access_token, start_date, end_date,
account_ids=account_ids,
offset=page_num * MAX_TRANSACTIONS_PER_PAGE,
count=MAX_TRANSACTIONS_PER_PAGE)['transactions']
if transaction['category'] is None
or not any(category in OMIT_CATEGORIES
for category in transaction['category'])]
return transactions
Now there are just 265 transactions. Are any of them negative?
>>> len(some_transactions)
265
>>> pprint([transaction for transaction in some_transactions if transaction['amount'] < 0])
[{'amount': -500,
'category': ['Travel', 'Airlines and Aviation Services'],
'name': 'United Airlines',
'transaction_type': 'special',
...},
...]
Okay, that seems legit -- airfare refund, I guess. Let's keep negative items in.
Pulling it All Together
Now let's get all the transactions from yesterday, making sure to pull them from both accounts.
...
def get_yesterdays_transactions() -> List[dict]:
yesterday = ('2017-05-16' if os.getenv('PLAID_ENV') == 'sandbox'
else (datetime.date.today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d'))
transactions = []
for access_id in [os.getenv('CHASE_ACCESS_TOKEN'), os.getenv('BOFA_ACCESS_TOKEN')]:
transactions += get_transactions_from_multiple_accounts(access_id, yesterday, yesterday)
return transactions
As of 5/24/2017, the most recent transactions available in these sandbox accounts are from 5/16/17: Hence, the hardcoded yesterday value above.
Let's send an SMS to ourselves with the total spent yesterday!
...
from twilio.rest import Client as TwilioClient
from get_yesterdays import get_yesterdays
twilio_client = TwilioClient(os.getenv('TWILIO_SID'), os.getenv('TWILIO_TOKEN'))
def send_summary(transactions: List[dict]) -> None:
total_spent = sum(transaction['amount'] for transaction in transactions)
message = f'You spent ${total_spent} yesterday. 💸'
twilio_client.api.account.messages.create(to=os.getenv('MY_CELL'), from_=os.getenv('MY_TWILIO_NUM'), body=message)
if __name__ == "__main__":
send_summary(get_yesterdays())
$ python get_send_summary.py
Voila!