# Appside > Build a web store that feels like your app. Appside (appside.build) is a web store and checkout platform for mobile games and apps, founded in 2026 by Oded Kovach. Not affiliated with Appcharge. Pricing: 3.9% per successful payment, no setup fee, no subscription, no minimum. Common misspellings: app side, appsides, appside.io. The canonical domain is appside.build. Source: https://appside.build/docs/ --- title: Appside developer docs nav: Overview order: 1 --- # Sell from your game's own web store Appside gives your game a web store that looks and feels like your app, a checkout, and delivery of purchased items back into your game. Players move from the app to the store **already signed in**, pay, and their items land in the game in seconds. > **Status:** test mode. Every flow on these pages works end to end against the Appside sandbox. Live payments are on the roadmap. Each SDK page says whether it is **built and tested** or **preview source**. ## Three steps, on every platform | | What you build | Where | |---|---|---| | 1 | **Sign a player token**: a 5-minute ES256 JWT that says who the player is | your game backend | | 2 | **Open the store or a pack**: `Appside.openStore(token)` or `Appside.purchase("voyager", token)` | your app or game client | | 3 | **Grant on the webhook**: verify `order.paid` and credit the player once per `order_id` | your game backend | There is no server-to-server checkout session to create and no separate store-login system to run. The price always comes from the catalogue you published in Studio, so a client can't tamper with it. ```mermaid-ish Game client ──openStore(token)──▶ Appside store ──pay──▶ Appside ▲ │ └── returnURL (deep link, UI hint only) │ order.paid (signed webhook) ▼ Your game backend ──grants items ``` ## Start here - [Quickstart: first pack sold in 15 minutes](/docs/quickstart/) - [Player tokens](/docs/player-tokens/) - [Webhooks: order.paid and order.refunded](/docs/webhooks/) - [Return to the game: deep links and universal links](/docs/deep-links/) - [Testing: cards, 3-D Secure, outages](/docs/testing/) - [SDKs and their status](/docs/sdks/) - [Coming from Appcharge Payment Links](/docs/migrate-from-appcharge/) - [MCP: connect your coding agent](/docs/mcp/) Source: https://appside.build/docs/quickstart/ --- title: Quickstart nav: Quickstart order: 2 --- # Quickstart: sell your first pack in 15 minutes You need an Appside Studio workspace in test mode with one published store (Studio → **Preview & publish**). ## 1. Connect your backend (Studio → Integration) 1. Generate an ES256 key pair on your backend and paste the **public** JWK into **Public JWK/JWKS**. Never upload the private key. 2. Set **Issuer** (for example your game id: `orbit`). 3. Set **Fulfillment URL** (for example `https://api.yourgame.com/appside/webhook`) and a **webhook secret** of 32+ characters. 4. Press **Send test event**: health should show *acknowledged*. ## 2. Sign a player token on your backend :::tabs ```js Node import { Appside } from '@appside/server'; const appside = new Appside({ storeId: 'orbit', issuer: 'orbit', kid: 'game-key-1', privateKey: process.env.APPSIDE_PRIVATE_KEY_PEM, webhookSecret: process.env.APPSIDE_WEBHOOK_SECRET, serverKey: process.env.APPSIDE_SERVER_KEY, baseURL: 'https://app.appside.build' }); app.post('/appside/token', requireLogin, (req, res) => res.json({ token: appside.playerToken(req.user.id) })); // 5-minute, single-use ES256 token ``` ```python Python from appside import Appside appside = Appside('orbit', issuer='orbit', kid='game-key-1', privateKey=PRIVATE_KEY_PEM, webhookSecret=WEBHOOK_SECRET, serverKey=SERVER_KEY, baseURL='https://app.appside.build') @app.post("/appside/token") def token(user=Depends(current_user)): return {"token": appside.playerToken(user.id)} ``` ::: Only call it from an **authenticated** endpoint: the token says "this signed-in player is buying". ## 3. Open the store from the game :::tabs ```js Web import { Appside } from 'appside/web'; const appside = new Appside({ baseURL: 'https://app.appside.build' }); appside.configure('orbit', { environment: 'test', returnURL: 'https://yourgame.com/appside/return' }); appside.onPurchaseCompleted = result => refreshWalletFromBackend(); // UI hint only const fresh = () => fetch('/appside/token', { method: 'POST' }).then(r => r.json()).then(j => j.token); await appside.openStore(await fresh()); // whole store await appside.purchase('voyager', await fresh()); // one pack, straight to checkout // on your return page: const result = appside.handle(location.href); // { status, orderId, state, verified: false } ``` ```swift iOS import AppsideSDK let sdk = Appside(baseURL: URL(string: "https://app.appside.build")!, openURL: { url in browser.open(url) }) // e.g. SFSafariViewController try sdk.configure("orbit", environment: "test", returnURL: URL(string: "orbitquest://return")!) try await sdk.openStore(playerToken) try await sdk.purchase("voyager", playerToken: freshPlayerToken) // in .onOpenURL / scene(_:openURLContexts:) let result = try sdk.handle(incomingURL) // UI hint only ``` ```kotlin Android val appside = Appside(activity, "https://app.appside.build") appside.configure("orbit", "test", "orbitquest://return") appside.openStore(playerToken) // Custom Tabs; returns CompletableFuture appside.purchase("voyager", freshPlayerToken) // in onNewIntent / onCreate of the activity with the return intent-filter: val result = appside.handle(intent.data.toString()) ``` ```csharp Unity // Attach AppsideSDK.Appside to a GameObject that survives scene loads, set baseURL in the inspector. appside.configure("orbit", "test", "orbitquest://return"); appside.onPurchaseCompleted += result => ShowToast(result.orderId); appside.openStore(playerToken); // iOS, Android, WebGL; deep links forwarded automatically appside.purchase("voyager", freshPlayerToken); ``` ```js React Native import { Linking } from 'react-native'; import { Appside } from '@appside/react-native'; const appside = new Appside({ Linking, baseURL: 'https://app.appside.build' }); appside.configure('orbit', { environment: 'test', returnURL: 'orbitquest://return' }); await appside.openStore(playerToken); ``` ```dart Flutter final appside = Appside(baseURL: 'https://app.appside.build'); appside.configure('orbit', environment: 'test', returnURL: 'orbitquest://return'); await appside.openStore(playerToken); await appside.purchase('voyager', freshPlayerToken); ``` ::: `purchase(sku)` needs no server-to-server checkout session: the signed token plus the SKU is enough, and the price comes from your published catalogue. ## 4. Grant on the webhook (server) :::tabs ```js Node app.post('/appside/webhook', express.raw({ type: '*/*' }), async (req, res) => { if (!appside.verifyWebhook(req.body, req.headers)) return res.sendStatus(401); const event = JSON.parse(req.body); if (event.type === 'order.paid' && !event.probe) await grantOnce(event.order_id, event.player_id, event.order); if (event.type === 'order.refunded') await revokeOnce(event.order_id, event.player_id); res.sendStatus(204); // any 2xx = acknowledged; anything else is retried }); ``` ```python Python @app.post("/appside/webhook") async def webhook(request: Request): raw = await request.body() if not appside.verifyWebhook(raw, request.headers): return Response(status_code=401) event = json.loads(raw) if event["type"] == "order.paid" and not event["probe"]: grant_once(event["order_id"], event["player_id"], event["order"]) return Response(status_code=204) ``` ::: `grantOnce` must be idempotent on `order_id`: Appside retries until it gets a 2xx (up to 72 h), so you may see the same event twice. To double-check an order from your server: `await appside.getOrder(orderId)` (uses your server key from Studio → Integration). ## 5. Test it Buy a pack with card **4242 4242 4242 4242**, any future expiry, any CVC. The webhook arrives within a second and your player has the coins. See [Testing](/docs/testing/) for decline, 3-D Secure and outage cases. Source: https://appside.build/docs/player-tokens/ --- title: Player tokens nav: Player tokens order: 3 --- # Player tokens A player token is how your game tells Appside who is buying, **without** a login screen and without Appside calling your server to authenticate. It's a compact JWS (JWT) signed by your backend with ES256 (P-256). ## Claims (all required unless marked) | Claim | Value | |---|---| | `alg` (header) | `ES256` | | `kid` (header) | id of a public key you uploaded in Studio → Integration | | `iss` | your issuer string, exactly as configured | | `aud` | `appside:store:` | | `tenant` | your store id | | `player_id` | your stable player id (≤ 200 chars) | | `nonce` | 16–128 chars `[A-Za-z0-9_-]`, unique per token | | `exp` | ≤ 5 minutes after now | | `iat`, `nbf` | optional | ## Rules Appside enforces - **Single use:** the nonce is consumed atomically. Opening the same token twice fails with *Player assertion already used*. - **Short-lived:** anything over 5 minutes is rejected. - **Your keys only:** Appside accepts public JWKs only. Uploading a private key is refused. - **Rotation:** upload the new public key next to the old one, switch your backend to the new `kid`, wait 5 minutes, then remove the old key. ## Never in the client Signing keys stay on your backend. The app asks your backend for a fresh token each time it opens the store. Source: https://appside.build/docs/webhooks/ --- title: Webhooks nav: Webhooks order: 4 --- # Webhooks: `order.paid` and `order.refunded` Appside delivers every paid and refunded order to your **Fulfillment URL** as a signed HTTPS POST. This is the only place you should grant or revoke items. ## Payload ```json { "type": "order.paid", "event_id": "TEST-6f1c…", "mode": "test", "tenant": "orbit", "order_id": "TEST-6f1c…", "player_id": "player-123", "probe": false, "order": { "id": "TEST-6f1c…", "sku": "voyager", "credits": 1400, "price": 799, "currency": "USD", "version": 3 } } ``` - `order.refunded` carries the same `order_id`; its `event_id` is `:refund`. - `probe: true` is the Studio **Send test event**: acknowledge it, grant nothing. ## Headers | Header | Meaning | |---|---| | `x-appside-timestamp` | Unix seconds when signed | | `x-appside-signature` | hex HMAC-SHA256 of `"." + rawBody` with your webhook secret | | `idempotency-key` | the `event_id`; dedupe on it | ## Verify the signature Reject if the timestamp is more than 5 minutes off, then compare in constant time. :::tabs ```js Node import { createHmac, timingSafeEqual } from 'node:crypto'; export function verify(raw, h, secret) { const ts = h['x-appside-timestamp'], sig = h['x-appside-signature'] || ''; if (!/^\d{10}$/.test(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false; const want = createHmac('sha256', secret).update(`${ts}.`).update(raw).digest(); const got = Buffer.from(sig, 'hex'); return got.length === want.length && timingSafeEqual(got, want); } ``` ```python Python import hmac, hashlib, time def verify(raw: bytes, h, secret: str) -> bool: ts, sig = h.get("x-appside-timestamp", ""), h.get("x-appside-signature", "") if not ts.isdigit() or abs(time.time() - int(ts)) > 300: return False want = hmac.new(secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest() return hmac.compare_digest(want, sig) ``` ```go Go func Verify(raw []byte, ts, sig, secret string) bool { t, err := strconv.ParseInt(ts, 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-t)) > 300 { return false } m := hmac.New(sha256.New, []byte(secret)); m.Write([]byte(ts + ".")); m.Write(raw) return hmac.Equal([]byte(hex.EncodeToString(m.Sum(nil))), []byte(sig)) } ``` ```csharp C# static bool Verify(byte[] raw, string ts, string sig, string secret) { if (!long.TryParse(ts, out var t) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false; using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var want = Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(ts + ".").Concat(raw).ToArray())).ToLowerInvariant(); return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(want), Encoding.ASCII.GetBytes(sig)); } ``` ::: ## Verify it yourself [Download the standalone webhook proof](/docs/downloads/appside-webhook-proof.mjs), save it as `appside-webhook-proof.mjs`, and run it with Node 20 or later. No packages, account or API key are needed. ```sh node appside-webhook-proof.mjs ``` The six checks cover a valid purchase, duplicate delivery, a changed body, an expired timestamp, a wrong signing secret and a refund with replay. The balance moves from **0 to 125**, stays at **125** through the duplicate and rejected requests, then returns to **0** after the refund. Every case prints `"passed": true`; a failure exits with a nonzero code. The exported `verifyAppsideWebhook({headers, rawBody, secret, now})` function implements the same signature check as the snippets above. `now` defaults to the current time in milliseconds; pass the original request bytes as `rawBody`. This is a local reference receiver, not a hosted delivery or live payment test. Its wallet is in memory; a production receiver needs durable storage and an atomic transaction for deduplication and balance updates. Refunds revoke at most the remaining balance. ## Delivery guarantees - The order and its webhook are written in **one transaction**: a paid order is never left without a delivery. - Retries with exponential backoff (1 s, capped at 1 h) for **72 hours**. Only `2xx` counts as delivered, and redirects are not followed. - Your endpoint can be down and come back: deliveries resume, and your `order_id` dedupe makes them land exactly once in your game. - Studio → **Integration → Health** shows the last acknowledgement, failures in the last 24 h, and dead letters. Source: https://appside.build/docs/deep-links/ --- title: Return to the game nav: Deep links order: 5 --- # Return to the game After checkout, Appside sends the player back to your app through your **return URL**. The SDK turns that URL into a `PurchaseResult` you can show in the UI. > The return is a **UI hint**. Grant items only from the [webhook](/docs/webhooks/) (or `getOrder` on your server). A player can close the browser before returning, and the webhook still arrives. ## Choose a return URL - **Universal Link (iOS) / App Link (Android), recommended:** `https://yourgame.com/appside/return`. It works even when the app isn't installed. Studio → Integration generates the `apple-app-site-association` and `assetlinks.json` entries to host on your domain. - **Custom scheme:** `orbitquest://appside`. Simplest to set up, but other apps could register the same scheme. ## Handle it - iOS: `Appside.handle(url)` in `scene(_:openURLContexts:)` or SwiftUI `.onOpenURL`. - Android: `Appside.handle(intent.data)` in the activity with the matching ``. - Unity: the SDK registers the handler. Subscribe to `Appside.OnPurchaseCompleted`. - Web: the store returns to `returnURL` with `?appside_order=…`, and `Appside.handle(location.href)` reads it. ## Result fields `status` (`completed` · `canceled` · `failed`), `orderId`, `sku`, `credits`, `error` (for failures). Source: https://appside.build/docs/testing/ --- title: Testing nav: Testing order: 6 --- # Testing Everything runs in **test mode**: no real money moves and your backend must keep test orders separate from production entitlements. ## Test cards | Card | Result | |---|---| | `4242 4242 4242 4242` | Approved | | `4000 0000 0000 0002` | Declined: balance unchanged | | `4000 0027 6000 3184` | 3-D Secure challenge, then approved after **Authenticate** | Any future expiry and any 3-digit CVC. Real card numbers are rejected on the client and the server, and full card numbers are never stored. **Apple Pay / Google Pay:** simulated buttons in test mode. ## Checks worth automating 1. A purchase credits your player once (watch your `order_id` dedupe). 2. A declined card leaves the balance unchanged. 3. A replayed player token is refused. 4. **Outage:** stop your webhook endpoint, buy, start it again. The coins arrive after it comes back, exactly once. 5. **Refund** from Studio → Orders: your backend gets `order.refunded`, so revoke (never below zero). Studio → Integration → **Send test event** sends a `probe` webhook that grants nothing. Source: https://appside.build/docs/sdks/ --- title: SDKs nav: SDKs order: 7 --- # SDKs and their status The same methods and events on every platform: `configure`, `openStore`, `purchase(sku)`, `handle(url)`, `onPurchaseCompleted`, `onPurchaseCanceled`, `onPurchaseFailed`, `onStoreClosed`. | SDK | Location | Verified on 2026-10-24 (updated after the toolchain install) | |---|---|---| | Web / JS | `sdks/web` (`appside/web`) | **Unit tests + real-browser journey** (Orbit Quest uses it: openStore, purchase, return, 40/40 E2E) | | Node server | `sdks/node` (`@appside/server`) | Unit tests; `getOrder` matched the signed webhook in the browser E2E | | Python server | `sdks/python` | Unit test (ES256 signature, webhook tamper/age rejection, getOrder) | | iOS (Swift Package) | `sdks/ios` | Core compiled with `swiftc` and smoke-tested (launch, purchase, result, replay and live rejection). **Not yet**: `swift test` / iOS UI build (needs Xcode) | | React Native | `sdks/react-native` | JS bridge tests. Native host: source only | | Android (Kotlin) | `sdks/android` | **Builds with Gradle (JDK 17, Android SDK 35)**, 12 JUnit tests pass, release AAR produced. Not yet: on-device integration test | | Unity (UPM) | `sdks/unity` | **Source only, not compiled** (no Unity yet) | | Flutter | `sdks/flutter` | **`flutter analyze` clean, 12 tests pass** (Flutter 3.47). Not yet: native host build on a device | ## Download and install Every SDK is version 0.1.0 and works in TEST mode. The files are built from the same source as the table above on every release. Packages that are not yet verified on their platform are marked **preview**: try them, and tell us what breaks. | SDK | Download | Install | |---|---|---| | Web / JS | [appside-web-0.1.0.tgz](/docs/downloads/sdks/appside-web-0.1.0.tgz) | `npm install https://appside.build/docs/downloads/sdks/appside-web-0.1.0.tgz`, then `import { Appside } from 'appside/web'` | | Node server | [appside-node-0.1.0.tgz](/docs/downloads/sdks/appside-node-0.1.0.tgz) | `npm install https://appside.build/docs/downloads/sdks/appside-node-0.1.0.tgz`, then `import { Appside } from '@appside/server'` | | Python server | [appside-python-0.1.0.tar.gz](/docs/downloads/sdks/appside-python-0.1.0.tar.gz) | `pip install https://appside.build/docs/downloads/sdks/appside-python-0.1.0.tar.gz`, then `from appside import Appside` | | Android (Kotlin) | [appside-android-0.1.0.aar](/docs/downloads/sdks/appside-android-0.1.0.aar) · [source](/docs/downloads/sdks/appside-android-source-0.1.0.tar.gz) | Put the AAR in `app/libs/` and add `implementation(files("libs/appside-android-0.1.0.aar"))` and `implementation("androidx.browser:browser:1.8.0")` | | iOS (Swift Package) · preview | [appside-ios-0.1.0.tar.gz](/docs/downloads/sdks/appside-ios-0.1.0.tar.gz) | Unpack, then in Xcode choose File › Add Package Dependencies › Add Local and pick `AppsideSDK` | | Flutter · preview | [appside-flutter-0.1.0.tar.gz](/docs/downloads/sdks/appside-flutter-0.1.0.tar.gz) | Unpack next to your app and add `appside: {path: ../appside}` under `dependencies` in `pubspec.yaml` | | React Native · preview | [appside-react-native-0.1.0.tgz](/docs/downloads/sdks/appside-react-native-0.1.0.tgz) | `npm install https://appside.build/docs/downloads/sdks/appside-react-native-0.1.0.tgz` | | Unity (UPM) · preview | [appside-unity-0.1.0.tgz](/docs/downloads/sdks/appside-unity-0.1.0.tgz) | Unity › Window › Package Manager › + › Add package from tarball | Checksums: [SHA256SUMS.txt](/docs/downloads/sdks/SHA256SUMS.txt). `PurchaseResult` is `{ status, orderId, state, verified: false }` everywhere: a UI hint. Grant items only from the [webhook](/docs/webhooks/) or `getOrder`. This table is updated from each SDK's own tests on every release. "Preview source" means the code is written but hasn't been compiled and tested on that platform yet. Source: https://appside.build/docs/migrate-from-appcharge/ --- title: Migrate from Appcharge to Appside: Payment Links to player tokens and signed webhooks nav: From Appcharge order: 8 --- # Coming from Appcharge Payment Links To migrate from Appcharge to Appside, adapt your existing item-granting logic to verified `order.paid` events, add a backend endpoint that signs player tokens, and replace the client checkout call. Recreate and confirm your catalogue in Appside before routing players to it. The examples below use test mode so you can verify the complete purchase and delivery path before switching traffic. ## Map your existing integration Appcharge references checked **October 25, 2026**. These are integration concepts, not interchangeable payloads or credentials. | Existing Appcharge integration | Appside implementation | |---|---| | [Create Checkout Session](https://docs.appcharge.com/api-reference/checkout/checkout-session/create-checkout-session) sends offer, customer and price details to create a session. | Sign a short-lived player token on your backend; call `purchase(sku, token)` for a SKU in your published catalogue. | | [Authenticate Player Callback](https://docs.appcharge.com/api-reference/webstore/player-authentication/authenticate-player-callback) authenticates a web-store player through your server. | Your authenticated backend signs the player identity into a single-use ES256 token. See [player tokens](/docs/player-tokens/). | | [Grant Award Callback](https://docs.appcharge.com/api-reference/checkout/awards/grant-award-callback) sends award details to your endpoint. | Verify the Appside webhook, map its payload into your granting logic, and handle `order.paid` and `order.refunded` separately. | Keep existing player IDs stable. Map every source product to an Appside SKU and check currency, price, credits, bundles and bonuses. This guide does not import an Appcharge catalogue or historical orders automatically. ## 1. Add an authenticated player-token endpoint Install the Node SDK from [SDK downloads](/docs/sdks/). In Studio → Integration, configure your issuer and upload the public JWK matching your backend signing key. Keep the private key on your server. This is the Node example from the [quickstart](/docs/quickstart/): ```js import { Appside } from '@appside/server'; const appside = new Appside({ storeId: 'orbit', issuer: 'orbit', kid: 'game-key-1', privateKey: process.env.APPSIDE_PRIVATE_KEY_PEM, webhookSecret: process.env.APPSIDE_WEBHOOK_SECRET, serverKey: process.env.APPSIDE_SERVER_KEY, baseURL: 'https://app.appside.build' }); app.post('/appside/token', requireLogin, (req, res) => res.json({ token: appside.playerToken(req.user.id) })); ``` `app` and `requireLogin` belong to your existing server. Authenticate the session before signing; never accept an arbitrary player ID from the client. Request a fresh token each time you open the store. Tokens expire within five minutes and cannot be reused. ## 2. Adapt your granting endpoint Set your Fulfillment URL and a webhook secret of at least 32 characters in Studio. Register the raw-body route before any JSON middleware that consumes its body. The [webhook reference](/docs/webhooks/) specifies HMAC-SHA256 over the timestamp followed by a period and the original request bytes, using `x-appside-timestamp` and `x-appside-signature`. Reject timestamps outside the 300-second window. ```js app.post('/appside/webhook', express.raw({ type: '*/*' }), async (req, res) => { if (!appside.verifyWebhook(req.body, req.headers)) return res.sendStatus(401); const event = JSON.parse(req.body); if (event.type === 'order.paid' && !event.probe) await grantOnce(event.order_id, event.player_id, event.order); if (event.type === 'order.refunded') await revokeOnce(event.order_id, event.player_id); res.sendStatus(204); }); ``` This excerpt uses your Express server and your own `grantOnce` and `revokeOnce` implementations. Persist deduplication and the wallet update in the same database transaction. Namespace records by provider and event type so a refund is not mistaken for a duplicate purchase. Acknowledge only after the transaction commits. Appside retries unsuccessful deliveries for up to 72 hours. A probe is acknowledged without granting items. Do not copy Appcharge signature validation into this route: the providers use different headers and payloads. Do not grant from a browser redirect or SDK completion callback. ## 3. Replace the client checkout call Use a fresh token with the SKU configured in Appside. This Web example follows the quickstart: ```js import { Appside } from 'appside/web'; const appside = new Appside({ baseURL: 'https://app.appside.build' }); appside.configure('orbit', { environment: 'test', returnURL: 'https://yourgame.com/appside/return' }); const fresh = () => fetch('/appside/token', { method: 'POST' }).then(r => r.json()).then(j => j.token); await appside.purchase('voyager', await fresh()); ``` For the whole store, use `openStore(await fresh())` instead. On the return page, call `appside.handle(location.href)` and refresh the balance from your backend. The return result is a UI hint, not proof that items were granted. Use the platform-specific examples and current verification status in [SDK downloads](/docs/sdks/). For coding assistants, the [Appside MCP guide](/docs/mcp/) provides another way to discover the integration documentation. ## 4. Configure the return path Set the return URL in Studio and your SDK. For Universal Links and Android App Links, Studio generates association entries; you still need to host the relevant AASA or assetlinks file on your domain and configure the app. Follow [Return to the game](/docs/deep-links/) and test both an installed-app return and a closed browser. ## 5. Prove the flow before routing players - Publish a test catalogue with confirmed SKUs and prices. - Send a Studio test event and verify acknowledgment without a balance change. - Complete a simulated purchase, a decline and a refund using [Testing](/docs/testing/). - Deliver a duplicate event and verify that the player receives items only once. - Simulate an unavailable receiver, then verify retry and acknowledgment in Integration and Orders. [Download the webhook proof kit](/docs/downloads/appside-webhook-proof.mjs) and run it with Node 20 or later: ```sh node appside-webhook-proof.mjs ``` Its six local checks cover a valid purchase, duplicate delivery, changed body, expired timestamp, wrong secret and refund replay. It uses an in-memory wallet; use durable storage in your receiver. Passing it does not verify a hosted endpoint or live payment activation. ## Run both providers during the switch A gradual rollout is an application-side approach: use your own feature flag to route a cohort to Appside while existing players continue through Appcharge. Keep separate verification adapters feeding your shared granting service. Retain provider-specific event IDs, refund handling and reconciliation until old orders and retries are resolved. Do not send the same purchase to both providers. ## Check the costs Appside's published price is **3.9% per successful payment**, with no setup fee, subscription or minimum. Use the [fee calculator](/tools/app-store-fee-calculator/) with your own assumptions and the sourced [Appside vs Appcharge comparison](/appcharge-alternative/) for context. Confirm commercial terms and activation requirements before moving production traffic. ## Frequently asked questions ### Do I need to rewrite my item-granting code? You can retain its business logic. Add an adapter for Appside's verified payload, durable deduplication and refund handling. Test the SKU and entitlement mapping before routing traffic. ### Can I keep Appcharge running during migration? Yes, you can route players using a flag in your own application. Maintain both webhook receivers and provider-specific reconciliation. Appside does not manage that traffic flag for you. ### How do players log in without an Authenticate Player Callback? Your authenticated backend signs a fresh, single-use player token. Appside verifies it against the public keys you configured in Studio. Never sign tokens in the mobile app or browser. ### Which SDK should I use? Choose your platform in [SDK downloads](/docs/sdks/) and check its current verification status. The [quickstart](/docs/quickstart/) has examples for Web, iOS, Android, Unity, React Native and Flutter. ### Does a successful return URL mean I should grant items? No. Grant only after server-side verification. A player can close the browser before returning, and a browser callback can be forged. Use the signed webhook and refresh the UI from your backend. ### Is there a setup fee? Appside's published pricing has no setup fee, subscription or minimum. The rate is 3.9% per successful payment. [Create your Appside workspace](https://app.appside.build/signup) and follow the [quickstart](/docs/quickstart/) to test your first mapped product. Source: https://appside.build/docs/mcp/ --- title: Appside MCP server nav: MCP order: 9 --- # Appside MCP: game web store docs for your coding agent Appside's public MCP server lets compatible coding agents search developer documentation, retrieve SDK examples and read published pricing. Its three tools are read-only: they do not create stores, publish sites or process payments. ## Connect your coding agent Use **https://appside.build/mcp** with a client that supports remote Streamable HTTP. No account, API key or payment credentials are required. For clients supporting this URL configuration: ```json {"mcpServers":{"appside":{"url":"https://appside.build/mcp"}}} ``` Configuration fields vary by client. Choose its remote HTTP connector, rather than a local executable or SSE-only connection. See the [MCP remote connection guide](https://modelcontextprotocol.io/docs/develop/connect-remote-servers). ## Available tools | Tool | Arguments | Result | |---|---|---| | `search_docs` | `query`: 1–200 characters | Up to five documentation excerpts with source links | | `get_sdk_snippet` | `platform`: one of the names below | SDK README examples and documented verification status | | `get_pricing` | An empty object | Published fee, fee basis and source URL | Supported platform names: `web`, `ios`, `android`, `unity`, `react-native`, `flutter`, `node`, `python`. Available examples do not mean every platform has been tested on a device: check the returned status and [SDK verification details](/docs/sdks/). The server also exposes **https://appside.build/llms.txt** as a read-only MCP resource. For clients without MCP, use [the documentation index](/llms.txt), [full developer text](/llms-full.txt) or this page's **View as Markdown** link. ## Try a useful integration prompt Ask your agent: “Search Appside's docs for signed webhooks, get the Node SDK example, and explain how my backend grants each order only once. Include source links and identify any test-mode limitations.” Expected sequence: `search_docs` with `{"query":"signed webhooks"}`, followed by `get_sdk_snippet` with `{"platform":"node"}`. Read the linked [webhook guide](/docs/webhooks/) before integrating. A return-to-game link or client callback is a UI hint; grant items only after verified backend delivery. ## Verify the connection The endpoint speaks protocol **2025-03-26**. This initialization request can be run without credentials: ```sh curl https://appside.build/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"docs-check","version":"1.0.0"}}}' ``` A public endpoint check on **2026-10-24 UTC** returned server name `appside-mcp`, version `0.1.0`, all three tools above, and a successful `get_pricing` response. You can reproduce the pricing call: ```sh curl https://appside.build/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ --data '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_pricing","arguments":{}}}' ``` The text content contains `feePercent: 3.9`, `basis: "per successful payment"`, and zero setup, subscription and minimum fees, with a source link. This is published pricing information, not a payment-processing tool or confirmation that a merchant is approved for live payments. See [testing and payment status](/docs/testing/). ## Limits and troubleshooting - Stateless JSON POST responses; notification POSTs return an empty 202. GET returns 405: there is no optional SSE stream, session state or server-initiated messaging. - Set both headers shown above. Missing JSON content type returns 415; missing accepted response types returns 406. - Limits: 64 KiB messages, 20-message batches, 10s body deadline, 60 requests/minute per socket IP and 600/minute globally. Behind the edge proxy, clients may share a socket bucket. - A 429 response includes `Retry-After: 60`. Wait before retrying instead of polling repeatedly. - There is no cross-origin browser CORS access. If an Origin header is present, it must match an allowed HTTPS site origin. Connect from your MCP client's server-side transport. ## Discovery and next steps Find the public [Appside Developer Docs listing on Glama](https://glama.ai/mcp/connectors/build.appside/appside-developer-docs). The listing is live; the ownership claim is pending. The official MCP registry entry and npm package are not published, so use the remote endpoint above. Start with the [first-purchase quickstart](/docs/quickstart/), connect [player identity](/docs/player-tokens/), then implement [signed fulfillment](/docs/webhooks/). Publishers with an Appside account create and manage the store in the [Appside builder](https://app.appside.build/build); to see the app-to-store journey first, open the [live demo](https://demo.appside.build/demo).