The app was finished. Everything that actually stopped it from shipping was an identity check — and not one of them is in the SDK documentation.
I am building Kotohira Digital Residents for Shipaton 2026 — a membership app for a Japanese town of about 8,000 people, where a ¥5,000 annual subscription opens a resident card that only completes after you have helped somebody in the town three times.
By mid-August, the code was done. Expo, React Native, TypeScript, react-native-purchases. The test suite was green. A crawler walked every screen in five subscriber states across three dates. I had written a whole article about the checks.
Then the project did not move for ten days.
Not because of a bug. Every single thing that blocked me was a system asking Who are you, and can you prove it — and every one of them failed in a way the SDK docs do not describe, because the SDK is not where they live.
Here they are, in the order they hit me.
1. The key I was not permitted to create
RevenueCat needs a Google Play service account to read your subscription state. Its documentation is clear about the format:
Find your credentials JSON file that was previously generated or downloaded and either drop it into your project settings or select it from the finder.
A JSON key. Not Workload Identity Federation, not an impersonation chain — a downloaded private key file. That is the only path the dashboard offers.
I created the service account. I clicked Create key. Google refused.
This is an organization policy, and here is the part that took me a while to accept: I had not set it. Google applies a set of "secure by default" constraints to new organizations automatically, and this is one of them. My own tenant was protecting me from downloading a long-lived private key — which is, to be completely fair, the correct default. Long-lived JSON keys are exactly the credential that leaks.
The advice you will find is "just use Workload Identity Federation." That is good advice, and it does not help, because the third party you need to authenticate to only accepts the file.
The constraint is enforced at the organization node and inherited downward. It can be overridden per project. So:
Step 4 is the one people skip, and it is the whole point. The exception should outlive the click, not the project.
My console is in Japanese. The role is listed as 組織ポリシー管理者. I searched for 組織のポリシー管理者 — the same words with one grammatical particle inserted, which is how a Japanese speaker would naturally say it — and got nothing. Twice. I concluded the role was not available to me and went looking for a different approach entirely.
The role ID never changes: orgpolicy.policyAdmin. Search by ID, not by the localized display name. Every cloud console is a translated product, and the translation is not part of the API contract.
⚠ One more, in case you hit it: there is a newer managed constraint, iam.managed.disableServiceAccountKeyCreation. If the classic one is off and creation is still blocked, the policy detail page tells you so at the top.
I drive some of this work through a browser automation tool. IAM forms — text fields, dropdowns, save — accepted synthetic events fine. Project creation worked. But certain Enable buttons in Google Cloud Console did nothing at all: no navigation, no error, no state change, whether targeted by coordinates or by element reference. A human click on the same button worked immediately.
I do not know the mechanism. I know the shape of it: when a button ignores you silently, stop iterating on the selector. Hand the URL to a person and move on. I wasted more time proving I could click than the click was worth.
2. The bank that cannot receive money
With the key sorted, Google Play was moving. Then I went to put the app on the Samsung Galaxy Store, because Shipaton counts a Galaxy Store release the same as an App Store or Play release, and it seemed like cheap redundancy.
Samsung requires Commercial Seller status to register an Android app. The application form asks for a payout destination. I picked bank transfer and filled in our corporate account.
( * ) is a required field. I went to look up our SWIFT code and found this, on the bank's own support site:
**We do not have a SWIFT code. \ — from the bank's own public support pages
Our business bank is a modern, excellent, fully online Japanese bank. It has no SWIFT/BIC, which means it cannot receive an inbound international transfer at all. Its overseas-money product is outbound-only, routed through a partner. For paying suppliers this has never once mattered. For being paid by a Korean company through an international settlement rail, it is fatal.
Nothing about this is a bug or a bad bank. It is a category error I made: I assumed "business bank account" was a single capability. Sending and receiving international money are separate capabilities, and a bank can ship one without the other.
Reading the same guide more slowly, the payout section says PayPal is also accepted, with one carve-out:
When the seller is a South Korean national — may enter the "bank account" information only. (PayPal is not allowed)
PayPal is barred for Korean sellers specifically. Everyone else may use it. So the fix was not "open an account at a megabank and wait a month" — it was "open a PayPal business account", which the docs promise takes hours.
It took six days.
3. Three rejections that were actually two queues
Here is the timeline, from my own inbox. I am including it because every write-up of business verification I could find describes it as one step, and mine was not.
Day 0, 19:04
Account created. Address registered.
Day 0, 19:11
"Please verify your identity. Your account is blocked until you do."
Day 1, 05:33
"We could not verify your identity."
Day 1, 08:21
"Your business information has been confirmed." PIN posted to the registered corporate address.
Day 2, 13:26
Day 2, 16:33
Day 6, 06:51
"Your identity has been verified." Account usable.
Look at Day 1. At 05:33 I was rejected. At 08:21 I was approved. Same account, three hours apart, opposite answers.
I spent most of Day 1 assuming the 08:21 approval had superseded the 05:33 rejection, and that I was now waiting on a posted PIN. I was wrong, and the cost of being wrong was two days of not resubmitting anything.
They are two independent checks running in parallel:
The company being verified tells you nothing about the human. Both queues have to clear, and only one of them was waiting on me. The mail subject lines are nearly identical, so if you read them as a single thread you will conclude, as I did, that the system is contradicting itself.
If you are in the middle of this: sort the messages by which entity they are about, not by time.
Japanese identity documents include an individual tax number on one side. The verification flow will happily accept a photograph of it.
I did not send it. The number is collected under a statute that enumerates the purposes it may be collected for, and "proving my identity to a payment company" is not among them. Where a supplementary document was needed I used a residence certificate issued without the number printed on it — you request that variant explicitly at the counter; the default may include it.
This cost me at least one of the three rejections. I would do it again. A verification queue is not a reason to hand a regulated identifier to a party with no lawful basis to hold it.
4. The purchase I refused to start unguarded
Ten days gone, and now I could finally prepare to do the thing I had been trying to do since the start: buy my own subscription on a real device without accidentally charging the business ¥5,000.
Google Play has license testers — accounts whose purchases are real transactions in every observable way except that no money moves. Registering them looks trivial. Three details are not.
The list is not the setting. Creating a mailing list of testers does not activate anything. You must return to the license testing screen, select that list as the license testers, and save. I created the list, saw it in the dropdown, and assumed. It was not applied.
The account that matters is the one signed into the phone, not the one signed into Play Console. I did not know which Google account was active on the test device, so I registered both candidates. This is not paranoia: if the purchasing account is not on the list, the purchase is an ordinary purchase, and a ¥5,000 annual subscription is charged for real.
The settings URL is not where you would guess. The page lives at /license-tester. The plausible /settings/license-testing silently redirects to the app list, which reads exactly like "you don't have this feature."
Then the last one, which would make a correct test look broken if I missed it:
A license-tester purchase is a sandbox transaction. In RevenueCat's dashboard it does not appear until you enable View Sandbox Data. I enabled that view before the first purchase, while the customer history was still correctly empty. Observability is part of the test setup, not something to add after a blank screen sends you debugging.
Two facts that make sandbox testing much faster once you know them, both from RevenueCat's docs: sandbox renewal periods are compressed — a one-year subscription renews every 30 minutes, up to six times — and because they are that fast, not every renewal shows up in the dashboard. The planned run is purchase → RevenueCat receipt → restore → cancel → entitlement lapse. Until that sequence is observed, I will not call the purchase path tested.
5. The requirement I never checked
While I was doing all of this, I had a sentence in my own planning document that said:
The thing that matters most for Shipaton is that RevenueCat has processed one real transaction. We are at zero.
I had been carrying that for weeks. It was shaping which blockers I treated as urgent. I had never read the rules.
When I finally did:
Entrants must create a working software application that uses the RevenueCat SDK to power at least one in-app or web purchase, or that serves ads through RevenueCat Ads.
The first public version of the Project must be released during the Submission Period.
Real revenue is not an entry requirement. It is used to shortlist for the grand prize — a prize I had already, on separate reasoning, decided not to chase — and even there the rules say judges weigh the quality and sustainability of the growth rather than the number.
The actual hard requirement was the one I had been treating as a scheduling detail: be publicly released before the deadline. An internal test track is not a public release.
I want to be precise about what went wrong, because it is not "I was lazy." I had read a summary of the rules — mine, written by me, early, when the summary was cheap to write and nobody would check it. Then I optimized against my summary for a month.
The document you wrote about the rules is not the rules. It ages, it drifts toward whatever you were worried about the day you wrote it, and nothing in your toolchain will ever tell you it is stale. In my previous article I built a verifier that regenerates the numbers in my prose from the code, precisely so my documents could not drift from my repository. It never occurred to me to apply the same suspicion to the documents describing things outside the repository, which is where the actual deadline lived.
6. What I would do on day one
Not "what I learned." What I would put in the first week's checklist, before writing a line of product code:
None of this is skill. It is all latency — every item is days of waiting that you can start early or start late, and starting late is the entire difference. The code was never the long pole.
Where the app actually is
Same disclosure as my last piece, updated:
If any of that changes I will write it up, including whatever I get wrong next.
(0)Comments