Capabilities

DELIVER_ITEM_GRANT

Hand a bought item to a character, exactly once

The capability the whole platform exists for: turning a purchase into an item the player is holding. It is required — a server without it stays out of the player portal entirely, because there would be nothing to sell.

The command payload, exactly as applyGrant receives it
{
  "commandId": "77e6a4f0-4f6b-4c85-9e37-1d2a55f0a001",
  "orderId": "b1c9c6a2-8d33-4f1e-9b70-6c0d64f0a002",
  "grantActionId": "5a3f2d10-91ab-4d4f-8c22-7e9b64f0a003",
  "target": { "normalizedAccountName": "robert", "characterId": 268485666 },
  "grant": {
    "type": "l2.grant_item",
    "schemaVersion": 1,
    "itemId": 57,
    "count": 1000
  }
}

The grant fields sit under grant and the target under target — read them from there, not from the payload root. commandId is the replay key the journal is built on, and arrives separately as the method's first argument. orderId names the purchase behind the command; it is the value the journal's order_id column stores. grantActionId identifies this action inside a multi-action order — useful in logs, not needed for idempotency.

enchantLevel is optional on the grant object; absent means +0. A grant path that ignores it delivers an enchanted purchase at +0, silently — see the reference grantItems for the per-unit handling a non-stackable item needs.

target carries normalizedAccountName and characterId. Check that the character really belongs to that account: a command names a character by id, and nothing further down would notice an id belonging to someone else.

What the answer means

OutcomeDurableWhat the gateway does
APPLIEDYesCompletes the command
ALREADY_APPLIEDYesCompletes a replay without applying it twice
QUEUEDYesParks the command as awaiting settlement — the L2Off booking; the same command settles later, once the game has decided
RETRYABLE_FAILURENoRetries; CHARACTER_OFFLINE waits for PLAYER_ONLINE_EVENT
REFUSEDYesCancels the order and releases the player's coins — return it only from a check that ran before any write, the journal included
PERMANENT_FAILUREYesStops retrying
RECONCILIATION_REQUIREDNoAsks a human

Never let an exception escape — a thrown error says nothing about whether the item was handed over, and the only honest answer in that case is RECONCILIATION_REQUIRED.

Delivering exactly once

The gateway replays a command after a reconnect or a crash. Replay protection is what stops that becoming a second free item, and it is the same shape on both families:

  1. Look up the commandId.
  2. If it is already settled, return that result.
  3. If a previous attempt is still APPLYING, return RECONCILIATION_REQUIRED — it died mid-grant and no automatic rule can tell whether the player received anything.
  4. Run every check that can decline — unknown item, invalid count, target mismatch. A decline is REFUSED, and the portal can act on it precisely because the journal was not touched.
  5. Write the APPLYING marker.
  6. Apply the change.
  7. Settle it as APPLIED.

Implementing it

GrantResult applyGrant(String commandId, JsonObject payload);

One method serves every grant kind. Return REFUSED with UNSUPPORTED_GRANT_TYPE for any payload type you do not implement, and advertise DELIVER_SKILL_GRANT only once l2.grant_skill is handled too.

The bridge runs in-process, so it hands the item to the live player object and the player sees it immediately. That also means it can only act on someone who is there: both reference implementations return RETRYABLE_FAILURE / CHARACTER_OFFLINE otherwise, and PLAYER_ONLINE_EVENT is what resumes it.

The journal is a table the bridge creates on startup:

CREATE TABLE IF NOT EXISTS portal_command (
  command_id VARCHAR(36) NOT NULL PRIMARY KEY,
  status VARCHAR(24) NOT NULL,
  error_code VARCHAR(100) NULL,
  order_id VARCHAR(64) NULL,
  created_at BIGINT NOT NULL,
  applied_at BIGINT NULL
);

The game database user needs rights to create and alter it and to read, insert and update its rows — the reference bridges add columns the journal gained after its first release, through JDBC metadata, because CREATE TABLE IF NOT EXISTS never migrates an existing table.

Revocation

There is none. A delivered item cannot be taken back through this capability, and neither can a skill — plan refunds around that.

Verification

Connector check grants one Adena to the character you designate. It is a real grant with no economic impact, but it is real: use a test character.

On this page