Docs/Build guides/Add to wallet
Add to wallet
Prepare and store a signed issuance that tokenizes selected existing offchain points or loyalty value into a customer-owned Loyalty Token balance.
Use issuance when a logged-in customer converts eligible offchain points or loyalty value from the brand's existing ledger into a tokenized Loyalty Token balance. Do not use this as the default path for every ecommerce purchase. If the Loyalty Token does not exist yet, the first successful issuance creates it automatically from the included metadata.
Inputs#
| Field | Type | Required | Meaning |
|---|---|---|---|
issuer | address | yes | Brand wallet that signs the action. |
to | address | session | Customer wallet receiving tokenized loyalty value. Not part of the issuer-signed terms; add it with the recipient signature or collect it on checkout. |
loyaltyId | bytes32 | yes | Brand-scoped program ID. |
amount | uint256 string | yes | Amount of offchain loyalty value to tokenize. Must be greater than zero. |
expiresAt | uint256 string | yes | Use 0 for no expiry, or a Unix timestamp for an expiring bucket. |
deadline | uint256 string | yes | Last Unix timestamp when the issuance can be submitted. Use 0 for no execution deadline. |
nonce | bytes32 | yes | Unique per issuer. Replays are rejected. |
chainId | uint256 string | yes | Expected chain ID. Loyfin uses Base 8453. |
verifyingContract | address | yes | The Loyfin factory that will execute the action. |
operationHash | bytes32 | optional | Brand reference hash for correlating this action with an internal database row. Defaults to zero bytes when omitted. |
metadata | Metadata | yes | Program metadata. Required for issuance because the first issuance can create the program. |
data | hex bytes | optional | Brand-defined data. Defaults to 0x. Max 2048 bytes. |
Request#
{
"issuance": {
"issuer": "0x1111111111111111111111111111111111111111",
"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"
}import { Loyfin, issuanceTypedData } from "@loyfin/server";
const loyfin = new Loyfin({ apiKey: process.env.LOYFIN_API_KEY });
const typedData = issuanceTypedData(issuance);
const signature = await issuerSigner.signTypedData(typedData);
const { issuance: session, duplicate } = await loyfin.issuances.create({
issuance,
signature
});
// Show session.checkoutUrl when the recipient still needs to sign.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
chainIdorverifyingContractmakes 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 an Add to wallet 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 conversion row, reserves or deducts the selected offchain points, and uses that row to build the signed issuance.
- Use operationHash to connect the Loyfin action with your internal database row. Use a secret-keyed HMAC or a stored random bytes32 value; do not publish a guessable raw-ID hash.
- Reserve or deduct the customer's spendable offchain points before publishing the signed issuance, so two fast requests cannot tokenize the same balance twice.
- Keep the row pending until confirmation. Loyfin sends account-level webhooks for completed issuance receipts; teams can also poll
GET /operations/:idorGET /issuances, or read onchain events directly. - The public API is the default path for performance and efficiency. Brands can also submit the
issuetransaction themselves or run their own relayer for a fully self-custodial issuance path.
Double-spend safety#
The brand ledger must reserve value before a signed issuance can be published.
Loyfin prevents replay of the same issuer nonce, but it cannot know whether two different nonces both came from the same offchain points. The brand backend should use a row lock, serializable transaction, or atomic update that succeeds only when the customer has enough spendable balance.
- Create one internal operation row with a stable ID,
operationHash, nonce, wallet address, amount, and status. - Retry transient Loyfin API failures with the same signed payload instead of creating a fresh nonce for the same offchain deduction.
- If the signed operation expires or is rejected, release the reservation or move it to manual review based on your support policy.
State machine#
The brand database remains the source of customer truth.
Eligible
Your backend checks customer auth, fraud rules, spendable offchain balance, and campaign constraints.
Reserved
Create an internal operation row and reserve or deduct the offchain points before publishing the signature.
Created
POST the signed issuance to Loyfin. Store the returned operation ID and keep the brand row pending.
Completed
Poll the operation or issuance receipt. When mined, mark tokenization complete in your own system.