Skip to content

tilttek/exchange

✅ published

Rewards your members buy with points, points they give each other, and items they trade, safely and once.

tilttek/exchange handles the three exchanges of a gamified site. A member exchanges points for a reward of the catalogue — a discount, a digital item, an access, a seat at an event — in two steps: a reservation spends the points on the ledger of tilttek/points and holds the unit, then the application confirms once it has delivered, or cancels and the points come back; a reservation left unconfirmed expires. A reward has its price in a currency of points, its stock, its period of availability, a limit per member and conditions, like a badge or a level, that the application checks. Members may give each other points, and trade items of their collections — virtual cards, the dice of a set — by offers, counter-offers and acceptance: the items offered are held from the offer on, and on acceptance both sides change hands in one transaction, or nothing moves. Guards keep it fair: no exchange with oneself, a delay for a new account, an allowance per period, and a journal of transfers the application examines. Every step is safe to run twice, and points are never turned into money.

It is for an application that lets its members spend and share what they earn — a store's loyalty programme, a community site, a game site — and wants exchanges that never lose a point or an item. tilttek/exchange-bundle integrates it with Symfony and keeps everything in Doctrine DBAL; an application without Symfony takes the library alone and writes its own stores, or uses the ones in memory.

It is a plain PHP library whose dependencies are tilttek/points, the ledger that rewards and transfers move points on, phil/resource, the opaque references of members and items, and psr/clock. Items live in the application's collections, reached through an Inventory it implements; the conditions of the rewards and the joining dates of the members come from the application too: the library reads no account, no badge, no level.

The design is small: Redemptions reserves, confirms, cancels and expires the Reservations of the Rewards of a Catalog; Transfers gives points within Guards and writes the Transfers to a TransferLog; Trades opens, accepts, declines, withdraws, counters and expires Offers kept by an OfferStore; an Announcer is told each event; ExchangeErasure erases a member.

Status

PartState
Reward, Catalog, InMemoryCatalog, Requirements: prices, stock, availability, limits and conditionsImplemented and tested
Redemptions and ReservationStore: reservations in two steps, stock and limits checked as they are written, expiryImplemented and tested
Transfers and TransferLog: points given between members, the journalImplemented and tested
Trades, OfferStore and Inventory: offers holding their items, acceptance in one transaction, counter-offers, expiryImplemented and tested
Guards and Allowance: no self-exchange, delay for new accounts, allowance per periodImplemented and tested
Events through an AnnouncerImplemented and tested
ExchangeErasure: the erasure of a memberImplemented and tested
Exceptions under one ExchangeException interfaceImplemented and tested

Documentation

DocumentWhat it covers
InstallationRequirements and installing.
RewardsThe catalogue, reservations, confirmations, cancellations, expiry, stock, limits and conditions.
TransfersPoints given between members and the journal.
TradesOffers, counter-offers, acceptance, the held items and the inventory.
GuardsSelf-exchange, new accounts, allowances.
EventsWhat the announcer is told, and when.
ErasureWhat erasing a member removes and what it keeps.
Writing a storeThe contracts of the stores, the transaction and the inventory.
Extension pointsWhat an application gives the library: stores, the transaction, the inventory, the catalogue, conditions, the announcer, the clock.
ExceptionsExchangeException and its exceptions, with their messages.
TestingThe library's tests and mutation bar, and how to test code that exchanges.
ReferenceEvery type of the library, with its role.

Quick start

composer require tilttek/exchange
use Phil\Resource\ResourceReference;
use Symfony\Component\Clock\NativeClock;
use Tilttek\Exchange\Reservation\InMemoryReservationStore;
use Tilttek\Exchange\Reservation\Redemptions;
use Tilttek\Exchange\Reward\InMemoryCatalog;
use Tilttek\Exchange\Reward\Reward;
use Tilttek\Points\Currencies;
use Tilttek\Points\Currency;
use Tilttek\Points\Ledger\InMemoryLedger;
use Tilttek\Points\Points;

$clock = new NativeClock();
$currencies = new Currencies(new Currency('points'));
$points = new Points(new InMemoryLedger(), $clock, $currencies);
$redemptions = new Redemptions(new InMemoryCatalog(new Reward('discount-10', 100, stock: 50)), new InMemoryReservationStore(), $points, $currencies, $clock);

$member = ResourceReference::of('member', '44');
$points->grant($member, 150, 'welcome:44');

$redemptions->reserve($member, 'discount-10', 'order:2026-0193:reward'); // 100 points spent, one unit held
$redemptions->confirm('order:2026-0193:reward');                         // delivered: the points stay spent

Requirements

PHP 8.5 or later.

Contributing

Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.

License

MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Tilttek project. See LICENSE.