# Verified SDK traffic â€” version 0.3

The backend requires device verification before returning a launch configuration, issuing an ad or accepting an impression, click, or statistics report. A public Game ID alone earns nothing. Old `/api/sdk/ad-request`, `/impression`, `/click`, and `/placement-stats` requests return HTTP 410. Upgrade the backend and Unity package together.

## Operator setup (once per deployment)

Verification is **disabled by default**. There is no development switch that awards credits to unsigned requests. Local Unity Test Mode displays a card without contacting the service.

`backend/IndieExchange.Api/appsettings.Security.example.json` is a reference template, not an automatically loaded configuration file. Copy its section into your deployed configuration or use environment variables.

Set these environment variables on the API process (double underscores denote configuration sections):

| Variable | Value |
| --- | --- |
| `SdkSecurity__Enabled` | `true` after completing configuration |
| `SdkSecurity__IpHashKey` | A cryptographically random secret of at least 32 characters; keep it stable and outside source control |
| `SdkSecurity__GoogleCloudProjectNumber` | Numeric Google Cloud project number, not the project ID |
| `SdkSecurity__GoogleServiceAccountFile` | Absolute path to the server-only service-account JSON file |
| `SdkSecurity__GeoIpDatabaseFile` | Absolute path to a licensed GeoIP2/GeoLite2 country or city `.mmdb` database |
| `SdkSecurity__KnownProxies__0` | Optional exact IP address of your trusted reverse proxy; add indexed entries for additional proxies |

For Docker, pass the variables into the `api` service and mount the service-account file and GeoIP database read-only at the configured container paths. Merely putting values in a Compose `.env` file does not pass them into a service.

Enable Play Integrity in Google Cloud, link the correct project in Play Console (or configure SDK-provider access through Play SDK Console), and authorize the service account to decode verdicts for the participating apps. This release uses one operator Cloud project/service account. Publishers must complete the applicable Google console setup; installing this package alone does not grant Google access to their apps. Request sufficient verification quota before launch. The package verifies ad loads, impressions, clicks, and periodic diagnostics separately; estimate quota from all of these operations.

Service-account credentials remain on the server. The Cloud project number and each game's Game ID are public identifiers. The package adds Play Integrity 1.6.0 to Android builds automatically and raises the Android minimum to API 23 if needed. It does not install or call AdMob.

Apple verification uses the embedded official Apple App Attestation root certificate. No Apple signing private key is sent to Indie Exchange. Each iOS app must enable App Attest in its Apple Developer account and use a provisioning profile containing that entitlement. The package adds DeviceCheck.framework and the production App Attest entitlement to the exported Xcode project, preserving an existing entitlements file. iOS 14 or newer and App Attest-capable devices are required for live exchange ads.

The API derives country from the connection IP. It does not trust `countryCode`, device language, arbitrary country headers, or untrusted forwarded headers. Without a working GeoIP database, country is `ZZ` and impression credits require manual review. Keep the database updated. Configure exact trusted proxies when terminating TLS upstream; otherwise traffic will be attributed to the proxy. IPv6 addresses are grouped by /64 for caps. Stored IP identifiers are HMAC hashes, not raw IP addresses.

Restart the API after configuration. Startup creates additive security tables and indexes; it does not rewrite existing impressions, campaigns, or balances. Take the usual deployment backup before changing a production backend.

## Approve each app

In **Admin â†’ Verified apps & credit review**:

1. Select the game and platform.
2. Enter the actual Android package name or iOS bundle identifier. Verify the publisher's ownership through the store account before approving.
3. Android: provide the SHA-256 digest of the **Play app signing certificate**, encoded as unpadded base64url. This is not the upload-key certificate. Multiple allowed signing certificates can be entered on separate lines for rotation. Convert the colon-separated hex digest with the PowerShell snippet below.
4. iOS: enter the App ID prefix from Apple Developer, which is usually the Team ID but can differ.
5. Optionally restrict Android `versionCode` or Apple bundle versions. Older Apple authenticator data without version metadata cannot satisfy a version restriction.
6. Enable **Identity checked and approved**. Initially leave automatic credit settlement off. After reviewing real traffic, enable it for that publisher if appropriate.

```powershell
$certificateHex = 'PASTE_THE_PLAY_APP_SIGNING_SHA256_HEX_DIGEST'
$digestBytes = [Convert]::FromHexString($certificateHex.Replace(':', '').Replace(' ', ''))
[Convert]::ToBase64String($digestBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
```

Changing a publisher's store URL or editable game metadata does not modify its approved identity. Changing an approved app identifier invalidates the old identity and requires fresh Apple key enrollment.

## Credit controls

Each verified ad load receives a random ticket valid for five minutes. The database stores only its hash. It identifies one server-created ad request, app identity, installation, publisher, campaign, creative, country, format, and quoted cost. Impression and click requests require a fresh device proof bound to the exact action, payload, installation, and two-minute single-use server challenge. Android also signs the request with an installation key kept by Android Keystore; Apple assertions use its attested key and a monotonically increasing counter.

The impression transaction takes a PostgreSQL accounting lock shared across API instances. A ticket can settle only once, regardless of new event IDs, retries, or simultaneous requests. Wallet rows are locked in a stable order; administrator adjustments use the same wallet-row protection. Settlement rechecks app approval, campaign/creative status, campaign funds, daily/lifetime budget, frequency cap, and publisher daily credit ceiling. Server-generated ledger IDs provide an additional uniqueness backstop.

Default controls, configurable under `SdkSecurity`:

| Setting | Default |
| --- | ---: |
| `InstallationDailyLimit` | 100 claimed impressions |
| `IpDailyLimit` | 1,000 claimed impressions |
| `PublisherDailyLimit` | 10,000 claimed impressions across the owner's games |
| `PublisherDailyCredits` | 1,000 earned credits |
| `MinimumImpressionSeconds` | 1 second after ticket creation |

Ad issuance has hard daily limits of twice the corresponding impression limits. Database-backed request limits also apply per IP, publisher, and installation. Invalid proofs, expired tickets, and replayed challenges are rejected. Valid traffic exceeding impression limits, new publishers, unknown countries, network changes, and Google recent-device-activity LEVEL_4 (when supplied) go to **Pending**, with a reason. Activity is a review signal, not proof of fraud; statistics upload at most once per ten minutes so diagnostics do not consume disproportionate verification quota. Pending credits are not spendable and do not debit an advertiser. Approval is idempotent and rechecks funds, budgets, frequency caps, and the daily credit ceiling. An approval can remain pending or become rejected if those checks fail. Review decisions retain the administrator ID, timestamp, and note.

The accounting lock deliberately serializes settlement for this initial release. Measure throughput before a high-volume launch; partitioning requires preserving cross-wallet/budget atomicity.

Automatic signup bonuses now default to zero to prevent account-registration credit farming. Existing balances are preserved. Grant launch credits through the administrator adjustment screen if needed.

## Unity behavior

The game's call stays the same:

```csharp
if (!await IndieAds.TryShowInterstitialAsync())
    ShowMyNormalAd();
```

Verification, downloads, and reporting run in the background. The show call does not contact the service. Unsupported devices, verification failure, outages, rate limits, disabled identities, and missing configuration cannot create a ready live exchange ad; the game can use its normal provider immediately. A previously cached valid ad may still display during an outage. Displaying it does not guarantee that reporting will settle credits.

Live requests from the Unity Editor are disabled. Use local Test Mode there, then validate a store-distributed Android build or a correctly signed iOS build on a physical device. The package does not pin the version of any paid-ad SDK. It pins its own Play Integrity library dependency.

## Limits of the protection

Device attestation does not prove that a human watched an ad. Genuine phones can be automated, and compromised legitimate apps can relay requests. Installation keys are not a permanent person or hardware ID; reinstalls can create new identities. IP and publisher caps, review, campaign budgets, and optional Google's recent-device-activity signal provide additional controls. Shared household/carrier IPs can legitimately trigger review. Apple receipt-based fraud metrics and Google's optional Device Recall are not implemented in this release. Do not describe this as guaranteed fraud prevention.

Update privacy disclosures for installation keys, request metadata, IP-derived country, device integrity services, and retained accounting/review records. Expired challenges and rate-limit buckets are removed periodically; unused expired tickets are removed after seven days. Claims, keys, and financial history remain for audit and need an operator retention policy.

## Validation and rollout

Cryptographic tests exercise trusted and untrusted chains, app binding, request binding, altered signatures, replay counters, licensing, and Google verdict rejection. PostgreSQL tests use disposable databases to exercise parallel claims, wallet/budget protection, rate limits, pending review, and replay protection. Set `INDIE_TEST_POSTGRES` to an administrative test connection before `dotnet test`; the tests create and drop only their own `indie_security_test_<random>` databases. Without it, those integration tests are reported as skipped.

Complete physical-device checks with your actual Google/Apple accounts before enabling production earning. Server-side synthetic certificate tests and a compiled Android bridge are not a substitute for store/device validation.

Official references: [Google setup](https://developer.android.com/google/play/integrity/setup), [standard requests](https://developer.android.com/google/play/integrity/standard), [verdicts](https://developer.android.com/google/play/integrity/verdicts), [Apple validation](https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server), [Apple certificate authority](https://www.apple.com/certificateauthority/private/).
