Guides

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
  1. 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.
  2. The core validates that result and writes APPLYING to its portal_command journal.
  3. The core runs the prepared operation against the live player object.
  4. After the game call succeeds, the core changes the journal row to APPLIED and reports success.
  5. If a replay finds that command already APPLIED, the core returns the stored result and runs no game call.
  6. 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.
  7. 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.
  8. A crash after APPLYING but before a confirmed APPLIED result 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.

On this page