Portals
One bundle runs on filudi.com and on the three portals. At init() the SDK detects the host from the
portal’s global or hostname, loads the portal’s own script when the page does not have it, and maps every
call. Nothing portal-specific ships in your bundle. Each portal has its own listing process, outside Filudi;
this page covers what your game does once it runs there.
Side by side
Section titled “Side by side”| filudi.com | CrazyGames | Poki | Playgama | |
|---|---|---|---|---|
| Detection | The handshake in a frame | CrazyGames.SDK or *.crazygames.com | PokiSDK or *.poki.com, *.poki-gdn.com | window.bridge |
| Ads | None yet | Rewarded and midgame | rewardedBreak, commercialBreak | Rewarded and interstitial |
| Saves | The API | Data module, else localStorage | localStorage | bridge.storage |
| Scores | Signed, to your boards | user.submitScore with your key | None | bridge.leaderboards |
top() and me() | Work | unavailable | unavailable | Work with in_game boards |
| Player | Guest or account | The portal’s user, else a random id | Guest | The portal’s player |
| Locale | The site’s | user.systemInfo.locale | navigator.language | bridge.platform.language |
| Events | The API | Dropped unless events: 'always' | Same | Same |
| Extras | sdk.host.crazygames | sdk.host.poki | sdk.host.playgama |
CrazyGames
Section titled “CrazyGames”The SDK loads https://sdk.crazygames.com/crazygames-sdk-v3.js when the page does not have it, and falls
back to the mock when the script fails, takes longer than timeoutMs or reports the disabled
environment. ready() is game.loadingStop, the gameplay brackets are gameplayStart and
gameplayStop, happyTime() is happytime. Ads go through ad.requestAd('rewarded' | 'midgame');
ad.hasAdblock turns available() off. The portal paces interstitials, so the SDK adds no interval there.
Saves go to the data module under one key, filudi:save, holding { v, d }; with the module off they stay
in localStorage. Scores reach the portal board only with the key from the CrazyGames developer portal,
encrypted with AES-GCM; keep it in a build variable.
await sdk.init({ gameId: 'gloop', version: '1.4.0', crazygames: { leaderboardKey: import.meta.env.VITE_CG_LEADERBOARD_KEY ?? '' },});sdk.host.crazygames?.onMuteChange((muted) => audio.mute(muted));fullscreenButton.hidden = result.host === 'crazygames'; // the portal draws its ownconst result = await sdk.init({ gameId: 'gloop', version: '1.4.0', crazygames: { leaderboardKey: import.meta.env.VITE_CG_LEADERBOARD_KEY ?? '' },});sdk.host.crazygames?.onMuteChange((muted: boolean) => audio.mute(muted));fullscreenButton.hidden = result.host === 'crazygames'; // the portal draws its ownThe portal’s mute setting, invite links and game context are host extras. device()
reads user.systemInfo.device.type.
The SDK loads https://game-cdn.poki.com/scripts/v2/poki-sdk.js when the page has no window.PokiSDK,
then calls PokiSDK.init(). A script that fails, takes longer than timeoutMs, or an init() that rejects
(Poki does when an ad blocker is on) puts the game on the mock with ads off. ready() is
gameLoadingFinished. Interstitials are commercialBreak, always completed: Poki says nothing about a
skip. Rewarded ads are rewardedBreak: completed on true, skipped on false.
Poki keeps no save, leaderboard or player: saves stay in localStorage, boards answer unavailable, the
player is a guest. shareableURL(params) and getURLParam(key) are the host extras.
const link = (await sdk.host.poki?.shareableURL({ seed: today })) ?? location.href;const seed = sdk.host.poki?.getURLParam('seed') ?? new URLSearchParams(location.search).get('seed');const link: string = (await sdk.host.poki?.shareableURL({ seed: today })) ?? location.href;const seed: string | null = sdk.host.poki?.getURLParam('seed') ?? new URLSearchParams(location.search).get('seed');Playgama
Section titled “Playgama”The adapter runs when the page has Playgama Bridge (window.bridge) and calls bridge.initialize() if the
page has not, within timeoutMs. Ads are advertisement.showRewarded() and showInterstitial(), read
through the state events: opened fires start, rewarded then closed is completed, closed alone is
skipped, failed is no-fill.
Saves go to bridge.storage under filudi:save as { v, d }. Scores go to bridge.leaderboards.setScore
unless the board type is not_available, and top() reads getEntries when the type is in_game.
visibility_state_changed fires pause and resume; the locale is bridge.platform.language; device()
reads bridge.device.type (a TV falls back to the SDK’s own guess). The audio setting is
sdk.host.playgama?.onAudioChange(fn), with enabled the opposite of muted.
sdk.host.playgama?.onAudioChange((enabled) => audio.mute(!enabled));sdk.host.playgama?.onAudioChange((enabled: boolean) => audio.mute(!enabled));Testing without the portals
Section titled “Testing without the portals”The playground installs each portal’s global in the frame before your bundle runs, with the shapes the adapters read, and lets you set the ad outcome, the environment, the data module, the leaderboard type, the player and the locale. What it cannot do: show a real ad, decrypt a real CrazyGames key (it checks the encryption against the key you paste), or stand for a portal’s review. Run the game on each host there before every release, as the release checklist says.
Distribution
Section titled “Distribution”A game runs on filudi.com by default. The console’s distribution switch opens it to approved third-party portals on the Filudi catalog once it has a live version; the console shows which sites list it. That is a different thing from CrazyGames, Poki and Playgama: those portals host your bundle themselves and you submit it to them.