Developer DocumentationPlaykit Integration
Wallet token sinks, hosted burn settlement, and legacy signed callbacks.
A burn is an on-chain action: the player sends Project Tokens from their own
wallet to a backend-chosen sink address — the dead address (0x…dead) by default, but the
backend may choose another destination. Forest records the verified on-chain transfer
with its reason/metadata for tracking. A transfer to 0x…dead is a real,
provable supply burn; any other sink is just the app-chosen destination, not a
supply reduction.
Use it for in-game sinks such as activation fees. Burns are wallet-side and never touch Game Balance.
Authorize before you transfer
The burn settlement requires an unused action authorization, so authorize first — otherwise the player can burn tokens on-chain and then fail Forest authorization/settlement, stranding the burn. The transfer is irreversible; the authorization is not.
The developer backend owns the burn intent — it decides actionId, amount,
burnAddress, reason, and metadata. The client only executes that exact intent
on-chain and relays the resulting txHash back; it does not choose the destination
or purpose.
Keep the burn intent in your hosted backend, scoped to ctx.player.id. It should define a single action ID, amount, destination, and purpose. Return the amount in display units for the wallet step, and retain its exact base-unit value for settlement.
The frontend executes that intent:
import { forest } from "@forest/runtime";
// intent is the saved burn intent returned by your own backend method.
await forest.game.action.authorize({
actionId: intent.actionId,
debitLimitAmount: intent.displayAmount,
});
const burn = await forest.game.burn({
amount: intent.displayAmount,
burnAddress: intent.burnAddress,
});
// Pass burn.txHash back to your backend to confirm the saved intent.After validating the player's saved intent and the supplied transaction hash, the hosted backend settles it:
return ctx.forest.settleBurn({
actionId: intent.actionId,
amount: intent.baseAmount,
txHash,
burnAddress: intent.burnAddress,
reason: "activation_fee",
});Forest verifies the transfer and returns the recorded burn with a display amount. Forest supplies authentication and the timestamp. Keep the confirmed result with the intent; stop automatic retries if completion is uncertain. Never repeat the wallet transfer simply because a response was lost.
Read recorded burns with forest.game.burns() in the frontend, or ctx.forest.game.getBurns() and ctx.forest.game.getBurnSummary() in the hosted backend. Burns leave Game Balance unchanged. Preview uses the shared simulated Test wallet for the same flow.
The remaining callForest and HMAC examples are for legacy HTML uploads with an external backend.
Your backend defines the intent (actionId, amount, burnAddress, reason,
metadata). The iframe calls
forest.game.action.authorize
with that actionId and a display-amount debit limit — which is also the
player-authorized max burn amount. Do not use 0.
callForest("forest.game.burn", {
amount: "10", // display units
burnAddress, // optional; defaults to 0x…dead
});
// response arrives via the message listener: { txHash, burnAddress }Forest performs the on-chain transfer and returns { txHash, burnAddress }
after receipt confirmation — it does not call any API, and never surfaces a
pending tx hash. Relay txHash and burnAddress to your backend.
Your backend HMAC-signs and submits POST /playables/{projectId}/html/burns with
the same actionId, the base-unit amount, txHash, burnAddress,
reason, metadata, and timestamp. Forest verifies on-chain that exactly
amount Project Tokens moved from a linked wallet to burnAddress, then records
the burn. Game Balance is never touched.
The signed amount is the same economic amount as the client burn — display
units in forest.game.burn (e.g. "10"), base-unit integer string in the
signed body (e.g. "10000000000000000000").
POST /playables/{projectId}/html/burns
Content-Type: application/json
X-Forest-Settlement-Signature: v1=<hex-hmac>await fetch(`${FOREST_API}/playables/${projectId}/html/burns`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Forest-Settlement-Signature": `v1=${hmac}`,
},
body: JSON.stringify({
actionId: "same-action-id",
amount: "10000000000000000000", // base-unit integer string
txHash: "0x…",
burnAddress: "0x000000000000000000000000000000000000dead",
timestamp: Math.floor(Date.now() / 1000),
reason: "activation_fee",
metadata: { item: "gold_miner_rig", level: 3 },
}),
});Request fields:
Prop
Type
Sign the exact request body with your Settlement Signing Secret, identical to the settlement flow. Successful responses return:
{
burnId: "burn-id",
actionId: "same-action-id",
amount: "10",
reason: "activation_fee",
metadata: { item: "gold_miner_rig", level: 3 },
txHash: "0x…",
burnAddress: "0x000000000000000000000000000000000000dead",
replayed: false
}Retrying a burn settlement
Burns are idempotent by actionId. A safe retry MUST replay the same
actionId, the exact same JSON body, and the same signature, and arrive
within the request-freshness window (~5 min). An exact replay returns
replayed: true. After the window, do not retry — confirm the burn settled by
looking up its burnId in forest.game.burns.
Idempotency is keyed on actionId, not txHash: a txHash backs exactly one
burn, and a txHash already recorded as a deposit is a conflict.
Burns do not change Game Balance (the tokens are burned straight from the
wallet) and are not part of forest.game.transactions.
Replay and validation failures return the shared settlement error codes —
SETTLEMENT_TIMESTAMP_STALE, ACTION_ID_REUSED, BURN_TX_HASH_REUSED,
DEBIT_LIMIT_EXCEEDED, INVALID_SETTLEMENT_AMOUNT, and signature errors. See
Errors and Security
for each code's cause and resolution.
For legacy HTML, the forest.game.burns RPC returns the current player's recorded burns (paginated,
newest first); the forest.game.burns.summary RPC returns the player's totals grouped by
reason. The Harness frontend SDK exposes forest.game.burns(); use the hosted backend for its summary. Both are player-scoped iframe reads (like forest.game.balance).
callForest("forest.game.burns", { page: 1, perPage: 20 });
// response: { data, meta } — data[]: { id, amount, reason, metadata, txHash, burnAddress, createdAt }
callForest("forest.game.burns.summary");
// response: { totalBurned, byReason: [{ reason, amount, count }] }totalBurned here is "verified burn/sink settlements" — not necessarily supply
reduced (only dead/zero destinations reduce supply). Burns sent without a reason
are grouped under reason: null. Amounts are display token amount strings.
Token-level burn history is separate from Game Balance history. The token Burns tab is populated from synced on-chain token transfers to the burn address or zero address, alongside backend buyback burns.