Grant entitlement
Grant ownership of a SKU to a user.
Source src/operations/entitlements.ts#L42grantEntitlementsrc/operations/registry.ts#L65REGISTRYsrc/contract.ts#L136kind 'grantEntitlement'src/contract.ts#L46EntitlementAttributessrc/economy.ts#L1313RESTRICTED_TO_PRIVILEGED
API grantEntitlement
grantEntitlement records that a user owns an item or feature, named by a sku (a product code such
as wrld_pass). It tracks ownership only. No money moves, and the ledger is untouched.
A spend grants the buyer’s entitlement as part of the sale. You reach for grantEntitlement
directly when ownership comes from somewhere else: a manual fulfillment, a migration, or a comp.
You name the user and the SKU. The actor is a trusted system service or a human operator:
const outcome = await economy.submit({
kind: 'grantEntitlement',
idempotencyKey: 'idem_0',
actor: { kind: 'system', service: 'fulfillment' },
userId: 'usr_owner',
sku: 'wrld_pass',
});
// → { status: "committed", transaction: { id: "txn_…", … } }curl -s https://economy.example/submit \
-H 'content-type: application/json' \
-d '{
"kind": "grantEntitlement",
"idempotencyKey": "idem_0",
"actor": { "kind": "system", "service": "fulfillment" },
"userId": "usr_owner",
"sku": "wrld_pass"
}'read.entitled("usr_owner", "wrld_pass") now returns true.
The grant always succeeds: it overwrites any prior record for the user and SKU, and checks no balance.
Parameters
The payload fields, beyond the kind tag, are:
| Field | Type | Default | Description |
|---|---|---|---|
idempotencyKey | string | (required) | A retried submit with the same key runs at most once. See idempotency. |
actor | Principal | (required) | Who is asking. Must be system or operator. |
userId | string | (required) | The user who gains ownership. Must be non-empty after trimming whitespace. |
sku | string | (required) | The item or feature owned. Must be non-empty after trimming whitespace. |
attrs | EntitlementAttributes | {} | Optional details stored with the grant (see below). |
EntitlementAttributes carries four optional fields: quantity (a count), version (a number),
expiresAt (an instant in epoch milliseconds, or null for “never expires”), and source (a free
string). Omit attrs entirely to record the bare ownership fact without inventing defaults.
Returns
grantEntitlement resolves to an Outcome.
committed: the ownership record was written. Thetransactionis a marker with a fresh id and commit time. Itslegsandlinksare empty, since nothing posted to the ledger.duplicate: theidempotencyKeywas already used; the earlier outcome is returned unchanged and the record is not rewritten.
grantEntitlement returns no rejected outcome. A well-formed grant on a healthy system always
commits. A malformed request throws instead; see reason codes.
Postings
None. grantEntitlement changes ownership, not money, so it posts no double-entry legs.
The committed transaction is a lifecycle marker: a receipt that an operation ran, with empty legs
and links lists. Ownership lives in its own record, keyed by user and SKU, that the ledger never
touches. You read it back with read.entitled.
Authorization
grantEntitlement is restricted to a privileged Actor: a trusted system service or a human
operator.
An end user can never grant ownership. A user principal is refused with a thrown AUTH.UNAUTHORIZED
before the handler runs. Granting names a userId the caller need not own and posts no debit, so the
gate stands in for the ownership check the posting path would otherwise apply.
Reason codes
grantEntitlement returns no reason codes: it has no rejected path. It fails only by throwing.
Each fault marks a broken request rather than an expected decline:
| Code | When |
|---|---|
AUTH.UNAUTHORIZED | The actor is a user. Granting is system- or operator-only. |
OP.MALFORMED | userId or sku is blank or whitespace; or attrs.expiresAt is present and not finite; or attrs.quantity is present and not a positive integer. |
The blank-field check lives in the handler because an entitlement posts to no wallet account, so the central blank-owner guard (which only inspects accounts an operation debits) never sees these fields.
Preconditions and invariants
grantEntitlement holds a few things true:
userIdandskuare each non-empty after trimming, so ownership is never recorded against a phantom user or of nothing.- The grant is idempotent on
idempotencyKey: a retried submit writes the record at most once. - The grant is a full overwrite. Re-granting the same user and SKU with new
attrsreplaces the prior record; there is no merge and no append. - No balance moves, so the ledger stays balanced and conserved.