This page documents how Easier Mobile uses the Google Ads API. The tool described here is built and operated by us, for our own Google Ads account and our own app only. It is not a product, is not distributed, and has no users outside Easier Mobile.
| Company | Easier Mobile — independent Android application publisher, Cầu Giấy, Hanoi, Vietnam |
| Google Play developer | EasierMobile — the publisher account under which Easier Mobile releases its apps |
| Company website | https://easymobile-web.pages.dev/ |
| API contact email | nguyentran.mobile.dev@gmail.com |
| Google Cloud project number | 135139800127 |
| Google Ads manager account (MCC) | 466-112-2733 |
| Access level requested | Basic Access |
| Campaign types supported | App campaigns (App campaigns for installs) only |
| Capabilities provided | Reporting, campaign management, and campaign creation from reviewed templates. No account creation, no account management, no keyword planning. |
| Tool users | Internal only — the single developer who operates Easier Mobile |
| Document version / date | 1.0 — 24 August 2026 |
Easier Mobile is an independent Android application publisher based in Cầu Giấy, Hanoi, Vietnam. We design and publish free-to-download utility applications for users who find standard smartphone interfaces difficult to read or operate — primarily older users and users with reduced vision.
Our published application on Google Play is Big Font — Big Text Size
(com.easiermobile.bigfont), a system-wide font scaling utility. Two further
applications, Magnifying Glass and AI Cleaner, are in development and are not yet published.
Revenue model. All applications are free to install. Revenue comes from two sources, in order of contribution:
User acquisition. We buy installs for Big Font through Google Ads App campaigns, funded and operated entirely by Easier Mobile, under the manager account 466-112-2733. We do not manage advertising for any third party, and we do not resell or sublicense any advertising service.
Because our revenue is dominated by advertising impressions rather than in-app purchases, the profitability of a paid user acquisition campaign cannot be judged from Google Ads metrics alone. Cost data lives in Google Ads; the corresponding revenue signal lives in AdMob and in Firebase/Google Analytics 4, delivered per impression through the Google Mobile Ads SDK onAdPaid callback.
We are building an internal tool that joins these two sides into a single view of average revenue per install (ARPI) and return on ad spend (ROAS) broken down by campaign, country and install cohort day, that applies conservative, rule-based budget and bid adjustments based on that view, and that launches new App campaigns reproducibly from reviewed configuration templates.
The tool has three functions:
The tool is strictly internal. It is not a product, it is not offered to clients, it has no public sign-up, and it is not distributed to anyone.
| Aspect | Detail |
|---|---|
| Who can use it | Employees of Easier Mobile only. At present the marketing/engineering team responsible for user acquisition. |
| Accounts accessed | Only Google Ads accounts under our own manager account 466-112-2733, which contain only Easier Mobile's own campaigns for Easier Mobile's own applications. |
| Third-party accounts | None. The tool never accesses an advertiser account that Easier Mobile does not own and fund. |
| Interface | A scheduled batch job with no public endpoint, plus a read-only Looker Studio dashboard reading the resulting BigQuery tables. Dashboard access is restricted by Google account to company members. |
| Third-party software | The tool is developed in-house by Easier Mobile. It is not built on, and does not embed, any Google Ads API tool developed by another party. |
| Layer | Technology |
|---|---|
| Language / client library | Python 3.11 with the official google-ads client library |
| Compute | Google Cloud Run job, triggered by Cloud Scheduler once per day at 03:00 UTC |
| Warehouse | BigQuery — Google Ads metrics, GA4 event export, AdMob revenue, audit log |
| Presentation | Looker Studio, read-only, restricted to company Google accounts |
| Secret storage | Google Cloud Secret Manager |
| Cloud project | 135139800127 |
GoogleAdsService.SearchStream is called with GAQL queries (section 7) to fetch yesterday's campaign, ad group, geographic and asset metrics for every account under the manager account.onAdPaid values from the Google Mobile Ads SDK) and Play Billing purchase events, producing ARPI and ROAS per campaign × country × cohort day.CampaignBudgetService.MutateCampaignBudgets, CampaignService.MutateCampaigns and CampaignCriterionService.MutateCampaignCriteria. In dry-run mode the mutate call is made with validate_only = true and nothing is changed.Campaign creation is not part of the daily automated job. It runs as a separate, manually invoked Cloud Run job that takes a single argument: the path to a campaign template committed to our internal Git repository. A launch run performs the following steps and then exits.
PAUSED), the ad group, the app ad and the location and language criteria.Every created campaign stays paused until a person enables it in the Google Ads user interface. The tool has no code path that enables a newly created campaign.
| Service | Method | Mode | Purpose | Est. daily operations |
|---|---|---|---|---|
| GoogleAdsService | SearchStream | Read | Campaign, ad group, geographic, asset and conversion metrics | ~25 |
| CustomerService | ListAccessibleCustomers | Read | Enumerate accounts under the manager account | 1 |
| GoogleAdsService | Search (customer_client) | Read | Resolve the account hierarchy under 466-112-2733 | 1 |
| CampaignBudgetService | MutateCampaignBudgets | Write | Adjust daily budget within the caps in section 8 | 0–10 |
| CampaignService | MutateCampaigns | Write | Adjust target CPA / target ROAS; pause or enable a campaign; create a paused App campaign from a template | 0–10 |
| CampaignCriterionService | MutateCampaignCriteria | Write | Set location and language targeting on a new campaign; pause or re-enable location criteria for unprofitable countries | 0–20 |
| AdGroupService | MutateAdGroups | Write | Create the ad group belonging to a newly created App campaign | 0–5, launch runs only |
| AdGroupAdService | MutateAdGroupAds | Write | Create the AppAdInfo ad that carries the approved creative assets | 0–5, launch runs only |
| AssetService | MutateAssets | Write | Register text, image and YouTube video assets from the approved creative library | 0–40, launch runs only |
| GeoTargetConstantService | SuggestGeoTargetConstants | Read | Resolve ISO country codes in a template to geo target constants | 0–2, launch runs only |
Expected volume. Under 100 operations per day in normal operation, with an allowance up to roughly 500 on days when historical data is backfilled. This is well inside the 15,000 operations per day of Basic Access. We do not anticipate needing Standard Access.
Services deliberately not used. The tool does not create, link or modify Google Ads accounts, does not manage user access or billing, does not upload click or offline conversions, does not use the App Conversion Tracking and Remarketing API, does not manage audiences or remarketing lists, and does not use keyword planning services. Creative assets are never generated at run time; the tool only registers files that already exist in the approved creative library.
SELECT
campaign.id,
campaign.name,
campaign.advertising_channel_type,
campaign.status,
campaign_budget.amount_micros,
metrics.cost_micros,
metrics.impressions,
metrics.clicks,
metrics.conversions,
metrics.conversions_value,
segments.date
FROM campaign
WHERE segments.date DURING YESTERDAY
AND campaign.advertising_channel_type = 'MULTI_CHANNEL'
SELECT
campaign.id,
campaign_criterion.location.geo_target_constant,
metrics.cost_micros,
metrics.conversions,
segments.date
FROM location_view
WHERE segments.date DURING LAST_7_DAYS
AND campaign.status = 'ENABLED'
operation = client.get_type("CampaignBudgetOperation")
budget = operation.update
budget.resource_name = budget_resource_name
budget.amount_micros = clamped_amount_micros # produced by the guardrail validator
client.copy_from(
operation.update_mask,
protobuf_helpers.field_mask(None, budget._pb),
)
response = campaign_budget_service.mutate_campaign_budgets(
customer_id=customer_id,
operations=[operation],
validate_only=DRY_RUN, # true until a change set is approved
)
| Date | Campaign | Country | Installs | Cost | CPI | Ad revenue D7 | ARPI D7 | ROAS D7 | Action proposed |
|---|---|---|---|---|---|---|---|---|---|
| 2026-08-23 | BigFont-AC-Installs-A | BR | 412 | $61.80 | $0.150 | $74.16 | $0.180 | 120% | Budget +15% (capped) |
| 2026-08-23 | BigFont-AC-Installs-A | MX | 288 | $46.08 | $0.160 | $46.94 | $0.163 | 102% | No change |
| 2026-08-23 | BigFont-AC-Installs-A | PH | 531 | $42.48 | $0.080 | $21.24 | $0.040 | 50% | Flag — below floor 2 of 3 days |
operation = client.get_type("CampaignOperation")
campaign = operation.create
campaign.name = template.campaign_name # from the version-controlled template
campaign.status = client.enums.CampaignStatusEnum.PAUSED # always paused on creation
campaign.advertising_channel_type = (
client.enums.AdvertisingChannelTypeEnum.MULTI_CHANNEL
)
campaign.advertising_channel_sub_type = (
client.enums.AdvertisingChannelSubTypeEnum.APP_CAMPAIGN
)
campaign.app_campaign_setting.app_id = template.app_id # e.g. com.easiermobile.bigfont
campaign.app_campaign_setting.app_store = (
client.enums.AppCampaignAppStoreEnum.GOOGLE_APP_STORE
)
campaign.app_campaign_setting.bidding_strategy_goal_type = (
client.enums.AppCampaignBiddingStrategyGoalTypeEnum
.OPTIMIZE_INSTALLS_TARGET_INSTALL_COST
)
campaign.campaign_budget = budget_resource_name # created at the template minimum
campaign.target_cpa.target_cpa_micros = template.target_cpa_micros
response = campaign_service.mutate_campaigns(
customer_id=customer_id,
operations=[operation],
validate_only=DRY_RUN, # true unless the launch run is explicitly confirmed
partial_failure=False,
)
SELECT campaign.id, campaign.name, campaign.status
FROM campaign
WHERE campaign.name LIKE 'BigFont-AC-Installs-%'
All write operations are generated by deterministic rules, never by discretion at runtime. No machine-learning model or free-form logic issues a mutation.
| # | Condition (evaluated on a 7-day rolling window) | Action |
|---|---|---|
| R1 | Campaign ROAS D7 ≥ 120% and spend has hit the daily budget on ≥ 5 of the last 7 days | Increase daily budget by 15% |
| R2 | Campaign ROAS D7 ≤ 70% for 3 consecutive days | Decrease daily budget by 20% |
| R3 | Campaign ROAS D7 ≤ 40% for 5 consecutive days | Propose pausing the campaign — requires human approval |
| R4 | Country ARPI D7 / CPI < 0.5 with ≥ 300 accumulated installs | Pause that location targeting criterion |
| R5 | Country ARPI D7 / CPI ≥ 1.2 with ≥ 300 accumulated installs | Raise the campaign target ROAS by one step (max 10%) |
| Guardrail | Value |
|---|---|
| Maximum budget change in one run | ±20% of the current budget |
| Absolute daily budget ceiling per campaign | A hard figure held in configuration; the tool can never exceed it regardless of rule output |
| Absolute daily budget floor per campaign | Configured minimum; prevents a campaign being throttled to nothing by a data error |
| Maximum mutate operations per run | 50. Exceeding this aborts the run and raises an alert rather than proceeding. |
| Minimum data before any rule fires | 300 installs and 7 days of data for the segment. Thin segments are never acted on. |
| Campaign pause | Never automatic. Rule R3 emits a proposal; a human enables it. |
| Dry run | Default mode. Every mutate is issued with validate_only = true and the resulting change set is emailed for review. Live mode is enabled per run by explicit configuration. |
| Data freshness check | If the GA4 or AdMob revenue tables are stale or the AdMob-to-GA4 revenue variance exceeds 10%, all write operations are skipped for that run. |
| Audit | Every attempted mutation is written to an append-only BigQuery table with the prior value, new value, triggering rule, request id and API response. |
Campaign creation is template-driven rather than rule-driven: no metric threshold ever causes a campaign to be created. A campaign exists only because a person committed a template and started a launch run.
| Guardrail | Value |
|---|---|
| Trigger | Manual invocation only. The daily scheduled job can never create a campaign. |
| Source of truth | A schema-validated template committed to our internal Git repository and reviewed before merge. No campaign field is generated at run time. |
| Initial status | PAUSED, always. The tool contains no code path that sets a newly created campaign to ENABLED. |
| Initial budget | The configured minimum daily budget for that application. A template may not request a higher opening budget; scale-up happens later through rule R1 and its caps. |
| Creative assets | Only files already present in the approved creative library, each carrying a reviewer signature. Nothing is generated, scraped or fetched from an external source. |
| Duplicate guard | Existing campaigns are queried by name pattern first. A collision aborts the run before any write. |
| Maximum campaigns per launch run | 6 — one per country basket in a single geography test. |
| Maximum active campaigns per application | A hard configured ceiling. Reaching it blocks further creation until campaigns are retired. |
| Applications targeted | Only application ids on an allow-list of our own published applications — currently a single entry, com.easiermobile.bigfont. |
| Dry run | Default. The whole structure is issued with validate_only = true and emailed for review; live creation requires explicit confirmation on the run. |
| Atomicity | partial_failure = false. If any part of the structure fails, the run stops and the partial structure is reported rather than left half built. |
| Audit | The template revision, every resource created and every API response is written to the append-only audit table. |
secretmanager.versions.access on exactly these secrets and nothing more.https://www.googleapis.com/auth/adwords is requested.SearchStream so that a date range is returned in a single streamed request rather than many paged requests.RESOURCE_EXHAUSTED and transient INTERNAL / UNAVAILABLE errors are retried with exponential backoff and jitter, capped at 5 attempts. Any retry_delay returned by the API is honoured.partial_failure = false, so a rejected operation does not leave a change set half applied.WRITE_SETTINGS Android permission is disclosed on its Google Play listing and in its privacy policy, and is used solely to apply the system font scale that the user selects.| Phase | Scope | API usage |
|---|---|---|
| 1 — Measurement | Ship onAdPaid ad-revenue reporting and the event funnel in Big Font; export GA4 and AdMob data to BigQuery; reconcile revenue to within 10%. | Read only |
| 2 — Reporting | Daily Google Ads metric ingestion and the ARPI/ROAS-by-country dashboard. | Read only |
| 3 — Automation, dry run | Rule evaluator and guardrails live, all mutations issued with validate_only = true and reviewed by a human for at least two weeks. | Read + validated writes |
| 4 — Automation, live | Budget, bid and location-criterion adjustments applied automatically within the caps of section 8. Campaign pause stays manual. | Read + write |
| 5 — Templated launch | Campaign creation from version-controlled templates, used to launch geography-basket tests as sets of structurally identical paused campaigns. Enabling a campaign stays manual. | Read + write |