FOSKAAY GASLESS GAMES INFRASTRUCTURE (Foskaay GGI)

Add gasless play to any on-chain game

Foskaay Gasless Games Infrastructure (Foskaay GGI) lets a game open a session on-chain, do everything inside for free, and settle once. Players never pay gas and never see a wallet popup. The game pays a small fixed fee per session instead of a fee per action, so an on-chain game can be as cheap to run as a normal web2 backend.

Why use it

How it works

Think of a session as a room. You open the room, everything inside is free, and you settle it once at the end. Only the open and the settle touch the chain.

StepWhat happensOn-chain?
OpenCreate the session: participants, an optional rules blob, a lifetime, and an optional committed random seed.Yes, one transaction
ActRecord a signed action. The payload is opaque; the rail never reads it. As many actions as you like.No, free
SettleClose the session, reveal any committed seed, and seal the final digest.Yes, one transaction
The result is tamper-evident. Every action carries a signature and a sequence number, all folded into one digest. Anyone can replay the action log and check it matches the sealed digest, so the outcome cannot be quietly changed after the fact.

Designing a game for Foskaay GGI (read this before you design your contract)

This is the most important page for a game dev (and for any AI agent helping one). Foskaay GGI is a transport, not a ledger and not a policy. It takes the accounts your game already owns, moves them off the base chain into the free room, lets everything happen there for nothing, and commits them back. It does not create your game, your players, your points or your rules. Your contracts own all of that, permanently, on the base chain.

The one sentence to remember. Design your game so that the accounts it needs during a match are as few as possible, because every account you lift into the room is paid for, and everything that happens inside the room, including crediting points, must run there or the sponsor pays per action again.
1. Optimise first: the fewer accounts, the cheaper it is

Each account you delegate into a session is a separate lift and a separate commit. If you make one account per game, or one account per feature, that count grows forever. A real example of the trap: a player with a dice account, a points account, a global points account, a lives account, a result account, a premium account and a player account is six or seven lifts for one player. Add a second game and it becomes seven or eight. At 50 games it is unmaintainable and the fees multiply.

The payoff, stated plainly. A design with one game account and one player account delegates two accounts for any number of games, instead of six to eight per player. That is a 50 to 75 percent cut in lifting and commit cost, and it is the single biggest optimisation available to you. We are happy if you choose to lift ten accounts; we will not stop you. But the smart design is one game plus one player.
2. My live experience on MagicBlock's Ephemeral Rollup (why this page exists)

I built on MagicBlock's ER as a beginner and learned this the hard way. I first created a separate account for the game, for points, for subscriptions, for lives and for the player. To play, I had to delegate six accounts at once, and I did not understand that as a beginner. When I added a second game (chess) it forced another game account, so it became seven. The cost went up every time, and I could feel it growing. At 50 games it would have been impossible to maintain. The fix was to consolidate: one player account that holds points for every game in buckets, and one game account that every game is a module inside. Adding a game became additive, not a new account. That is why this page asks you to think about account count before you write a line of Solidity.

3. The fix, in the shape of the design we recommend
4. The rules that keep it cheap and fully on-chain
Before you design any game demo on Foskaay GGI (your own, or GlobalFolkGames' own), read this section first. It is the difference between a game that costs a fraction of a cent per match and one that quietly costs fifty times more, and it is the mindset an outside developer should bring: cheap to lift, free to play inside, one clean commit home.

Should your game contract be upgradeable?

Short answer: yes, for a game, almost always. A game is never finished on day one. You will want to lower or raise points, add a subscription, add lives, add in-game assets, fix a balance, or add a new mode. On a normal contract, every one of those means deploying a new contract at a new address and migrating players, which breaks every existing game, every saved match and every link. An upgradeable contract keeps the same address and lets you change the logic behind it, so existing games and addresses never break.

The two shapes
ShapeWhat it meansBest for
Immutable contractDeploy once, the code can never change. A fix means a new address.One-off, finished, tiny contracts. A fair lottery. A proof of concept you will throw away.
Upgradeable (UUPS proxy)The address is permanent. You can ship new logic behind it without moving the address or touching players' data.Game contracts, points, player accounts, anything you will add to over time. This is what Foskaay GGI's own core uses.
Pros of making your game upgradeable
Cons, stated honestly
The storage safety rules (non negotiable)
What we did. Both of our demo contracts (FoskaayGGIDemoGames, which holds Ludo today and any demo game tomorrow, and FoskaayGGIDemoPlayer, which holds everything about a player across every demo game) are UUPS upgradeable, OpenZeppelin only, with an append-only storage gap and an owner-only upgrade. You can add a game, a subscription, lives or assets later without ever redeploying or breaking the address your players and your frontend already use.

Built to fit your game, never the other way round

Foskaay GGI is deliberately unopinionated. It gives you primitives and makes no decision for your game. It does not care how many players you have, how your data is laid out, or how often you settle.

The core (everything you need to ship): one contract
ContractWhat it does
FoskaayGGIConnect a session (the fee is paid here and sent straight to your destination), settle the result, and give free pure randomness to the games that ask. The fee is built in, so there is no separate vault to deploy or wire.
Batching and randomness are built in
A developer who wants a separate account per player, per feature, and a commit on every turn can do exactly that here and pay a little more. That is your cost to optimise. Our job is to make the floor cheap, not to tell you how to build.

Quickstart

Integrating is three things: install a package, write a small adapter for your game, and connect, sign and settle. There is no fork and no contract to copy.

npm install @foskaay/ggi-sdk
  1. Install @foskaay/ggi-sdk (and @foskaay/ggi-contracts-sdk if your own contract calls the rail directly).
  2. Write a small adapter that turns your game's state into a hash. This is the only game-specific code, and it lives in your game.
  3. Connect, sign, settle: one connect transaction (the fee is paid there), free signed moves during play, one settle at the end.
// Pseudo-code: the shape of an integration const ggi = new GgiClient({ network: 'testnet', walletClient }); // 1. connect a session and delegate your game + player accounts // (one transaction; the 3-part fee is read from the chain and paid here) const key = ggi.createSessionKey(); await ggi.handoverWithAccounts({ sessionId, gameLogic: myGameAddress, startHash: hashOf(myStartState), seedCommit: optionalSeedCommit, players: [p0, p1], sessionKeys: [key0, key1], randomCount: 1, accounts: [myGameAddress, myPlayerAddress], // your delegated accounts games: 1, }); // 2. play for free: run your pure rules via eth_call, dice free via randomN const state = await ggi.applyMove(stateBytes, kind, seat, token, value, seeds); const finalHash = await ggi.hashState(state); // 3. commit the match + credit the players (one transaction) const sig = await ggi.signMove(key, sessionId, finalHash); await ggi.settleGame(sessionId, [gameTuple], seatPlayers, gameTag); // 4. close the session on the core (players sign the final hash) await ggi.settle({ sessionId, finalHash, seedReveal, sigs: [sig0, sig1], signers: [p0, p1], });

Full API reference and the adapter example ship with the SDK README. This page is the overview; the package docs go deeper.

Contracts

Foskaay GGI is one core contract (FoskaayGGI), deployed per network. Your own game and player contracts are yours to deploy and own; the core is the only rail address you call. The addresses below are public and safe to use in your app. FoskaayGGIGames and FoskaayGGIPlayers are the Ludo demo contracts: a working example of one game on the rail, used here to prove the SDK and the gasless promise end to end. They are not part of the core. Bring your own game and player contracts; use the demo only as a reference.

TESTNET Arc Testnet (chain id 5042002)

These addresses are permanent. The contracts are upgradeable behind UUPS proxies, so improving the rail never changes an address and never moves your data.

ContractResponsibilityAddress
FoskaayGGIConnect a session (paying the fee), settle the result, free randomness0x793785CE66992211B7c60dFCf0318869678D33a4
FoskaayGGIGames (game: match + rules + settle)Holds the match on-chain, runs the Ludo rules free in the Foskaay GGI Midchain, and settles N games in one tx0x24e38ac2e80958782a8Bc5CD479bbe2e5D81EcDF
FoskaayGGIPlayers (player account)Per player, per game tag points/lives/records; credited by its game, pointsOf0x1614ebc72eA1cB3D31975b3976B5B474FAcE3b3C
FoskaayGGILudo (legacy demo game, pure rules only)The earlier pure-rules demo; superseded by FoskaayGGIGames in Phase 50xa5040Ece5945a8551499ad1148fc3cD15b165987

Fee in native USDC, charged once at connect: a session base plus a per-delegated-account part plus a per-game part. Nothing inside the session is charged. The exact numbers live on the single pricing page. RPC https://rpc.testnet.arc.io  |  Explorer https://explorer.testnet.arc.io  |  USDC 0x3600000000000000000000000000000000000000

MAINNET Arc Mainnet (chain id 5042)
ContractAddress
FoskaayGGI0xb406295b4F7E5B513b656122AfFF29AF720E9E23
FoskaayGGIGames0xb2d5DfF81B076948f50dA2CcF01887f5ed6Ae2b2
FoskaayGGIPlayers0x9425c1d6bA7923D5C804c5e549E08629AbBe3165

Deployed and live on Arc mainnet. The SDK defaults to mainnet and reads the addresses and the fee from the chain at runtime, never hardcoded. Testnet remains for development.

Networks

The whole Foskaay GGI site runs on Arc mainnet. The exact same contracts are also deployed on Arc testnet, so game developers can build, test and confirm their game flow against testnet first (the fees are paid there too, for a full experience), then point their game at mainnet the exact same way when it goes live.

Picking a network with the SDK
import GgiClient from '@foskaay/ggi-sdk'; const live = new GgiClient({ network: 'mainnet' }); // production const dev = new GgiClient({ network: 'testnet' }); // build + test, free to run
MAINNET Arc Mainnet (chain id 5042)
ContractAddress
FoskaayGGI0xb406295b4F7E5B513b656122AfFF29AF720E9E23
FoskaayGGIGames0xb2d5DfF81B076948f50dA2CcF01887f5ed6Ae2b2
FoskaayGGIPlayers0x9425c1d6bA7923D5C804c5e549E08629AbBe3165
TESTNET Arc Testnet (chain id 5042002)
ContractAddress
FoskaayGGI0x793785CE66992211B7c60dFCf0318869678D33a4
FoskaayGGIGames0x24e38ac2e80958782a8Bc5CD479bbe2e5D81EcDF
FoskaayGGIPlayers0x1614ebc72eA1cB3D31975b3976B5B474FAcE3b3C

Same contracts, same fee model on both networks. Developers keep using testnet while building, then switch network: 'mainnet' when they ship.

Fees

The exact numbers (Foskaay GGI fee, Arc gas, total you pay, games per 1 USDC) live on the single pricing page, so there is one place to update and never a stale number here.

Starting is half. Ending is the other half

A session does not close itself. Your game decides when a game is over (it knows), and you close it with two calls:

Unbatched (one game per session): connect with games: 1, play, then settleGame([one game]) and settle. The game is on-chain the moment it ends. On testnet that is 0.0014 USDC in the rail fee (0.0004 base + 0.0004 per account x2 + 0.0002 x1) plus Arc gas.

Batched (many games per session): connect once with games: N, play N matches for free, then ONE settleGame with all N games and ONE settle. Measured on Arc testnet: 3 games in one session, rail fee 0.0018 (only the per-game part grew), one settleGame wrote all 3 games and credited the player 3x, and the core settle stayed flat at about 72k gas. All transactions succeeded.

Batching only changes when the result lands on-chain, never how fast play is: every move is already free and signed instantly. A dev picks close-on-game-end (unbatched) or close-when-the-batch-is-full (batched). Numbers live on the pricing page.

Persistent gameplay (survive refresh, logout, rejoin)

Foskaay GGI is the free room; your game and player contracts are the store. The moment a game settles, the game (with its board bytes) and the player's points are written on-chain immediately, even while the session is still open. We measured this live on testnet: 3 games in one open session; after each game, reading the contracts from outside the session showed the points and committed games already there, identical after the session closed.

The midchain: no per-move Arc transactions, ever

During play, NOTHING is written to Arc. Each move is computed free by the contract (eth_call), hash-chained (prevHash -> newHash) and signed, and the signed log IS the Foskaay GGI midchain. The relay is only an untrusted cache: any device replays the log and verifies it client-side (chain continuity, every signature, and the on-chain anchors: the Handover's startHash and participants, and the settle's finalHash). Arc sees exactly two transactions per session: connect and settle. A per-move storage write on Arc (an on-chain recordLive) would be a THIRD transaction, the fee regression this design forbids.

The pattern a game dev adds (their contracts, never the rail)

The SDK exposes this to you: verifyMoveLog(sessionId, log) for the client-side midchain verification, gamesOf(sessionId), playerGamesOf(sessionId, player), pointsOf(player, tag), gameCount(sessionId). State lives in the midchain (your contracts) and any device reads it, verifies it, and continues, refresh or rejoin. Nothing is invented in the browser.

Refresh and rejoin work how?

The live session URL (?game=sessionId) is the only handle, no storage. On rejoin the device fetches the signed move log from the relay (untrusted) and VERIFIES it client-side against the on-chain anchors before drawing one token, via the SDK's verifyMoveLog. If the log is missing (a serverless relay restarted), the page shows the on-chain truth (paid session, and the committed result once settled) instead of fabricating a board. Mid-game sessions are midchain state, so they never left for Arc: resumable while the log is alive, permanently reviewable once settled.

Persistent gameplay is game-side code on top of Foskaay GGI. The rule everyone follows: connect and settle are the only Arc transactions; everything in between stays in the signed midchain and is verified against the chain. Move counts stay unbounded, fees stay at two.

Get started

Four steps from zero to a gasless game. If you already have a game, only steps 3 and 4 are new work.

1. Install
npm install @foskaay/ggi-sdk viem

@foskaay/ggi-sdk is the client. Add @foskaay/ggi-contracts-sdk only if your own contract calls the rail directly.

2. Create a client
import { GgiClient } from '@foskaay/ggi-sdk'; const ggi = new GgiClient({ network: 'testnet', // 'testnet' | 'mainnet' walletClient, // a viem WalletClient that signs });
3. Write a small adapter (the only game-specific code)

Your adapter turns a move into an opaque payload. The rail never reads it, so any game fits.

function toPayload(move) { // anything serialisable, and stable for the same move return { from: move.from, to: move.to, die: move.die }; }
4. Connect, sign, settle
// CONNECT once per session and delegate your game + player accounts await ggi.handoverWithAccounts({ sessionId, gameLogic: myGameAddress, startHash: hashOf(myStartState), seedCommit: mySeedCommit, // only if your game needs randomness players: [p0, p1], sessionKeys: [k0, k1], randomCount: 1, accounts: [myGameAddress, myPlayerAddress], games: 1, }); // PLAY for free: run your pure rules, hash the state, sign it silently const state = await ggi.applyMove(stateBytes, kind, seat, token, value, seeds); const finalHash = await ggi.hashState(state); const sig = await ggi.signMove(key, sessionId, finalHash); // SETTLE: commit the match and credit the player, then close the session await ggi.settleGame(sessionId, [gameTuple], seatPlayers, gameTag); await ggi.settle({ sessionId, finalHash, seedReveal, sigs, signers });
That is the whole integration. No fork, no contract to copy, no infrastructure to run. Your game keeps its own economy; the rail owns the room.

The SDK README has the full API reference, every read helper, and the session-key examples.

The demo: a real game on the rail

The best way to see Foskaay GGI is to play a game that runs on it. The Ludo demo is a complete on-chain match: the contract owns the rules, the dice, the capture rules, the crown and the points; the page only displays. Every roll and move runs free through the contract (eth_call) and the relay hash-chains + signs them; only the connect and the settle are real transactions.

Open the live Ludo demo. It is an implementation example: a game developer can copy the shape (one game contract + one player account) and bring their own rules and economy.

FAQ and fixes

Do my players need a wallet?

No. They sign in however you onboard them, and every action is signed silently by a session key. They never see a wallet popup and never hold gas.

Does every move cost something?

No. Nothing is written to the chain during play. The fee is charged once, at connect, as a session base plus a per-delegated-account part plus a per-game part, and a thousand actions inside the session cost nothing more than one. The numbers live on the pricing page.

What if a session is abandoned?

Only the open fee is paid. The settle fee is charged only when a session actually settles.

Can a player tamper with the result?

No. Every action is signed and folded into one digest. Changing an action breaks the signature, and a fake result cannot settle.

Do I have to use your account layout?

No. Layout, cadence and accounts are entirely your choice. One account per player, one per feature, or one per match all work unchanged.

How do I read what happened?

Every session and its sealed digest is readable from the contracts using the SDK read helpers. A session reader UI is part of the roadmap so receipts are visible to everyone.

Something looks wrong. Where do I start?

First check the network you passed to the client matches the contract addresses on this page. Then confirm the contracts are deployed on that network (mainnet and testnet are both live, with the addresses on the Networks page). Most issues are a network mismatch. If it persists, open an issue on the repository or contact us from the Support page and we will look with you.

Foskaay Gasless Games Infrastructure (Foskaay GGI). Built by Foskaay for GlobalFolkGames and for any game that wants gasless play on Arc.