A session, a feature, and a pass each leave a record the contract can answer later. A receipt does not. It is proof that one payer paid one amount for one product, and that amount can change from purchase to purchase when the product allows it.

What a receipt keeps
PaymentMode.RECEIPT is 2. In _processPaymentMode, a session writes a validUntil timestamp, a feature sets an unlock, and a pass sets ownership. The receipt branch writes nothing. The contract comment says the payment is verified from event logs.
Every successful pay still emits Paid. For a receipt, mode is 2 and validUntil is 0. The event also carries amount, token, quoteId, and a receiptHash:
bytes32 receiptHash = keccak256(
abi.encodePacked(
q.appId,
q.productId,
q.payer,
q.amount,
q.quoteId,
block.timestamp
)
);quoteId is marked used, so that signed quote cannot pay twice. A second purchase needs a new quote. The contract tests pay a receipt twice and expect the two receiptHash values to differ. There is no hasReceipt view. Understanding PayXor's Four Payment Modes is the short map of the four modes. This post is the receipt path with a buyer-chosen amount.
Turn the flag on for mode 2 only

In the dashboard, a receipt product shows Allow custom amounts. With that box checked and the price left blank, the saved price is "0". A dollar amount entered before the box is checked is still stored, as that number times 1000000. The checkbox is sent only when the mode is a receipt.
The admin API, and create_product on @payxor/mcp-admin, take the same choice as allowsCustomAmount. It is accepted only when mode is 2. Any other mode returns allowsCustomAmount can only be enabled for RECEIPT mode (mode 2) products. A receipt that should take a custom amount uses price: "0" and allowsCustomAmount: true. price is a digit string. The product id is the keccak256 of the app name, a colon, and the product name, the same as any other product. Admin calls take the app UUID. See From a question to a signed quote, in two MCP servers.
A receipt can also keep a fixed price. Leave allows_custom_amount off, which is the default, and every quote uses price.
getProductInfo does not tell you which kind you created. The public product payload is appId, productId, name, mode, price, duration, and entitlementId. The admin product response includes allows_custom_amount. The checkout has to already know that this product takes a custom amount.
The quote signs the amount you send

POST /api/quote takes an optional amount. It has to be a digit string greater than zero, in the smallest unit of the token the payer will spend. 1000000 is 1 USDC where that token has 6 decimals. One DAI, at 18 decimals, is 1000000000000000000. The dashboard's dollar field always multiplies by 1000000 when it saves a fixed price. A custom amount does not go through that field. Send the raw units of the token selected for this payment.
The route allows that field only for a receipt that opted in:
- A session, feature, or pass returns
Custom amounts are only allowed for RECEIPT mode products. - A receipt with
allows_custom_amountfalse returnsThis product does not allow custom amounts. - When
amountis omitted, the quote usesproduct.price.
If that stored price is "0", the signed amount is zero. pay reverts with InvalidAmount when q.amount == 0. A pay-what-you-want product has to send amount on every quote.
The value that comes back on quote.amount is the one the contract will transfer. The payer cannot edit it. The signature check is the one in Backend-Authoritative Pricing. The contract then splits a fee from that amount and sends the remainder to the app payee.
Pass one string to both hooks

useTokenPayment and usePayment both take amount. Pass the same string. The token hook uses it as the price for the balance and the allowance, and it does not fall back to getProductInfo while that string is set. needsApproval is true when the allowance is below that amount. approve() still sends a max-uint256 approval. usePayment forwards the string to getQuote, and pay submits the quote that comes back.
const amount = "2500000";
const tokens = useTokenPayment({
appId,
productId,
address,
chainId,
publicClient,
walletClient,
apiUrl,
amount,
});
const payment = usePayment({
appId,
productId,
address,
chainId,
publicClient,
walletClient,
apiUrl,
tokenAddress: tokens.selectedToken,
amount,
});2500000 is 2.5 USDC at 6 decimals. If the payer switches to an 18-decimal token, the string has to change with it. The allowance check and the quote have to agree. The rest of the wallet sequence is From wallet connect to entitlement, in two React hooks.
get_quote on @payxor/mcp-client takes the same optional amount. The client server still has no pay tool and no approve tool.
Read the proof from the transaction

After the transaction is mined, confirm it with the backend the same way as any other mode, using the transaction hash and the quoteId. There is no useReceiptStatus hook. useSessionStatus, useFeatureStatus, and hasPass do not change because of a receipt.
verifyReceipt(txHash, quoteId, chainId) loads the transaction receipt. It returns true when the transaction succeeded and the receipt contains a log from the PayXor contract. It does not decode Paid. To tell one purchase from another, read that event. Indexed fields are appId, productId, and payer. The data includes amount, token, mode, receiptHash, and quoteId.
That log is the receipt.