Design draft · Amazon Appstore implementation of the Flutter in_app_purchase federated plugin · 2 September 2026 · status: proposal, not yet implemented
Verdict: feasible. The platform-interface package was built for exactly this kind of third-party implementation, Flutter (since 3.27) lets an app swap out the endorsed in_app_purchase_android for a non-endorsed one, and the Amazon Appstore SDK is on Maven Central with a small, stable, well-documented API. It will not be endorsed upstream and there are a handful of semantic mismatches that need explicit decisions — so treat it as a community package with a clearly documented lossy edge or two, not as a drop-in Play Billing twin.
in_app_purchase 3.3.0 (June 2026) is fully federated: in_app_purchase (app-facing) → in_app_purchase_platform_interface ^1.4 → endorsed in_app_purchase_android ^0.5 (Google Play Billing, Pigeon-based) and in_app_purchase_storekit ^0.4 (StoreKit 1/2).
The platform interface explicitly invites third-party implementations: extend InAppPurchasePlatform, call InAppPurchasePlatform.setInstance(...); store-specific extras go through InAppPurchasePlatformAddition. Breaking changes to the interface are avoided by policy, and the base class ships default (throwing) implementations, so new interface methods do not break compile of an external implementation.
The surface to implement is small: purchaseStream, isAvailable(), queryProductDetails(), buyNonConsumable(), buyConsumable(), completePurchase(), restorePurchases(), countryCode().
Override mechanism exists. flutter/flutter #137040 (merged July 2024, stable from Flutter 3.27) lets an app override an endorsed native implementation simply by adding a direct dependency on another package that declares implements: in_app_purchase for the same platform. Both packages remain in pubspec.lock; only the override is registered. This is the piece that was missing for years and it is now in place.
The Android implementation pattern to copy is well established: Pigeon @HostApi/@FlutterApi, a typed billing_client_wrappers library, GooglePlayProductDetails/GooglePlayPurchaseDetails subclasses, and a registerPlatform() static that sets both the platform instance and the addition instance.
1.2 The Amazon side
Use the Appstore SDK, not the compatibility shim. The Appstore Billing Compatibility SDK (the Play-Billing-lookalike, com.amazon.device:appstore-billing-compatibility:4.2.0) loses technical support and maintenance on 15 September 2026 — thirteen days from now. Building on it would mean building on an abandoned layer. The Appstore SDK proper (com.amazon.device:amazon-appstore-sdk:3.0.9, May 2026) is the supported path and is on Maven Central, so the plugin can pull it with a normal Gradle dependency; no JAR redistribution or license question.
The IAP API is small and asynchronous: PurchasingService.registerListener / getUserData / getProductData / getPurchaseUpdates(reset) / purchase(sku) / notifyFulfillment(receiptId, result) / enablePendingPurchases, with responses delivered to one PurchasingListener via a manifest-declared ResponseReceiver broadcast receiver. Every request returns a RequestId which the response carries back — that is the correlation key the Dart layer needs.
Three product types: CONSUMABLE, ENTITLED, SUBSCRIPTION. Fulfillment is explicit (notifyFulfillment), and sending anything other than FULFILLED cancels and refunds the order. Pending purchases (Amazon Kids parent approval) exist since 3.0.4. Tiered subscriptions, Quick Subscribe and add-on subscriptions exist but are gated to select partners — out of scope for v1.
Amazon's own cross-platform IAP plugins (Cordova, Xamarin, AIR) lost support in June 2026; Amazon's guidance is now "integrate the Appstore SDK natively". A Flutter package doing exactly that is aligned with where Amazon is pushing developers.
Market reality check. Amazon shut the Appstore on non-Amazon Android devices on 20 August 2025 (and killed Amazon Coins). The only remaining targets are Fire tablets and Fire TV running Fire OS. Amazon's newest Fire TV hardware is moving to Vega OS, which is not Android and does not run Flutter. So the addressable surface for this package is Fire OS tablets and the installed base of Android-based Fire TVs — real, but not growing.
1.3 Prior art
Package
What it is
Relevance
amazon_iap, flutter_amazon_iap
Thin 1:1 wrappers over PurchasingService with response streams
Useful as reference for the native bridge; neither implements InAppPurchasePlatform, so apps need store-specific code paths.
in_app_purchase_amazon (predatorx7, GitHub)
Early scaffold, "a new Flutter plugin project"
Occupies the natural package name; check pub.dev availability before publishing (fallback: in_app_purchase_amazon_appstore).
purchases_flutter (RevenueCat)
Hosted-backend SDK with Amazon support
Proves the mapping is workable; not an option if you don't want a third-party backend.
flutter_inapp_purchase (OpenIAP)
Alternative unified IAP plugin; Amazon sponsors the OpenIAP spec
Worth checking whether Amazon Appstore support has landed there — if it has, it is the only competitor for "one API, three stores" in Flutter.
2. Why it is not trivial
Request/response, not futures
The SDK is one static listener plus broadcast intents. The Dart side must correlate by RequestId and keep pending completers; the native side must register the listener with the application context once and survive activity recreation.
No numeric price, no currency
Product.getPrice() is a localized string ("$5.00"). ProductDetails.rawPrice and currencyCode are required fields with no exact source. Needs a documented best-effort strategy.
FAILED is ambiguous
Amazon's PurchaseResponse.FAILED "can simply mean the user canceled". The interface distinguishes canceled from error; Amazon does not.
Fulfillment ≠ acknowledge
notifyFulfillment with anything but FULFILLED refunds. completePurchase() maps cleanly to FULFILLED; the refund-path needs a platform-addition method.
Lifecycle expectations
Amazon expects getUserData() and getPurchaseUpdates(false) on every onResume. The interface has no lifecycle hook, so the plugin must do this itself and feed results into purchaseStream.
Subscription model differs
Parent SKU vs. term SKUs, receipts carry termSku; no general upgrade/downgrade API (modifySubscription is partner-gated). ChangeSubscriptionParam has no equivalent.
Duplicate receipts by design
Device-cache resets can redeliver receipts. The plugin must be idempotent and apps must dedupe on receiptId.
Build-time store choice
Override happens at pub level, not per Gradle flavor. One Flutter project produces either the Play or the Amazon binary unless you use the two-package layout below.
2.1 Feature map — what an app written against in_app_purchase gets on each store
Read this as "what does my existing in_app_purchase code do when the platform underneath is Amazon". The Play and App Store columns describe the endorsed implementations as of 3.3.0; the Amazon column describes the package proposed in this document, not the raw Amazon SDK.
✔ works 1:1◐ works with known limitation✘ not available— not applicable / store has no such concept
Feature (interface / addition)
Google Play
Apple App Store
Amazon (new)
Comment on the Amazon column
Core purchase flow
isAvailable()
✔
✔
✔
Backed by getUserData(); false when no Amazon account is signed in or the device has no Appstore.
Outside the plugin on all three stores; noted for completeness.
Store extras (platform addition)
Refuse fulfilment / trigger refund from the client
✘
✘
✔
Amazon-only capability: notifyFulfillment(UNAVAILABLE | NOT_ELIGIBLE | EXISTING_PURCHASE) on the addition.
Product icon URL
✘
✘
✔
smallIconUrl on the wrapper.
Raw store objects reachable (billingClientPurchase / skPaymentTransaction / receipt)
✔
✔
✔
AmazonProductDetails.product, AmazonPurchaseDetails.receipt, plus rawJson passthrough.
Alternative billing / external links
✔
◐
—
No Amazon programme.
Integration & tooling
Zero-config registration (endorsed)
✔
✔
◐
One extra direct dependency (_default shim) and Flutter ≥ 3.27. No code changes.
App-side native config required
✔ none
✔ none
◐
PEM public key in assets/. Manifest entries are merged from the plugin.
Two stores in one binary with runtime switch
—
—
◐
Mode B works technically; Amazon review may object to Play Billing classes in the APK.
Emulator / simulator testing without a real device
◐
✔
✘
Needs a Fire device (or any Android device) with Amazon App Tester installed. Dart mapping layer is unit-testable via Pigeon mocks.
Target devices going forward
✔
✔
◐
Fire tablets + Android-based Fire TV only; no non-Fire Android since Aug 2025, no Flutter on Vega OS.
Reading the map. Everything in the "core purchase flow" group is green or nearly so — an app that only queries products, buys, completes and restores will run unchanged on Amazon. The amber cells cluster in three places: price fidelity (one lossy field), status nuance (cancel vs. error, pending only for Kids), and the subscription-management extras that Play and Apple expose but Amazon either doesn't have or gates behind partner programmes. The red cells are almost all features that are store-specific on every store already (offer codes, proration, obfuscated ids), so an app written portably against the interface is unlikely to depend on them. The two places where Amazon is actually ahead — client-side refund path and cancellation visibility — go into the platform addition rather than being hidden.
3. Packaging and registration
Two packages, both in one repo. The split costs almost nothing and unlocks both deployment styles.
Package
Contains
Declares implements?
in_app_purchase_amazon
Kotlin plugin, Pigeon bridge, InAppPurchaseAmazonPlatform, wrappers, addition, example app
No. Native pluginClass only. Dart registration is explicit via InAppPurchaseAmazonPlatform.registerPlatform().
in_app_purchase_amazon_default
~10 lines: a dartPluginClass whose registerWith() calls registerPlatform()
Yes: implements: in_app_purchase, platforms: android. Adding it as a direct dependency triggers the Flutter-tool override and unregisters in_app_purchase_android for that build.
Mode A — dedicated Amazon build (recommended for v1)
# pubspec.yaml of the Amazon-flavoured app
dependencies:
in_app_purchase: ^3.3.0
in_app_purchase_amazon_default: ^0.1.0 # overrides in_app_purchase_android on Android
Requires Flutter ≥ 3.27. App code is unchanged: InAppPurchase.instance now talks to Amazon. This matches how Amazon submissions already work (separate PEM key asset, separate signing, usually a separate product flavor for Fire OS form factors).
Mode B — single binary, runtime switch
dependencies:
in_app_purchase: ^3.3.0
in_app_purchase_amazon: ^0.1.0 # no override; Play Billing stays registered
// main.dart
WidgetsFlutterBinding.ensureInitialized();
if (await InAppPurchaseAmazonPlatform.isAmazonInstall()) { // installer == com.amazon.venezia, or --dart-define=STORE=amazon
InAppPurchaseAmazonPlatform.registerPlatform(); // replaces the Play instance before first use
}
Works because InAppPurchasePlatform.instance is a plain setter and InAppPurchase.instance delegates lazily. Caveat: both SDKs ship in the APK. Google tolerates that; Amazon review historically frowns on Play dependencies in Fire OS submissions. Document it, don't default to it.
implementation "com.amazon.device:amazon-appstore-sdk:3.0.9" pinned, not 3.+.
Plugin AndroidManifest.xml contributes (via manifest merge) the <queries> for com.amazon.venezia and com.amazon.sdktestclient, and the com.amazon.device.iap.ResponseReceiver with android:exported="true" and permission com.amazon.inapp.purchasing.Permission.NOTIFY. Apps add nothing to their manifest.
consumer-rules.pro: -keep class com.amazon.** { *; }, -dontwarn com.amazon.**.
AppstoreAuthenticationKey.pem stays an app responsibility in app/src/<flavor>/assets/. The example app documents it; the plugin checks for its presence at register time and logs a clear warning if missing (a missing key makes every call fail with FAILED, which is otherwise hard to diagnose).
minSdk 24 to match in_app_purchase_android.
4. Native layer (Kotlin + Pigeon)
Mirror in_app_purchase_android: one Pigeon file, one Kotlin plugin class, one listener class. No method channels by hand.
// pigeons/messages.dart
enum AmazonRequestStatus { successful, failed, notSupported, alreadyPurchased, invalidSku, pending }
enum AmazonProductType { consumable, entitled, subscription }
enum AmazonFulfillmentResult { fulfilled, existingPurchase, notEligible, unavailable }
class PlatformUserData { late String userId; late String marketplace; String? countryCode; }
class PlatformPromotionPlan { late String promotionPrice; late String promotionPricePeriod; late int promotionPriceCycles; }
class PlatformPromotion { late String promotionType; late List<PlatformPromotionPlan> plans; }
class PlatformProduct {
late String sku; late AmazonProductType productType;
late String title; late String description; late String price; late String smallIconUrl;
String? subscriptionPeriod; String? freeTrialPeriod;
late List<PlatformPromotion> promotions;
late String rawJson;
}
class PlatformReceipt {
late String receiptId; late String sku; late AmazonProductType productType;
late int purchaseDateMillis; int? cancelDateMillis; late bool isCanceled;
String? termSku; int? deferredDateMillis; String? deferredSku;
late String rawJson;
}
class PlatformUserDataResponse { late String requestId; late AmazonRequestStatus status; PlatformUserData? userData; }
class PlatformProductDataResponse { late String requestId; late AmazonRequestStatus status; late List<PlatformProduct> products; late List<String> unavailableSkus; }
class PlatformPurchaseResponse { late String requestId; late AmazonRequestStatus status; PlatformUserData? userData; PlatformReceipt? receipt; }
class PlatformPurchaseUpdatesResponse { late String requestId; late AmazonRequestStatus status; PlatformUserData? userData; late List<PlatformReceipt> receipts; late bool hasMore; }
@HostApi()
abstract class AmazonBillingApi {
void registerListener(); // idempotent; app context
String getUserData(); // returns requestId
String getProductData(List<String> skus);
String getPurchaseUpdates(bool reset);
String purchase(String sku);
void notifyFulfillment(String receiptId, AmazonFulfillmentResult result);
void enablePendingPurchases();
bool isSandboxMode(); // LicensingService.getAppstoreSDKMode() == SANDBOX
String? installerPackageName(); // for Mode B detection
void setAutoSyncOnResume(bool enabled);
}
@FlutterApi()
abstract class AmazonBillingCallbackApi {
void onUserDataResponse(PlatformUserDataResponse r);
void onProductDataResponse(PlatformProductDataResponse r);
void onPurchaseUpdatesResponse(PlatformPurchaseUpdatesResponse r);
void onPurchaseResponse(PlatformPurchaseResponse r);
}
Kotlin plugin responsibilities
onAttachedToEngine: keep application context, build Pigeon host, create the single PurchasingListener that forwards every callback to the Flutter API. registerListener() from Dart calls PurchasingService.registerListener(appContext, listener) once.
Implements ActivityAware + DefaultLifecycleObserver. On onResume, if auto-sync is enabled (default true), fire getUserData() and getPurchaseUpdates(false). Responses for these plugin-initiated requests are tagged so the Dart layer routes them to the stream rather than to a waiting completer.
Every callback runs on the UI thread already; marshal straight into Pigeon. Convert Receipt/Product via their toJSON() for rawJson so nothing is lost even if a field is unmapped.
Map Java enums to Pigeon enums in one place; unknown values become failed plus a log line, never a crash.
class InAppPurchaseAmazonPlatform extends InAppPurchasePlatform {
static void registerPlatform() {
InAppPurchasePlatform.instance = InAppPurchaseAmazonPlatform();
InAppPurchasePlatformAddition.instance = InAppPurchaseAmazonPlatformAddition(...);
}
static Future<bool> isAmazonInstall(); // installer == com.amazon.venezia || dart-define
@override Stream<List<PurchaseDetails>> get purchaseStream; // broadcast
@override Future<bool> isAvailable();
@override Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers);
@override Future<bool> buyNonConsumable({required PurchaseParam purchaseParam});
@override Future<bool> buyConsumable({required PurchaseParam purchaseParam, bool autoConsume = true});
@override Future<void> completePurchase(PurchaseDetails purchase);
@override Future<void> restorePurchases({String? applicationUserName});
@override Future<String> countryCode();
}
class InAppPurchaseAmazonPlatformAddition extends InAppPurchasePlatformAddition {
Future<AmazonUserData> getUserData();
Future<void> notifyFulfillment(String receiptId, AmazonFulfillmentResult result); // the refund path
Future<void> enablePendingPurchases();
Future<bool> isSandboxMode();
Future<List<AmazonReceipt>> getPurchaseUpdates({required bool reset}); // raw, paginated-to-completion
void setAutoSyncOnResume(bool enabled);
}
class AmazonProductDetails extends ProductDetails {
final AmazonProduct product; // full wrapper incl. promotions, periods, icon
final bool priceIsEstimated; // true when rawPrice/currencyCode were inferred
}
class AmazonPurchaseDetails extends PurchaseDetails {
final AmazonReceipt receipt;
final String userId; // needed for RVS
final String marketplace;
final AmazonRequestStatus rawStatus; // so apps can see FAILED vs. anything else
}
Request router
One Map<String, Completer> per response type keyed by requestId. If a response arrives with no matching completer (plugin-initiated auto-sync, or a response that outlived an engine restart) it is routed to purchaseStream when it carries receipts, otherwise dropped with a debug log. getPurchaseUpdates pagination (hasMore) is handled inside the router: it re-issues the call and accumulates until hasMore == false, then completes. Timeouts: none by default (Amazon can legitimately take a while while the parent approves a pending purchase); an optional per-call timeout is exposed on the addition.
6. Semantic mapping
Interface call
Amazon call(s)
Notes
isAvailable()
registerListener() then getUserData()
SUCCESSFUL → true. FAILED (not signed in / no Appstore) and NOT_SUPPORTED → false. Cache the user data for later use.
queryProductDetails(ids)
getProductData(ids), chunked
Amazon caps the SKU set per call (historically 100; verify against 3.0.9 Javadoc). unavailableSkus → notFoundIDs. Subscriptions: callers must pass term SKUs, since only term SKUs carry prices — document loudly.
buyNonConsumable(param)
purchase(sku)
Entitlements and subscription term SKUs. AmazonPurchaseParam adds nothing in v1; exists for forward-compat.
buyConsumable(param, autoConsume)
purchase(sku); if autoConsume, notifyFulfillment(FULFILLED) on SUCCESSFUL
Parity with Android's auto-consume. Warn in docs: auto-fulfilling before delivery removes Amazon's retry safety net; default true only for parity.
completePurchase(p)
notifyFulfillment(p.purchaseID, FULFILLED)
Re-fulfilling an already fulfilled receipt is explicitly fine per Amazon docs, so this is safe on restored purchases too.
restorePurchases()
getPurchaseUpdates(true) to exhaustion
Non-canceled receipts → PurchaseStatus.restored, pendingCompletePurchase = true. Canceled receipts → PurchaseStatus.canceled so apps revoke. Consumables only come back if unfulfilled or bought on this device — same limitation as Play, document it.
countryCode()
getUserData().countryCode
Available since SDK 3.0.7. Fallback to marketplace.
(auto, on resume)
getUserData(), getPurchaseUpdates(false)
Unfulfilled receipts → PurchaseStatus.purchased on the stream, exactly like Android re-emitting unacknowledged purchases on connect. Also catches Kids pending purchases that got approved while the app was away.
purchaseStream error path
—
Never addError; emit PurchaseDetails with status: error and an IAPError(source: 'amazon_appstore', code, message), matching the Android implementation's behaviour.
Status mapping
Amazon
PurchaseStatus
Extra
PurchaseResponse.SUCCESSFUL
purchased
pendingCompletePurchase = true
PurchaseResponse.PENDING
pending
Requires enablePendingPurchases() to occur at all; final receipt arrives via purchase updates.
PurchaseResponse.FAILED
canceled
Amazon documents FAILED as "customer exited before completing". Expose rawStatus for the rare genuine error.
PurchaseResponse.ALREADY_PURCHASED
error (code: already_purchased)
Plus trigger getPurchaseUpdates(true) so the owned item is re-emitted as restored, as Amazon recommends.
PurchaseResponse.INVALID_SKU / NOT_SUPPORTED
error
code: invalid_sku / not_supported
Receipt with isCanceled == true
canceled
From purchase updates; cancelDate in wrapper.
Field mapping
ProductDetails
Source
id
sku
title, description
title, description (already localized)
price
price string verbatim
rawPrice
Parsed from price using the marketplace locale's decimal/grouping separators; 0.0 if unparseable, priceIsEstimated = true
currencyCode
Lookup table keyed by UserData.marketplace (US→USD, UK→GBP, DE/FR/IT/ES/NL→EUR, JP→JPY, CA→CAD, AU→AUD, BR→BRL, MX→MXN, IN→INR, …); empty string when unknown
currencySymbol
Leading/trailing non-numeric run of price
PurchaseDetails
Source
purchaseID
receiptId
productID
sku (for subscriptions: the term SKU; parent SKU is on the wrapper)
transactionDate
purchaseDate millis as string
verificationData.localVerificationData
receipt.toJSON()
verificationData.serverVerificationData
receiptId — the token RVS wants; userId is on AmazonPurchaseDetails because RVS needs both
verificationData.source
'amazon_appstore'
7. Open decisions and known gaps
Price fidelity. The inferred rawPrice/currencyCode are the one place this implementation is lossy versus Play/StoreKit. Decision: ship the heuristic, flag it with priceIsEstimated, and tell apps that need exact amounts to read them from their own catalogue or from RVS. Do not silently return plausible-looking wrong numbers without the flag.
FAILED → canceled. Chosen because that is what it means 99% of the time and because canceled is what UIs should do with it. The raw status is preserved. Alternative (map to error) would make every user back-out show an error dialog in apps written against Play semantics.
Subscription upgrades. No equivalent of ChangeSubscriptionParam. A GooglePlayPurchaseParam passed to this platform is ignored with a debug warning; modifySubscription() can be added to the addition later for partners who have tiered subscriptions enabled.
Auto-sync on resume default. On by default because Amazon calls it mandatory and apps written against the interface would otherwise silently miss approved pending purchases. Can be disabled via the addition for apps that call restorePurchases() themselves.
Idempotency. The plugin dedupes receipts within one process lifetime by receiptId before emitting. Cross-launch dedupe is the app's job (as on every store) and the README says so.
Receipt verification. Out of scope client-side (Amazon says server-side RVS only). README includes the RVS request shape and links the sandbox endpoint. Expose isSandboxMode() so backends can be pointed at the RVS sandbox during App Tester runs.
Endorsement. Assume none. flutter/packages hosts only first-party store implementations; design and document as a non-endorsed override, which is precisely what #137040 was built for.
Package name. Confirm in_app_purchase_amazon is free on pub.dev; otherwise in_app_purchase_amazon_appstore. Publish under a verified publisher so it can be found via "packages that implement in_app_purchase".
8. Testing
Dart unit tests against the Pigeon-generated TestAmazonBillingApi mock: every mapping row above becomes a test; router pagination, dedupe, orphaned-response handling, chunked getProductData.
Kotlin unit tests for enum/JSON conversion with Robolectric (the SDK classes are plain Java; no device needed for conversion tests).
Device integration on a Fire tablet with Amazon App Tester and an amazon.sdktester.json pushed to /sdcard/: consumable, entitlement, subscription (incl. free trial + promo), cancel-in-dialog, pending purchase via Amazon Kids profile, already-purchased, restore after data clear. Automate with integration_test plus manual steps for the dialog interactions. Fire TV Stick (Android-based) for D-pad flow sanity.
Live App Testing (Amazon LAT) before first release: production RVS, real marketplace pricing strings for the parser table (collect at least US, UK, DE, JP, IN samples).
CI: flutter analyze, unit tests, example app builds in both Mode A and Mode B, Flutter stable and beta.
9. Milestones and effort
Milestone
Deliverable
Estimate
M0 — spike
Bare plugin, getUserData round-trip through Pigeon on a Fire tablet with App Tester; confirms manifest merge, PEM handling, override registration on Flutter 3.27+
2–3 days
M1 — core
Full Pigeon API, router, InAppPurchaseAmazonPlatform with all interface methods, wrappers, unit tests
1.5–2 weeks
M2 — parity polish
Addition API, auto-sync, pending purchases, price parser + marketplace table, dedupe, example app mirroring the upstream example
1 week
M3 — release
_default shim package, README/migration guide, LAT run, publish 0.1.0
3–5 days
Roughly 4–5 weeks for one engineer familiar with Flutter plugins and Android, most of it in M1 and in device testing. The native bridge itself is a few hundred lines; the time goes into the semantic edges and the test matrix.
10. Risks
Risk
Level
Mitigation
Shrinking target: no non-Fire Android since Aug 2025; new Fire TV hardware on Vega OS (no Flutter)
medium
Be explicit that the package serves Fire tablets + Android-based Fire TV. Don't over-invest in TV-specific work.
Amazon changes the SDK (they did five point releases 2022–2026, all additive)
low
Pin the SDK version; raw JSON passthrough means new fields are reachable before the wrapper is updated.
Interface additions in in_app_purchase_platform_interface
low
Base class ships default implementations; track the changelog, implement when relevant.
Override requires Flutter ≥ 3.27; older toolchains register both plugins in alphabetical order (Amazon would lose)
low
Set the SDK constraint; Mode B works on older Flutter anyway.
Amazon review objecting to Play Billing classes in a single binary (Mode B)
medium
Recommend Mode A; document Mode B as at-your-own-risk.
Price heuristic wrong for an unlisted marketplace
medium
priceIsEstimated flag; table extendable at runtime via the addition; never crash on parse failure.
Local testing friction (App Tester, no emulator sandbox)
medium
Ship the sdktester JSON with the example; keep the pure-Dart mapping layer fully unit-testable without a device.
Bus factor
high
Small surface, heavy tests, and a design doc (this one) so the package can be handed over.