Callbacks & Automation
When a game's state changes, the MinigameToken can automatically notify the minting platform. This enables reactive systems like automatic prize distribution, quest completion, and leaderboard updates.
IMetagameCallback Interface
pub const IMETAGAME_CALLBACK_ID: felt252 =
0x3b4312c1422de8c35936cc79948381ab8ef9fd083d8c8e20317164690aa1600;
#[starknet::interface]
pub trait IMetagameCallback<TState> {
/// Called on every update_game() call to notify the metagame of a game action
fn on_game_action(ref self: TState, token_id: u256, score: u64);
/// Called when a game ends (game_over becomes true)
fn on_game_over(ref self: TState, token_id: u256, final_score: u64);
/// Called when a token completes its objective
fn on_objective_complete(ref self: TState, token_id: u256);
}How It Works
When update_game(token_id) is called on the MinigameToken:
- Token reads the minter address from the packed token ID
- Token checks if the minter implements
IMetagameCallbackvia SRC5 - If supported, token dispatches the appropriate callback(s)
update_game(token_id)
│
├── Read score from game contract
├── Read game_over from game contract
│
├── minter.on_game_action(token_id, score) ← fires every time
│
├── Game over? (and wasn't before)
│ └── Yes → minter.on_game_over(token_id, final_score)
│
└── Objective completed? (and wasn't before)
└── Yes → minter.on_objective_complete(token_id)SRC5 Gating
Callbacks are opt-in. The token only dispatches callbacks if the minter's contract:
- Implements
SRC5(hassupports_interface()) - Returns
trueforIMETAGAME_CALLBACK_ID
If the minter is an account or a contract without SRC5, no callbacks are attempted. This is safe - no reverts, no wasted gas.
Implementing Callbacks
#[starknet::contract]
mod MyTournament {
use game_components_metagame::callback::IMetagameCallback;
use openzeppelin_introspection::src5::SRC5Component;
component!(path: SRC5Component, storage: src5, event: SRC5Event);
#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
src5: SRC5Component::Storage,
leaderboard: Map<felt252, u64>, // token_id -> score
completed: Map<felt252, bool>,
}
#[constructor]
fn constructor(ref self: ContractState) {
// Register callback support so the token knows to call us
self.src5.register_interface(
0x3b4312c1422de8c35936cc79948381ab8ef9fd083d8c8e20317164690aa1600
);
}
#[abi(embed_v0)]
impl CallbackImpl of IMetagameCallback<ContractState> {
fn on_game_action(ref self: ContractState, token_id: u256, score: u64) {
// Update leaderboard
let id: felt252 = token_id.try_into().unwrap();
self.leaderboard.write(id, score);
}
fn on_game_over(ref self: ContractState, token_id: u256, final_score: u64) {
// Mark as completed, trigger prize distribution
let id: felt252 = token_id.try_into().unwrap();
self.completed.write(id, true);
self.leaderboard.write(id, final_score);
}
fn on_objective_complete(ref self: ContractState, token_id: u256) {
// Award achievement badge or unlock next quest
}
}
}Use Cases
Tournament Automation
Player finishes game
→ update_game() called
→ on_game_over(token_id, final_score) callback
→ Tournament contract updates leaderboard
→ If all entrants finished, distribute prizesQuest Completion
Player achieves objective
→ update_game() called
→ on_objective_complete(token_id) callback
→ Quest contract marks quest as complete
→ Unlock next quest in chainReal-time Leaderboards
Player scores points
→ update_game() called
→ on_game_action(token_id, score) callback
→ Leaderboard contract re-ranks entries
→ Frontend subscribes to events for live updatesSecurity Considerations
- Callbacks are called by the token contract, not the player. The
MetagameCallbackComponentenforces this automatically viaassert_only_token— you don't need to check the caller yourself - Callbacks should not revert on unexpected data. A reverting callback blocks
update_game()for all tokens minted by that platform - Gas limits: Keep callback logic lightweight. Heavy computation should be deferred to separate transactions