LTV and Cost of acquisition

I a few years back I read a story or blog post stating that not all paid app users are the same. The story went something like this: you can buy two users at $2.00 but one user would bring you back $3.00 for his/her lifetime of playing while the other users would only bring you back $1.98. With one user you would make money and with the other, you would lose money.

What if you could track each user as they install your game all the way through to when they stop playing your game? You could then know for example that the average lifetime value (LTV) of a woman acquired via Facebook ads is higher than the same type of user acquired via Google Ads. There are services / APIs that do just that and after reading the article, I figured this was the ticket to actually making some money with my hobby. Because I thought that if I can spend $1.00 and get even $1.30 back then I can make a business out of that.

I tried two services both available to Corona users and both free to a certain point. One of them was AppsFlyer. They charge .05 per non-organic installs. The other service that was available to me was Tenjin. For the free plan, you get 1M events. The AppsFlyer pricing seemed at first a very steep price especially when I usually bid for users at .15 or lower, but I figured if this scheme actually makes me money it would pay for itself. I can give 0.05 cents per install to AppsFlyer if that install brings me in .30 or even .20.  Interesting enoughAppsFlyer has never charged me a single cent. I don’t know if my volume is so low that it would cost them more to send an e-mail or they only charge you after you reach a certain threshold.

Both APIs are very easy to set up and both customer services are excellent. I think Tenjin customer service is a bit better, but it might be just the size of the operation. With Tenjin, I felt that I was in contact with the actual team running the show. This is just my perception and maybe it was an illusion but that is how it felt. When you are paying exactly $0 dollars for a service and you getting e-mails and phone calls from the actual engineering team trying to help you out, it feels special.

If this was one of those over-hyped marketing blogs, this is where I tell you how well it all worked out and how I made my first million. The reality was completely different. I think both services are excellent and I plan to add Tenjin back into the mix soon.

Here is the problem I encountered and maybe there is a simple solution, but one I could never figure out. I would spend $5.00 a day on Facebook for iOS version of cuadros and according to Facebook and the services that would result in 15 installs. Sometimes one would say 14 and another would say 13. When the services would calculate the LTV of those 15 users, I would undoubtedly lose money. According to the services they would bring in let us say $3.00. So I just lost $2.00 on those 15 users on that day.

When you looked at the revenue for any particular day for cuadros across platform the revenue would be far more than $5.00. Usually between $10 and $20 and sometimes in the low $70s. Of course, there were more users than the 15 installs that were attributed to Facebook. The Android version would get an additional x organic installs and the iOS version would get an additional x organic installs. Those were “free installs” because apparently, I didn’t pay for them.

So why pay for the paid users? I am losing $2.00 a day on the paid users! Guess what disappeared when I stopped paying for those 15 users, the rest of the organic installs not only on iOS but also on Android. I have run this experiment multiple times. Even advertising on Android will result in some installs occurring on iOS. Advertising on a completely different app in your portfolio might cause organic installs in other apps in your portfolio. Maybe it should have been obvious, but advertising is like dropping a drop of ink in water it disperses and touches many molecules.

In no way does this mean that the services are useless, they provide other data that should be extremely useful in more capable hands, and maybe this dispersion doesn’t matter with bigger budgets.










I love OneSignal. For those that don’t know they provide mobile push notification for your apps. They recently also added e-mail notifications. There are other providers that do this, but I have not found any that are low-cost or free. OneSignal is free. Completely free. It doesn’t matter how small or large you are. I never question a company revenue methods. I was just glad it was free.

I used them for years for cuadros. When the GDPR deadline approached I looked at what kind of data they collected and for what purpose to add it to the consent dialog. And then I realized that the reason that it is free, is because they collect data to sell. As with most of these privacy policies, they amount to a bunch of maybe will collect this data or that data statements.  I didn’t feel comfortable adding all that information to the consent dialog and I didn’t think anybody would consent to the random list of possible data elements.

Fast forward to a few weeks ago. The Corona plugin is a native plugin meaning that OneSignal wrapped up their iOS and Android plugin in a Lua facade. The Corona plugin has not been updated in a while which resulted in a nasty bug when building for Oreo (probably Pie also).

Since it appeared that it was not going to be fixed in a reasonable timeframe I decided to use their REST API to create a fully Lua solution. I also figured that I could limit or at least know exactly what data I would be sending and this would make the process of being GDPR compliant easier.

Last week was a long weekend, and I didn’t have any day work spill over to the weekend. So I took the opportunity to write it. I am excited that I’ll be able to inform users of new features and other random things. Don’t worry, I know less is more when we talk about push notification.

on the shoulders of giants

After working for two years on a C++ gaming platform the company that provided the tools stopped supporting the engine. It was time to look for another engine. I was amazed at how many options were available.

I had a few simple asks: I wanted to keep using the same programming language. I didn’t want to spend time learning a new language when I could spend that time working on games. I wanted the company to have an active community where if a problem came up other people would be able to help. I wanted ready available and easy to integrate libraries for everything I could possibly need. I wanted to be able to build in the cloud on somebody else’s servers.

The last two parts were my most important ask because I was extremely tired of supporting plugins (my previous engine) and dealing with the minutia of maintaining build configurations.

To evaluate the game engines I decided that I was going to use my next project as a measuring stick. So about 2 years ago, I decided I was going to create a casual puzzle game (the type of games I play). Since I didn’t know any of these engines, I was looking for a well-crafted puzzle template.

I wanted a mostly made backend system that I could configure and maybe write a few scripts. I didn’t want to code this from scratch and then maintain it.

I wanted a monetization plugin that was maintained by the company. And once the app grew, I wanted some sort of mediation plugin. I didn’t want to roll my own mediation with a bunch of if/then clauses.

I wanted a way to track installs and a few events.

In general, I didn’t want to start on the ground floor. I wanted to start on the shoulders of the biggest giant that I could find. Corona was and is that giant.

I was and I am still amazed that Corona had in most cases more than one option for whatever need I can think of. Corona has more than a dozen ad providers and at least a few providers of surveys. Corona has at least 5 options for analytics. Corona has at least 3 options for backend system.

As far as a community, Corona forums are always filled with people willing to help.

And yes I know that Corona isn’t C++, but Lua was so easy to pick-up. And yes you don’t have to point out that I did not come up with the term “shoulder of giants”. It was first written to my knowledge by Sir Newton.

The result of my efforts almost 2 years ago is cuadros. It has almost 50K installs as of this writing and about 800 unique users a day. I am using the Admob Corona plugin for banners and interstitials ads. For rewarded ads, I use NextApp, Ironsource and hopefully Vungle soon. I am using Gamespark as a backend. I have used Tenjin and Flurry to track installs and events. I use one signal to send notification on all three platforms. For all these plugins, I don’t have to install or copy or add dependencies. Everything is done for me. Because I am not a build engineer or a server admin or a desktop configurator. I am a game developer.





Gamehouse and QuickLua

The nice people at MadeWithMarmalade are running a promotion with gamehouse where if you integrate their game network promotion api you win money and a Marmalade license extension. There are some stiff requirements (at least for me and my apps) to get the incentives, but I decided to integrate a couple of my apps just to see what it was like (and because I am on vacation with nothing to code). What I found out is that unlike most tap or impression exchanges, gamehouse will show your app thousands a time a day for free (at least during their beta period).

Two of the apps I integrated gamehouse with are written in C++ (what all the instructions are written for), but one of my app I used Marmalade’s Lua api. What follows is how I got the gamehouse extension working with my Lua project.

1. Download the gamehouse Marmalade extension from here:

2. Once the sdk is unzipped copy the gamehouse folder to the extension folder inside your marmalade folder. In my case that is: C:\Marmalade\7.1\extensions.

3. Modify your project .mkb to include the gamehouse extension in the sub-project section. Your subproject section should look like this (unless you have added other projects):

s3e-data-dir = resources
app-icf = ‘resources/common.icf,resources/app.icf’


4. Download the files located here:

Place the header file in the header folder. On my machine it is located here: C:\Marmalade\7.1\quick\include.

And the source file here: C:\Marmalade\7.1\quick\source

5. Modify quickuser.mkf located here: C:\Marmalade\7.1\quick

By adding  the two files you just added. It should look something like this after your changes.

includepath .


#Gamehouse c++ files.

6. Modify quickuser_tolua.pkg also located here: C:\Marmalade\7.1\quick by adding the reference to the header file. Your file should look something like this:

// Mark-up in header files
$cfile “quickuser.h”
$cfile “include/CGGameHouse.h”

7. Run the quickuser_tolua.bat file. According to the documentation it should be located here: C:\Marmalade\7.1\quick. Unfortunately I have not seen it in a few releases. If you don’t have it either copy it from the same folder but the 6.4 install (the last version that had it for me).

8. At this point you can either open the quick_prebuilt.mkb or any of your Quick project in Visual Studio and compile it using the x86 Debug target.

9. From within your Lua project now you should be able to initialize gamehouse by calling:


and request the display of the ad by calling:


10. Remember to add the required items for the particular platform you are deploying it to. For example for Android you need to add the required activity to the android manifest file. Instructions can be found in the gamehouse integration document found in this thread:

That is all, if you run into problems let me know and I’ll try to help. I am back to bill paying work after the new year.

InMobi and ad size support for IwGameAds.

If anybody has bothered (highly unlikely) to download and install any of my games in Blackberry Appworld they would have noticed that at times you will get an ad that takes over the entire screen of the game. This happens even if you are in the middle of playing the game. Unfortunately until this point IwGameAds did not have a mechanism to be able to request an ad size. With some providers like mmedia this is not a problem because you define the size with you define the slot.

Again I have made changes to IwGame to support ad sizes. These are the first batch of ad sizes supported: 168×28, 216×36, 300×50, 300×250, 728×90, 320×480, and 320×50. Up to this point I have only tested and modified (where needed) 2 providers: mmedia and Inneractive. With these code changes I have also added InMobi (was not supported in the original version of IwGameAds). I couldn’t find a way to specify ad size in mmedia, and it isn’t necessary since you can specify it on their portal. All these words is just to say that the ad size functionality is now supported for InMobi and Inneractive. As I test and update the other providers, I will make sure to add the ad size option. You can keep track of what providers I have tested by looking at the read me on gitHub. This change meets my current needs, but I realize that it would be more useful if you can specify the ad size per slot. I plan to make that change as soon as time permits.

Example 1: The ad size of 300×50.






Multiple AppId support for IwGameAds.

I have used IwGameAds for a while (I didn’t write the original code). One of the great things of using it with Marmalade  is that you can write your code once and ads appear everywhere you deploy it.

I use 2 ad providers, and I usually set up the platform as Android since IwGameAds only supported 1 appId per provider. This meant that whatever device is presenting the ads, it would always shows up as Android on the ad console. It also meant that such as ads for Android games would show up on Blackberry devices.

I made some small changes to the code to accommodate multiple appids per provider. You can get the version with my changes in my repo.

To add the extra appIds you can do it when creating the mediator:

Example 1:

// Create Inner-active ad party and add to the mediator

CIwGameAdsParty* party = new CIwGameAdsParty();

party->ApplicationID = “Default when it doesn’t match any of the others.”;

party->IOSAppID = “iOS AppId”;

party->BBAppID = “ONX based blackbery devices App Id.”;

party->AndroidAppID = “Android App Id.”;

party->WP8AppID = “Windows Phone 8 App Id.”;

party->Provider = CIwGameAds::InnerActive;


Example 2: Not all the appIds have to be provided it would just use the default when the others can’t be found:

CIwGameAdsParty* party = new CIwGameAdsParty();

party->ApplicationID = “Default when it doesn’t match any of the others.”;

party->IOSAppID = “iOS AppId”;

party->WP8AppID = “Windows Phone 8 App Id.”;

party->Provider = CIwGameAds::InnerActive;


Example 3: You can also use it without a mediator: