Skip to main content

Payment request

A recipient generates a payment request and sends the link to the payer. The payer sends the funds and the recipient then discovers the incoming payment via the request's tag.

Why use it

  • Neither party reveals their address. Two people can request and settle a payment without learning each other's wallet address. The shared request carries per-request keys and pre-computed note slots rather than the recipient's address, and the payer fulfills it through a relayer, so neither side sees the other's wallet.
  • The recipient doesn't need to be registered. Receiving normally relies on a viewing key registered in the keystore; a request instead supplies its own per-request key, so even a brand-new wallet can be paid before it ever registers.
  • Payments reconcile to your own records. Issue one request per invoice or order and keep the request-to-reference mapping locally. When a payment arrives you can match it back to that invoice, even though the on-chain note carries no invoice number.

Step 1: recipient creates a request

const toHex = (value: bigint): `0x${string}` => `0x${value.toString(16)}`;
const paymentRequest = await session.generatePaymentRequest({
amount: toHex(parseUnits("0.5", 6)),
tokenId: USDC_SEPOLIA,
});

console.log("Pay this:", JSON.stringify(paymentRequest));

generatePaymentRequest() returns the public request shape: paymentId, noteAddressHashes, amount, tokenId, tag, and paymentPubKey. Recipient secrets stay in local request storage. Your app decides how to encode the public request into a link or QR code. If your app also packages recipient recovery state such as noteSecretsMap, treat that payload as sensitive.

Step 2: payer sends

const prepared = await session.prepareFulfillPaymentRequest({
paymentRequest: paymentRequestFromShareLink,
inputCommitments: [/* picked input */],
// optional: relayAddress, feeAmount, processorAddress to override
});

await session.relayTransfer(prepared.relayOptions[0]);

prepareFulfillPaymentRequest() wraps prepareTransfer() with the request's tag and encryption target. It returns the same relay/self-submit option shape as a normal transfer.

Step 3: recipient discovers

await session.discoverNotes();

const acct = await session.exportAccount();
const matched = acct.notes.filter(
(n) => paymentRequest.noteAddressHashes.includes(n.noteAddressHash),
);

for (const note of matched) {
console.log("Received:", note.value);
}

Discovery re-derives each open request's paymentPrivKey, matches Note events by tag, and marks matched requests as fulfilled. Notes do not carry paymentId, so correlate them with the request's pre-committed noteAddressHashes.

Gotchas

  • Tag isn't authenticated: a sender can stamp a payment with a different request's tag. Pair with off-chain signing if non-repudiation matters.
  • One event batch, many tags: discovery reuses the same ASP/RPC Note event batch and matches events against your open request tags. Prune fulfilled requests to keep the map small.