Docs/Build guides/Issue

Issue

Prepare and store a signed issuance that tokenizes selected offchain points, credit, perks, or campaign value into a customer-owned Loyalty Token balance.

GoalTokenize selected loyalty value for a customer.

Use issuance when a customer converts eligible offchain points, credit, perks, or campaign value into a tokenized Loyalty Token balance. If the Loyalty Token does not exist yet, the first successful issuance creates it automatically from the included metadata.

Inputs#

FieldTypeRequiredMeaning
issueraddressyesBrand wallet that signs the action.
toaddressyesCustomer wallet receiving the tokenized loyalty balance.
loyaltyIdbytes32yesBrand-scoped program ID.
amountuint256 stringyesAmount of offchain loyalty value to tokenize. Must be greater than zero.
expiresAtuint256 stringyesUse 0 for no expiry, or a Unix timestamp for an expiring bucket.
deadlineuint256 stringyesLast Unix timestamp when the issuance can be submitted. Use 0 for no execution deadline.
noncebytes32yesUnique per issuer. Replays are rejected.
chainIduint256 stringyesExpected chain ID. Production uses Base mainnet 8453; non-production uses Base Sepolia 84532.
verifyingContractaddressyesThe Loyfin factory that will execute the action.
operationHashbytes32optionalBrand reference hash for correlating this action with an internal database row. Defaults to zero bytes when omitted.
metadataMetadatayesProgram metadata. Required for issuance because the first issuance can create the program.
datahex bytesoptionalBrand-defined data. Defaults to 0x. Max 2048 bytes.

Request#

Issuance bodyjson
{
  "issuance": {
    "issuer": "0x1111111111111111111111111111111111111111",
    "to": "0x2222222222222222222222222222222222222222",
    "loyaltyId": "0x4242424242424242424242424242424242424242424242424242424242424242",
    "amount": "1000",
    "expiresAt": "0",
    "deadline": "0",
    "nonce": "0x7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a",
    "chainId": "8453",
    "verifyingContract": "0x3333333333333333333333333333333333333333",
    "operationHash": "0x9999999999999999999999999999999999999999999999999999999999999999",
    "metadata": {
      "loyaltyId": "0x4242424242424242424242424242424242424242424242424242424242424242",
      "name": "Bloom Coffee Rewards",
      "symbol": "BLOOM",
      "media": "ipfs://bafy.../bloom.png",
      "description": "Rewards for Bloom Coffee customers.",
      "contractURI": "",
      "tokenURI": ""
    },
    "data": "0x"
  },
  "signature": "0xababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab"
}
POST /issuancesbash
curl https://api.loyfin.com/issuances \
  -X POST \
  -H "content-type: application/json" \
  -d '{
  "issuance": {
    "issuer": "0x1111111111111111111111111111111111111111",
    "to": "0x2222222222222222222222222222222222222222",
    "loyaltyId": "0x4242424242424242424242424242424242424242424242424242424242424242",
    "amount": "1000",
    "expiresAt": "0",
    "deadline": "0",
    "nonce": "0x7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a",
    "chainId": "8453",
    "verifyingContract": "0x3333333333333333333333333333333333333333",
    "operationHash": "0x9999999999999999999999999999999999999999999999999999999999999999",
    "metadata": {
      "loyaltyId": "0x4242424242424242424242424242424242424242424242424242424242424242",
      "name": "Bloom Coffee Rewards",
      "symbol": "BLOOM",
      "media": "ipfs://bafy.../bloom.png",
      "description": "Rewards for Bloom Coffee customers.",
      "contractURI": "",
      "tokenURI": ""
    },
    "data": "0x"
  },
  "signature": "0xababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab"
}'
TypeScripttypescript
const response = await fetch("https://api.loyfin.com/issuances", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ issuance, signature })
});

if (!response.ok) {
  throw new Error(await response.text());
}

const { operation, duplicate } = await response.json();

Next step#

Store the signed tokenization issuance and show it as Created. The issuer does not need to submit the transaction: Loyfin's official relayer can relay it, or an independent relayer can submit first and earn the relayer fee. When mined, the indexer writes a completed Issuance event object.

Common errors#

  • Wrong chainId or verifyingContract makes the signature invalid onchain.
  • Expired buckets cannot be issued, and actions cannot execute after their deadline.
  • A reused issuer nonce is treated as duplicate or rejected before balances change.

Brand database flow#

Loyfin tokenizes selected balances; it does not replace your primary points ledger.

Most loyalty points still live in the brand's database. A common integration is a Convert to token or Tokenize button inside the brand app. When the customer clicks it, the app calls your brand API; your backend checks auth, checks the spendable points balance, creates an internal issuance or conversion row, and uses that row to build the signed issuance.

  • Use operationHash to connect the Loyfin action with your internal database row, such as a hash of your internal operation ID.
  • After POST /issuances returns successfully, deduct or lock the customer's spendable offchain points so the same balance cannot be tokenized twice.
  • Keep the row pending until confirmation. Today you can poll GET /operations or GET /issuances; issuer webhooks are coming soon; teams that want full independence can also read onchain events directly.
  • The public API is the default path for performance and efficiency. Brands can also submit the issue transaction themselves or run their own relayer for a fully self-custodial issuance path.

State machine#

The brand database remains the source of customer truth.

01

Eligible

Your backend checks customer auth, fraud rules, spendable offchain balance, and campaign constraints.

02

Reserved

Create an internal operation row and reserve or deduct the offchain points before publishing the signature.

03

Created

POST the signed issuance to Loyfin. Store the returned operation ID and keep the brand row pending.

04

Completed

Poll the operation or issuance receipt. When mined, mark tokenization complete in your own system.