Docs/Integrate your program/Add to wallet
Add to wallet
Create a signed issuance to convert a customer’s eligible points into tokens in their wallet.
When a customer chooses Add to wallet, your backend reserves or deducts the selected points and creates a signed issuance. The customer completes checkout to receive those points as tokens. Your existing loyalty system stays in place. The first successful issuance creates the program contract from the included metadata if it does not exist yet.
Inputs#
| Field | Type | Required | Meaning |
|---|---|---|---|
issuer | address | external signing | Brand wallet that signs the action. Omit together with the issuer signature for enabled API signing. |
loyaltyId | bytes32 | yes | Brand-scoped program ID. |
amount | uint256 string | yes | Number of eligible points to convert into wallet tokens. 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": "1790965800",
"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."
},
"data": "0x"
},
"signature": "0xababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab",
"to": "0x2222222222222222222222222222222222222222"
}// Run on your backend. Sign the issuance as shown in Signing.
// verifiedWallet is the recipient saved with this customer’s conversion.
const response = await fetch("https://api.loyfin.com/issuances", {
method: "POST",
headers: {
"content-type": "application/json",
"x-loyfin-api-key": process.env.LOYFIN_API_KEY
},
body: JSON.stringify({
issuance,
signature,
to: verifiedWallet
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const { issuance: session, duplicate } = await response.json();Next step#
Store the issuance and show Awaiting wallet authorization, then give the private checkout URL to the intended customer. Checkout collects the recipient authorization and asks the customer wallet to submit the transaction with the action fee and gas. 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 loyalty ledger.
Add an Add to wallet button to your existing app. When a logged-in customer chooses an amount, your backend checks eligibility and the available balance, creates a conversion record, and reserves or deducts those points. It then signs the issuance using that record.
- 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 points in the brand account before publishing the signed issuance, so two fast requests cannot tokenize the same balance twice.
- Keep the conversion pending until finalized completion. Use verified completion webhooks by default. Polling or direct chain reads are fallbacks and must verify finality before completing the conversion.
- The session API is the default coordination path. The customer submits through Loyfin Checkout, or the brand can embed the same wallet-paid
issueflow in its own interface.
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 spend the same 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.
- Release reserved points only after verified expiry with reservationReleaseSafe: true, or equivalent finalized chain checks. Reconcile other failures before restoring points.
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 points in the brand account before publishing the signature.
Awaiting authorization
POST the issuance, store its operation ID, and give the customer the checkout link. Keep your internal conversion pending.
Completed
After finalized completion, mark the conversion complete in your database.