Ruby

One currency across every game on indie.fun. Players buy it once and spend it anywhere, including in your game.

What it is

A Ruby is indie.fun's currency. Players buy Rubies from indie.fun by clicking the Ruby in the navbar, and the balance follows them into every game on the site. Your game reads that balance and charges against it, instead of minting a premium currency of its own that nobody arrives with.

It is stored and settled entirely by indie.fun. Your game never holds a balance, never adds to one, and never has to be trusted with one. It asks what a player has and asks to charge them, and both answers come from us.

A player’s balance
Theirs, not yours. What they earned in someone else’s game buys something in yours, which is the whole point of one currency.
Your treasury
Your game’s own Rubies. Players spending in your game fill it; promotions and prizes are paid out of it. You can top it up on your game’s Ruby tab.
A promotion
Rubies you pay players for playing. Advertised to everyone on indie.fun, and paid out of your treasury automatically.

A game cannot create Rubies

Everything your game hands out, it bought first, at the same price a player pays. That is what stops a Ruby quietly becoming worthless, and it is why a promotion is a real marketing spend rather than a button.
1

Read what the player has

getRubies() returns the signed-in player's balance and any promotions your game is currently running that they can still earn. A signed-out player has a null balance rather than zero. Rubies live in an indie.fun account, so the right move is to offer the login, not an empty wallet.

const { balance, promos } = await indie.getRubies();

if (balance === null) {
  showLoginPrompt();
} else {
  shop.setBalance(balance);
}

To keep it live, subscribe. onRubyChange fires immediately with the current state, then whenever the balance moves, including when indie.fun has just paid the player for one of your promotions, which arrives as rewards and is the moment to show “+1 Ruby” on screen.

indie.onRubyChange(({ balance, rewards }) => {
  shop.setBalance(balance);
  for (const reward of rewards) {
    toast(`+${reward.reward} Ruby, ${reward.title}`);
  }
});
2

Charge for what you sell

spendRubies() moves Rubies from the player to your treasury. It resolves rather than throws: running out of Rubies is a thing to handle, not a crash, and the players who hit it are exactly the ones about to buy more.

const result = await indie.spendRubies(30, {
  item: 'Golden card back',
  key: `cardback-gold:${player.id}`,
});

if (result.ok) {
  unlock('cardback-gold');
} else {
  showTopUp(result.error);   // "Not enough Rubies."
}

The key is what makes a retry safe

The same key never charges twice. Pass one that describes the thing being bought (an order id, an unlock name) and a dropped response, a double-click and a reconnecting client all cost the player exactly thirty Rubies. Leave it out and one is minted for you, which protects you from a lost response but not from a second click.

A spend is authorised by the player's own token, and that token has to have been issued for your app. It is refused otherwise, which is what stops a game that holds a player's token from spending their Rubies into somebody else's treasury.

3

Pay players to try your game

A promotion is an offer, “play for five minutes, get a Ruby”, shown to every player on indie.fun, wherever on the site they are. They see the counter in the navbar go up, open it, and find your game in the list.

You start one on your game's Ruby tab: what it says, how much each player gets, how much you are willing to spend in total, and what they have to do. Your treasury has to already hold the budget.

There is nothing for your game to claim

Rewards are settled by indie.fun against its own record of what the player did: the play clock behind session length, or a progression step your game reports. Your game does not ask for a payout and could not receive one if it did: a claim a page can make is a claim a page can forge, and this pays out of your treasury.
Play time
Minutes actually played, as the session clock measures them, the tab visible and the player not idle. A game left open on a second monitor earns nothing.
Progression step
A milestone your game reports through progress(). You choose it; we only count having received it.

Only signed-in players can earn one, because a Ruby has to land in an account. A promotion is therefore also one of the better reasons a player will make one.

From your server

The server SDK holds your App Secret, so it can do the thing a browser must never do: pay a player directly. Use it for what only your server can referee, a tournament placing, a refund, an apology. For anything you want advertised, run a promotion instead.

import { Indie } from 'indie-sdk/server';

const indie = new Indie({ appId: process.env.INDIE_APP_ID, appSecret: process.env.INDIE_APP_SECRET });

const { balance } = await indie.getRubyTreasury();
const held = await indie.getPlayerRubies(player.id);

await indie.grantRubies(player.id, 25, {
  key: `weekly-${weekNumber}-${player.id}`,   // never pays twice
  reason: 'Weekly top 10',
});

Server methods

getRubyTreasury()Your game’s balance and its recent movements.
getPlayerRubies(playerId)What one of your players holds, across all of indie.fun.
grantRubies(playerId, amount, { key, reason })Pay a player out of your treasury. The key makes a retry safe; a treasury that cannot cover it returns { ok: false } rather than going negative.

Browser methods

On your client

getRubies(){ balance, promos }, with a null balance when nobody is signed in.
spendRubies(amount, { item, key })Charge the player. Resolves { ok, balance, error }.
onRubyChange(cb)Called with { balance, rewards } now and on every change, including a promotion indie.fun has just paid.

There is deliberately no way to give Rubies from the browser. Anything a page can ask for is something a page can ask for a hundred times, so handing out currency is either a promotion or a server-side grant.

Check it worked

Open your game's Ruby tab. The treasury balance is what your game holds right now, and every movement under it is one line per purchase, spend, promotion payout and grant, so a spend you just made from the browser shows up there, named by the item you passed.

If nothing appears, the two usual causes are that the player was not signed in (a guest has no balance to spend) or that the token was not issued for your app. Check Authentication is working first.