Why a donation panel breaks when the player is online
The idempotency problem every L2J donation panel eventually hits, and the grant flow that avoids it
By Forgeport · Published 10 August 2026 · Updated 24 August 2026
Most homegrown L2J donation panels get item delivery half right: they find the character in the database, add the item, done. It works in testing. It breaks in production, and almost always the same way — a reconnect, a timeout, or a retry hands the same purchase to the same player twice, or the panel can't tell whether a delivery actually happened and just guesses.
The problem is idempotency, not database access
On L2J, delivery code running inside the game server process can reach the live Player object.
The difficult part is that "grant this item" is not a safe operation
to run twice, and network calls get retried. A panel that adds the item and then marks the order
paid has a window where a retry, a double-click, or a webhook firing twice grants the item again
before the first grant is recorded.
A grant flow that survives replays
The fix is to persist intent before touching game state, so a replay can recognize itself. In Forgeport, the connector core owns this sequence:
new command → APPLYING → APPLIED- A command arrives with a stable id. Your item-delivery code validates the account, character,
item and live player, then returns a prepared
Delivery.apply(...)operation. It does not add the item yet. - The core validates that result and writes
APPLYINGto itsportal_commandjournal. - The core runs the prepared operation against the live player object.
- After the game call succeeds, the core changes the journal row to
APPLIEDand reports success. - If a replay finds that command already
APPLIED, the core returns the stored result and runs no game call. - If the character is offline, your code returns
Delivery.offline(). The core waits for the character's next login event instead of guessing or polling. - Input that can never succeed (unknown item id, a name already taken, a skill already known) is
refused before
APPLYING, so the reservation on the player's coins can be released. - A crash after
APPLYINGbut before a confirmedAPPLIEDresult is the one case that needs a human. The core flags it for reconciliation instead of silently retrying, since it genuinely cannot know whether the item landed.
The two important failure modes are an offline character and a crash during the grant. Repeated polling delays offline delivery, while retrying an unknown mid-grant state can duplicate the item.
Implementation scope
Forgeport's L2J connector core handles retry, reconnect, command delivery, the journal and every
state transition above. Your type-safe delivery handler has a smaller responsibility: validate
pack-specific facts and return the game change as Delivery.apply(...) without running it early.
Two complete aCis and Mobius integrations show that boundary end to end. Neither integration parses protocol JSON or implements the journal. See Architecture and the integration examples. The hosted panel this flow plugs into is described on the L2J web panel page.