Settlement payments
An ERC-20 transfer carries a recipient and an amount and nothing else. It cannot say what it pays
for. A payment through the Settlement contract can: the chain is left with a single
Settled(ref, payer, merchant, token, amount) event, and ref — the order reference — is computed
by the contract from what was actually paid.
What the contract guarantees
- One order, one payment. A second
payof the same reference reverts. - An expired order cannot be paid.
- Only the named payer can settle an order — the payer is part of the reference.
- It holds nothing. Funds go from the payer to the merchant in the same call. There is no owner and no upgrade path; nobody can designate or change a destination.
Pay an order
import { settleOrder } from "kawasekit";
const { ref, transactionHash } = await settleOrder(kernelClient, { merchant: order.merchant, amount: order.amount, // raw units; JPYC has 18 decimals validUntil: order.validUntil, // unix seconds, bigint details: order.details, // bytes32 — see below expectedRef: order.ref, // what the order's issuer quoted});settleOrder sends one UserOp carrying [JPYC.approve(Settlement, amount), Settlement.pay(…)].
The smart account’s batch makes approve-and-pay a single atomic payment, and the allowance is for
exactly the amount.
Three things are deliberately not parameters: the payer (it is the account — the contract hashes
msg.sender), and the Settlement address and the token (they come from this package’s tables for
the client’s chain). An order’s issuer may tell you which contract to pay; settleOrder does not
listen.
Always pass expectedRef when someone else issued the order
The reference is recomputed locally and compared before anything is sent. If it differs,
settleOrder throws SettlementOrderRefMismatchError — the order you were quoted is not the order
you are about to pay.
If a payment fails, do not guess
An order can be paid once. A retry of an already-paid order is refused by the contract, and that
most likely reaches you as a thrown bundler error, not as success: false. ref is known
before anything is sent, so ask the chain:
import { getSettlementAddress, polygonAmoy, settlementAbi } from "kawasekit";
const paid = await publicClient.readContract({ address: getSettlementAddress(polygonAmoy.id), abi: settlementAbi, functionName: "settled", args: [ref],});The order reference
| hash | who computes it | what it covers |
|---|---|---|
ref | the contract (and hashOrderRef, identically) | token, payer, merchant, amount, validUntil, details — under an EIP-712 domain that includes the chain and the deployment |
details | you, off chain (hashOrderDetails) | the order id, its lines, and a 32-byte salt |
import { hashOrderDetails, hashOrderRef } from "kawasekit";
const details = hashOrderDetails({ orderId: "ord_01J8ZK3V7Q", lines: [{ itemId: "item_espresso", unitPrice: 1200n * 10n ** 18n, quantity: 1 }], salt, // 32 random bytes, fresh per order});The contract never sees what details commits to, so nothing readable about a purchase reaches the
chain. Anyone who holds an order’s contents can verify a payment without trusting whoever issued the
order: recompute details, recompute ref, look for Settled(ref, …).
Scope a session key to Settlement
import { createBuyListPolicies, getJpycAddress, getSettlementAddress, polygonAmoy } from "kawasekit";
const policies = createBuyListPolicies({ jpycAddress: getJpycAddress(polygonAmoy.id), settlementAddress: getSettlementAddress(polygonAmoy.id), merchants: [merchantA, merchantB], maxPerTransfer: cap, validUntil,});The key may call JPYC.approve(Settlement, ≤ cap) and Settlement.pay(JPYC, merchant ∈ merchants, ≤ cap, …) — and nothing else. A bare JPYC.transfer is refused on chain, so every yen such a key
moves carries an order reference.
Keys issued by kawasekit ≤ 0.10.x were scoped to JPYC.transfer. To revoke one, rebuild its
policies with the deprecated createLegacyTransferBuyListPolicies (removed in 0.12.0).