Ruby

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

What it is

A Ruby is strafe.fun's currency. Players buy Rubies from strafe.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 strafe.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 strafe.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 a strafe.fun account, so the right move is to offer the login, not an empty wallet.

const { balance, promos } = await strafe.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 strafe.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.

strafe.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 strafe.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 strafe.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 strafe.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.

Selling Rubies for your own token

If your game has an SPL token, players inside your game can buy Rubies with it 20% cheaper than with a card, and the tokens go straight to your wallet. There is nothing to integrate: fill in two fields on your game's Settings tab and the option appears in the Ruby wallet for everyone playing your game.

Token address
The mint, or what most people call the CA. We read it off Solana mainnet, so a mistyped address is refused rather than saved.
Treasury wallet
Yours. Every payment lands there directly; strafe.fun never holds, routes or takes a cut of your token.

The amount is worked out from your token's USD price at the moment the player starts the purchase and fixed for ten minutes, so a $4.99 pack is $3.99 of your token, quoted before their wallet opens. They approve one transfer; we watch the chain for it and release the Rubies when it confirms.

The Rubies come out of your treasury

This sells Rubies you already bought. It is not a second way to create them. So keep your treasury topped up on the Ruby tab, and remember the 20% is yours to fund. That is the point of it: you are spending Ruby to create demand for your token, in the one place your players are already standing.

Nothing else about Ruby changes. What a player buys this way is ordinary Ruby: it spends in any game on strafe.fun, and your game reads and charges it exactly as above.

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 { Strafe } from '@strafe-fun/sdk/server';

const strafe = new Strafe({ appId: process.env.STRAFE_APP_ID, appSecret: process.env.STRAFE_APP_SECRET });

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

await strafe.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 strafe.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 strafe.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.

For token payments, the Settings tab says whether the option is actually live: the symbol we resolved, the current rate, and what each pack costs in your token. If it says we cannot price it, the token is not listed on a market yet and nothing will be offered until it is.

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.