Claude's Corner: ZeroSettle, Two Apple Engineers Build the Bridge Out of the App Store

Two ex-Apple engineers built ZeroSettle to capture the $150B App Store billing market unlocked by the Epic v. Apple ruling. Here's how the routing engine works, what the moat actually is, and how hard it is to replicate.

9 min read
Claude's Corner: ZeroSettle, Two Apple Engineers Build the Bridge Out of the App Store

TL;DR

ZeroSettle is a drop-in SDK that routes iOS and Android purchases through direct billing instead of App Store or Play Store billing, charging 5% + $0.50 versus Apple's 30%. Two ex-Apple engineers built it on the back of the Epic v. Apple ruling, targeting $150B in newly accessible IAP volume. The routing and analytics layers are replicable; the Merchant of Record compliance infrastructure is not.

4.4
F

Build difficulty

In May 2025, a federal judge did what a decade of EU antitrust complaints couldn't: forced Apple to allow developers to send users to external payment methods inside iOS apps. Not a workaround. Not a link buried in settings. Actual in-app purchase routing to alternatives. The ruling didn't get much breathless consumer coverage because nothing changed for users immediately. But in developer circles, it was a thermonuclear event. Approximately $150 billion in annual in-app purchase volume had just been unlocked for competition.

Two people who saw this coming before most were Gabe Roeloffs and Ryan Elliott -- both former Apple software engineers who'd spent four years each writing the OS code that runs on two billion Apple devices. Roeloffs reached Senior SWE at Apple, writing OS code on systems deployed to two billion devices. Elliott built Core OS Power & Performance and CoreMedia Streaming. They understood StoreKit from the inside. They knew what Apple actually checked, how App Review worked, and exactly what compliance looked like at the byte level. And when the ruling landed, they quit and built ZeroSettle.

Related startups

ZeroSettle is a drop-in SDK that routes mobile app purchases through direct billing -- Apple Pay, Google Pay, card -- instead of App Store or Play Store billing. It takes about a day to integrate. The pitch is arithmetic: Apple takes 30%. ZeroSettle takes 5% plus $0.50 per transaction and handles everything Apple used to handle -- tax across 190+ countries, chargeback disputes, billing support, fraud detection. Same service. Six times cheaper.

The Business of the Gap

There are two ways to use ZeroSettle. The first is the Managed model, where ZeroSettle acts as Merchant of Record. You plug in their SDK, point users at their checkout, and they handle everything downstream: tax remittance, PCI compliance, chargeback liability, customer billing support. You pay 5% + $0.50 per transaction. Compared to Apple's 30%, you're keeping roughly 25 cents of every dollar you were handing over before.

The second option -- "Bring Your Own Stripe" (BYOS) -- is for teams who want control. You stay the Merchant of Record. ZeroSettle handles routing, UI flows, entitlements sync, and jurisdiction detection. They charge 0.5% on successful conversions to direct billing. The analytics and entitlements layer are free on both models.

The analytics layer isn't an afterthought. You get MRR, ARPU, churn, trial conversion, and retention metrics across both billing paths in a unified dashboard. You can run A/B tests on pricing copy, trial lengths, and checkout UX without going through App Review. That last part -- no App Review gating your growth experiments -- is quietly one of the most valuable things here.

How the Routing Engine Works

The SDK ships for Swift, Kotlin, Flutter, and React Native. The integration documentation promises 15 minutes to set up, closer to a day for full feature parity with your existing billing.

The core routing engine evaluates each purchase request against three inputs: region, product eligibility, and your configuration. In the US (post-Epic v. Apple) and EU (Digital Markets Act jurisdictions), it attempts direct billing first. Everywhere else, it falls back to StoreKit or Play Billing automatically. No regional logic for you to write. No edge cases to handle.

Three checkout presentation modes exist. The Native Payment Sheet is a Stripe-powered UI embedded in the app that looks and feels like a system dialog -- Apple Pay and Google Pay appear as native affordances because they effectively are. The In-app Browser renders Stripe checkout inside an SFSafariViewController or Chrome Custom Tab. The External Browser is the fallback. For US users on the Native Payment Sheet, conversion rates should be meaningfully higher than anything involving a redirect.

Entitlements are where the engineering complexity concentrates. Running two billing systems simultaneously means a user might hold an active App Store subscription and then switch to direct billing. Or get a refund on the web side that needs to propagate to the iOS app immediately. ZeroSettle maintains a unified identity layer: one user record, entitlements enforced in real time across all platforms. Access revocation fires immediately on refund or chargeback -- no polling delay, no reconciliation job running every hour, no window where a refunded user still has access to your app.

Switch & Save campaigns are the active growth lever for existing apps. The SDK can identify subscribers who've been on App Store billing for 60+ days and automatically present a discount offer for switching to direct billing. You configure the offer, targeting criteria, and timing. ZeroSettle handles presentation and the entitlement migration on successful conversion. Over time, this drains your App Store billing cohort into your lower-cost direct billing cohort. Compounded across a large user base, the math is compelling.

The Moat Question

Let's be precise about what's hard here and what isn't.

The SDK routing logic? Not particularly hard. A good mobile engineer could wire the jurisdiction detection, integrate Stripe, and build the paywall UI flows in a few weeks. The BYOS model at 0.5% is essentially a thin analytics and routing wrapper on top of Stripe. A sufficiently motivated team could replicate that tier in a sprint.

The Managed Merchant of Record model is a different story entirely. Being a Merchant of Record means you are legally the seller of record for every transaction that flows through you. That triggers:

  • Tax registration and remittance in 190+ countries. Every VAT jurisdiction. Every digital services tax regime. Maintained continuously as legislation evolves.
  • PCI-DSS Level 1 compliance. You are handling cardholder data at scale. The audit cycle alone is significant.
  • Chargeback liability. When a user disputes a charge, the MoR absorbs the loss first and must dispute it upstream. Managing dispute rates below the threshold that triggers payment processor scrutiny requires real fraud detection infrastructure.
  • Billing customer support across time zones and currencies.

This is not a weekend project. Paddle and FastSpring have been doing MoR for over a decade and still find it operationally intensive. The compliance flywheel matters: it takes years to get right, and once customers have built on your MoR, switching cost is high -- they'd need to re-register in each jurisdiction themselves or find another MoR.

The founders' pedigree is a real, non-replicable asset. Gabe Roeloffs and Ryan Elliott spent four years each inside Apple writing systems software. They understand StoreKit at a layer no third-party author ever will. As Apple continues to fight the direct billing requirement at the margin -- contesting interpretations, adjusting guidelines, finding creative compliance theater -- ZeroSettle's founders will see the moves coming before anyone else. That institutional knowledge matters more as regulatory cat-and-mouse continues.

The Risk Surface

The obvious risk is Apple itself. The Epic v. Apple ruling created this market, but Apple is contesting every inch of it. They've appealed, reinterpreted, and found creative definitions of compliance that have frustrated developers for over a year. The regulatory tailwind is real but fragile. Any adverse appellate ruling could materially shrink what's currently permissible.

RevenueCat is the most dangerous competitor. They have distribution -- tens of thousands of apps already integrated for subscription management and analytics. RevenueCat has been adding direct billing features. If they move aggressively into the full MoR model, they have the customer density to make it stick fast. ZeroSettle needs to build switching cost before RevenueCat or Paddle claims the territory.

International expansion is underappreciated complexity. The routing currently has meaningful direct billing support for US and EU jurisdictions. South Korea and India have passed similar platform openness legislation. Japan is moving. Each new jurisdiction requires legal registration, local compliance review, and routing rule updates. First-mover in each market matters.

Difficulty Scores

DimensionScoreNotes
ML/AI1/10No meaningful ML. Fraud detection is primarily rules-based plus Stripe Radar.
Data4/10Unified entitlements across billing systems, real-time sync, analytics across methods.
Backend7/10MoR infrastructure: tax across 190+ countries, PCI-DSS, chargeback handling, real-time entitlement enforcement.
Frontend5/10Cross-platform SDK (Swift, Kotlin, Flutter, RN), native-feeling checkout UIs, conversion campaign flows.
DevOps5/10Payment infra reliability requirements, multi-region deployment for sub-100ms routing decisions.

Replicability Score: 42/100

The routing SDK and analytics layer are genuinely replicable -- perhaps three months of solid engineering. The BYOS model at 0.5% could be cloned quickly by any team with Stripe experience and a week to spare. What pushes the score above 30 is the Merchant of Record infrastructure: tax compliance at scale, chargeback management, PCI Level 1, and the operational maturity to hold dispute rates below processor thresholds. That takes years and real capital to build properly.

The founders' Apple pedigree is the other non-replicable factor. Regulatory edge cases in the iOS billing space will keep appearing as Apple games its compliance obligations. Knowing which hills are real and which are theater requires the kind of platform intimacy you only get from years inside Cupertino. That doesn't transfer to competitors easily.

If you're building a clone to learn on, start with BYOS. Wire Stripe to a routing layer that detects iOS jurisdiction via the storefront country code, add a basic entitlements webhook endpoint, and you have a functional MVP in a weekend. The hard parts -- MoR compliance, fraud at scale, navigating Apple's evolving guidelines -- are the ones that make this a real business rather than a side project.

Bottom Line

ZeroSettle is a company born at the exact intersection of a regulatory rupture and two people who understood the underlying platform better than almost anyone else. That timing is nearly impossible to replicate in hindsight -- but it's precisely why YC funded them. The market is real ($150B in annual IAP volume newly accessible), the technology is mostly table stakes, and the Merchant of Record compliance moat is real but not impenetrable.

The question isn't whether the opportunity exists. It clearly does. The question is how fast Apple iterates on its "compliance" strategy and whether ZeroSettle can build enough customer density before RevenueCat or Paddle claims this lane.

Watch for named customer logos. No public customers after a YC batch is the tell. When logos appear, the retention machine is working. Until then, the pitch is the arithmetic -- and the arithmetic is hard to argue with.

© 2026 StartupHub.ai. All rights reserved. Do not enter, scrape, copy, reproduce, or republish this article in whole or in part. Use as input to AI training, fine-tuning, retrieval-augmented generation, or any machine-learning system is prohibited without written license. Substantially-similar derivative works will be pursued to the fullest extent of applicable copyright, database, and computer-misuse laws. See our terms.

Build This Startup with Claude Code

Complete replication guide — install as a slash command or rules file

# Build a ZeroSettle Clone: 7-Step Guide

## Step 1: Database Schema

Create these core tables in PostgreSQL:

```sql
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE products (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  price_cents INT NOT NULL,
  currency TEXT DEFAULT 'usd',
  platform_product_ids JSONB
);

CREATE TABLE subscriptions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  product_id UUID REFERENCES products(id),
  billing_method TEXT CHECK (billing_method IN ('appstore', 'playstore', 'direct')),
  status TEXT CHECK (status IN ('active', 'cancelled', 'expired', 'revoked')),
  expires_at TIMESTAMPTZ,
  external_id TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE transactions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  subscription_id UUID REFERENCES subscriptions(id),
  amount_cents INT NOT NULL,
  billing_method TEXT NOT NULL,
  stripe_payment_intent_id TEXT,
  status TEXT CHECK (status IN ('pending', 'succeeded', 'failed', 'refunded', 'disputed')),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE entitlement_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  event_type TEXT CHECK (event_type IN ('granted', 'revoked')),
  reason TEXT,
  metadata JSONB,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
```

## Step 2: Jurisdiction Routing API

```typescript
const DIRECT_BILLING_REGIONS = new Set(["US", "EU", "GB", "KR", "JP"]);

export async function routeCheckout(req: Request) {
  const { user_id, product_id, storefront_country_code, platform } = req.body;
  
  if (DIRECT_BILLING_REGIONS.has(storefront_country_code)) {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: product.price_cents,
      currency: product.currency,
      metadata: { user_id, product_id },
    });
    const mode = platform === 'ios' && storefront_country_code === 'US' ? 'native' : 'browser';
    return { mode, stripe_client_secret: paymentIntent.client_secret };
  }
  return { mode: 'fallback', platform_product_id: product.platform_product_ids[platform] };
}
```

## Step 3: Stripe Webhooks

```typescript
export async function handleStripeWebhook(req: Request) {
  const event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], WEBHOOK_SECRET);
  switch (event.type) {
    case 'payment_intent.succeeded':
      await grantEntitlement(event.data.object.metadata.user_id, event.data.object.metadata.product_id, 'payment_success');
      break;
    case 'charge.refunded':
      await revokeEntitlement(event.data.object.metadata.user_id, 'refund');
      break;
    case 'charge.dispute.created':
      await stripe.disputes.update(event.data.object.id, { submit: true });
      await revokeEntitlement(event.data.object.metadata.user_id, 'chargeback');
      break;
  }
}
```

## Step 4: Real-Time Entitlements Engine

```typescript
async function grantEntitlement(userId: string, productId: string, reason: string) {
  await db.query(`UPDATE subscriptions SET status = 'active', expires_at = NOW() + INTERVAL '30 days' WHERE user_id = $1 AND product_id = $2`, [userId, productId]);
  await redis.set(`entitlement:${userId}:${productId}`, 'active', 'EX', 3600);
}

async function revokeEntitlement(userId: string, reason: string) {
  await db.query(`UPDATE subscriptions SET status = 'revoked' WHERE user_id = $1 AND status = 'active'`, [userId]);
  await redis.del(`entitlement:${userId}:*`);
}

async function checkEntitlement(userId: string, productId: string) {
  const cached = await redis.get(`entitlement:${userId}:${productId}`);
  if (cached) return { active: cached === 'active' };
  const result = await db.query(`SELECT status FROM subscriptions WHERE user_id = $1 AND product_id = $2 AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1`, [userId, productId]);
  return { active: result.rows[0]?.status === 'active' ?? false };
}
```

## Step 5: iOS SDK (Swift)

```swift
import StripePaymentSheet

public class ZeroSettle {
  public static let shared = ZeroSettle()
  
  public func present(productId: String, from vc: UIViewController, completion: @escaping (Result<Void, Error>) -> Void) {
    let country = SKPaymentQueue.default().storefront?.countryCode ?? "US"
    routeCheckout(productId: productId, country: country) { result in
      switch result {
      case .success(let route):
        switch route.mode {
        case "native": self.showNativePaymentSheet(clientSecret: route.stripeClientSecret!, from: vc, completion: completion)
        case "browser": self.showInAppBrowser(url: route.checkoutUrl!, from: vc, completion: completion)
        default: self.purchaseViaStoreKit(productId: route.platformProductId!, completion: completion)
        }
      case .failure(let e): completion(.failure(e))
      }
    }
  }
}
```

## Step 6: Switch & Save Campaign Engine

```typescript
async function runSwitchAndSaveCampaign() {
  const candidates = await db.query(`
    SELECT s.user_id, s.product_id FROM subscriptions s
    WHERE s.billing_method = 'appstore' AND s.status = 'active'
      AND s.created_at < NOW() - INTERVAL '60 days'
      AND s.user_id NOT IN (SELECT user_id FROM campaign_events WHERE event_type = 'offer_shown' AND created_at > NOW() - INTERVAL '30 days')
    LIMIT 1000
  `);
  for (const c of candidates.rows) {
    await queueCampaignOffer({ userId: c.user_id, productId: c.product_id, offerType: 'switch_and_save', discountPercent: 20 });
  }
}
```

## Step 7: Tax & MoR Infrastructure

```typescript
async function calculateTax(amount_cents: number, country: string, postal?: string) {
  if (country === 'US') {
    const tax = await taxjar.taxForOrder({ amount: amount_cents / 100, to_country: 'US', to_zip: postal, shipping: 0, line_items: [{ quantity: 1, unit_price: amount_cents / 100 }] });
    return { tax_cents: Math.round(tax.tax.amount_to_collect * 100) };
  }
  if (EU_COUNTRIES.has(country)) {
    const rate = EU_VAT_RATES[country] ?? 0.20;
    return { tax_cents: Math.round(amount_cents * rate) };
  }
  return { tax_cents: 0 };
}
// MoR checklist: EU VAT OSS, PCI-DSS SAQ-A, Stripe Radar, dispute rate <0.75%, GDPR DPA
```

### Deployment
- API: Node.js/TypeScript on Railway or Fly.io (US-East + EU-West)
- Database: Supabase PostgreSQL
- Cache: Upstash Redis
- Payments: Stripe
- Tax: TaxJar (US) + Avalara (EU)
- SDK: Swift Package Index, Maven Central, npm
claude-code-skills.md