When I first started KinkBNB in 2015, I bought an off-the-shelf AirBNB clone codebase to build the first version on. One of the things we had to do was hook it up to Google Places API with an API key. It was amazing to me how it transformed the dead piece of software we’d gotten from an off-shore programming company into something that seemed to work. It was only after we got into operation that the problem became clear — despite the fact that the software worked, it didn’t actually monetize our traffic because AirBNB’s business model only works at scale. It took us two different packages to come to this realization in 2017.
This became the impetus for KinkBNB Mark III — a completely custom written piece of software. Based on my research I chose the Django framework to write this version. There were a few advantages to this. I’m a pretty hands on guy and I know python like the back of my hand from writing IRC bots in the late 90’s. It allowed me to keep the old databases and userbase intact, and that was pretty cool. But above all I figured out how to drop this Google API into our codebase. This gives KinkBNB the spark of life it would not have otherwise — you can type in an address and it will not only autocomplete based on your location, it will give us the GPS coordinates to do a Point based search in Haystack.
I’ve revisited this particular API a few times for KinkBNB. I have used it in other projects for clients as well. It’s pretty good for building dealer locators as well as finding the nearest restaurants to your sex dungeon. I am still all about this API. It’s Google that I have a problem with.
Specifically, their non-communication in making changes. I don’t have much of a reason to pay attention to the backend of this API in the Google Cloud Console — at least I didn’t before June of 2018. It was then that Google decided they wanted to make money off this API and started charging for it. We didn’t actually notice this for a few reasons.
We’re in a very weird area with KinkBNB. We get a lot of traffic and we are profitable, but it’s that weird kind of profitability you get in that first year of profitability. Namely, everything affects your bottom line. We exhibit at the Folsom Street Fair and it’s not an inexpensive proposition — but this year we paid for it completely with profits. It was about this time that I started getting notices in my personal mail about our credit card being declined for small payments.
Of course, several years ago when I wasn’t thinking I’d signed up for this with my personal email. I shrugged it off until after the Fair, how bad could it be?
This photo by the SF Chronicle captures me (left, in my trusty hat) on my phone trying to figure out why Google is charging us so much despite all the stuff going on around me.
As it turned out, it was pretty bad. Not only were we badly behind, for some reason Google had given us a credit or something going forward several months. We were pulling a LOT of requests and at their full pricing tier. My best guess is they knew folks would need time to fix it, but in the meantime here is $500 a month credit for several months to make your bills not pile up. I sat up and took notice when that credit stopped.
Here’s the fun code portion of the article! The very basic call to Places is simple. You grab an API key from the API Dashboard and throw up this piece of HTML and Javascript which is currently on their website and VOILA — you have a map.
<!DOCTYPE html>
<html>
<head>
<title>Simple Map</title>
<meta name="viewport" content="initial-scale=1.0">
<meta charset="utf-8">
<style>
/* Always set the map height explicitly to define the size of the div
* element that contains the map. */
#map {
height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
var map;
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: -34.397, lng: 150.644},
zoom: 8
});
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
async defer></script>
</body>
</html>
Now, the piece of code to focus on is that maps.googleapis.com call at the end.
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
async defer></script>
That piece of code right there is the very BASE call. You can use that as the basis of every call to Places API, whether it is the maps or the geocoding or the autocomplete. Notice also that is only has two values being passed — the API key and the callback. Folks, that piece of code controls how much you are being charged. If you just call it with your API key, it charges you the maximum amount they can, per request — because ostensibly it returns all the data in their databases about what’s near you or in your coordinates.
Google Maps and Places now has a new pricing structure according to their website. If you want just basic address capabilities, that’s a very low rate. But if you grab everything in their API, the bill piles up VERY quickly. Luckily you can staunch the flow of money pretty quickly.
December is my traditional month to do a good code sprint on KinkBNB. This year I had to fix this problem ASAP, and code a new GPS enabled search function for our user directory. I took the time to figure out and understand what was going on. It takes a few days sometimes to figure out if your code actually worked when you have an external API, and I can report that the following problem was definitely fixed by some simple additions to the Places API call lines.
First off — you need to make your Google Places API calls priced by session instead of by every API call. Luckily in Django it’s just a matter of adding a value to your API call.
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?libraries=places&&v=3.34&key={{ GOOGLE_MAPS_API_KEY }}&sessiontoken={{request.session.session_key}}"></script>
Next, you need to limit what data you are calling from Places. I wrote the following Javascript function that runs when the page loads — it lets me access the location data from the browser, stores it for the user who is logged in, and sets up the Autocompleting search bar on our search pages.
{% block head_js %}
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?libraries=places&&v=3.34&key={{ GOOGLE_MAPS_API_KEY }}&sessiontoken={{request.session.session_key}}"></script>
<script type="text/javascript">
function showError(error) {
switch(error.code) {
case error.PERMISSION_DENIED:
alert("User denied the request for Geolocation.");
window.location.href = "https://www.kinkbnb.com/explore/";
break;
case error.POSITION_UNAVAILABLE:
alert("Location information is unavailable.");
window.location.href = "https://www.kinkbnb.com/explore/";
break;
case error.TIMEOUT:
alert("The request to get user location timed out.");
window.location.href = "https://www.kinkbnb.com/explore/";
break;
case error.UNKNOWN_ERROR:
alert( "An unknown error occurred.");
window.location.href = "https://www.kinkbnb.com/explore/";
break;
}
}
function initialize() {
var input = document.getElementById('geocomplete');
if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(function(position) {
var pos = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
document.getElementById('id_lat').value = position.coords.latitude;
document.getElementById('id_lng').value = position.coords.longitude;
$.ajax({
url : '/kinksters/{{ request.user.id }}/gps/',
type : "POST",
data : {
'csrfmiddlewaretoken' : "{{ csrf_token }}",
'lat':position.coords.latitude,
'lng':position.coords.longitude,
},
success : function(result) {}
},showError);
}, function() {
});
} else {
// Browser doesn't support Geolocation
window.location.href = "https://www.kinkbnb.com/explore/";
}
var autocomplete = new google.maps.places.Autocomplete(input,{fields: ["address_components", "geometry"],
types: ["geocode"],});
google.maps.event.addListener(autocomplete, 'place_changed', function () {
var place = autocomplete.getPlace({placeIdOnly: true});
var lat = place.geometry.location.lat();
var long = place.geometry.location.lng()
document.getElementById('id_lat').value = place.geometry.location.lat();
document.getElementById('id_lng').value = place.geometry.location.lng();
});
}
google.maps.event.addDomListener(window, 'load', initialize);
</script>
{% endblock head_js %}
As in the previous little codeblock, there are specific lines you need to pay attention to here.
var autocomplete = new google.maps.places.Autocomplete(input,{fields: ["address_components", "geometry"],
types: ["geocode"],});
var place = autocomplete.getPlace({placeIdOnly: true});
This is how you limit what data you get back from Places API. You are limiting your fields to geometry and address data, and the type of call you are making is “geocode”. The second line is to specify that you don’t want all the Places Details in your call. If you leave these lines in without specifying the code type, placeIDOnly, or the address fields, then Google will charge you at the full data rate for their Places Details API — and this is what took a few days to figure out after I figured out to make all API calls part of a session. Naturally if you want to get more data back from them this is where you do it as well — but all the tiers cost more on top of what Google is charging as a base. Per Google’s website, here are the available fields and pricing tiers:
Basic
The Basic category includes the following fields:address_component, adr_address, alt_id, formatted_address, geometry, icon, id, name, permanently_closed, photo, place_id, plus_code, scope, type, url, utc_offset, vicinity
Contact
The Contact category includes the following fields:formatted_phone_number, international_phone_number, opening_hours, website
Atmosphere
The Atmosphere category includes the following fields: price_level, rating, review
If you want to include reviews and ratings, then you can do this. Just be warned that this is charged at the maximum rate possible. If you get between 20,000 and 30,000 users a month on your search bar and every API call is at maximum rate, that’s going to add about $1000+ a month to your bill. The minimum rate is much more palatable so far.
I had virtually no help from Google to solve this. I say virtually, because I actually used Google to search the internet for the answers — and those were not published officially by Google except in their dry documentation that just assumes you’re starting from scratch. I didn’t get or notice an email warning about the change in pricing, nor did I find anything explaining this change and how it would affect older customers. It seems to be a trick to get as much surprise revenue as possible before their users fixed it. Nobody would be that evil, would they?
And so it comes full circle to my article’s title. Google used to have a motto. It was “Don’t be evil.” Unfortunately they seem to have left this motto by the wayside, sometime around the time they bought that company that builds scary military robots. Surprising people with pricing changes is about as evil as it gets. I hope someone at Google sees this and tries to do better next time they want to implement a large change that will affect small businesses like mine if we don’t jump right on fixing our code.