Skip to content

feat: seller receipt signing (EIP-191) for PurchaseLog - #63

Closed
M2M-TRC8004-Registry wants to merge 1 commit into
BofAI:mainfrom
M2M-TRC8004-Registry:feature/seller-receipt-signing
Closed

feat: seller receipt signing (EIP-191) for PurchaseLog#63
M2M-TRC8004-Registry wants to merge 1 commit into
BofAI:mainfrom
M2M-TRC8004-Registry:feature/seller-receipt-signing

Conversation

@M2M-TRC8004-Registry

Copy link
Copy Markdown

Summary

  • New receipt_signer.py: Implements compute_receipt_digest() and sign_receipt() for EIP-191 personal-sign over keccak256(abi.encode(listingId, buyerAgentId, paymentHash, amount, chainId, contractAddress)), matching PurchaseLog.sol's on-chain verification.
  • FastAPI middleware: Adds optional SellerSigningConfig parameter to x402_protected(). After successful settlement, the middleware auto-signs a receipt and includes it in the PAYMENT-RESPONSE header as receiptSignature.
  • paymentHash = SHA-256 of PAYMENT-SIGNATURE header: The payment hash is derived from the buyer's raw payment header (not the settlement tx hash), ensuring a cryptographic binding between the x402 payment and the on-chain purchase record.
  • TypeScript + Python types: ReceiptSignatureData added to both SDKs' SettleResponse.
  • Timing fix: validAfter = now - 60 to prevent facilitator not_yet_valid race conditions.

Security

  • uint32 overflow validation on listingId and buyerAgentId before signing
  • EIP-191 envelope prevents cross-protocol signature reuse
  • chainId + address(this) in digest prevents cross-chain / cross-contract replay
  • s ≤ half-order enforced by eth-account's signing library (malleability guard)

Usage

from bankofai.x402.fastapi import x402_protected, SellerSigningConfig

signing = SellerSigningConfig(
    private_key="<seller-hex-key>",
    listing_id=5,
    purchase_log_address="<PurchaseLog-contract-address>",
)

@app.get("/data")
@x402_protected(requirements, seller_signing=signing)
async def get_data(request: Request):
    return {"data": "premium content"}

Test plan

  • Full e2e on TRON Nile: agent registration → listing → x402 purchase → PurchaseLog.logPurchase() → verified: true
  • Verified paymentHash matches across seller middleware, buyer receipt, and on-chain submission
  • Confirmed receipt signature recovers to seller address on-chain

Implement ECDSA receipt signing so sellers can attest to purchases
on-chain via PurchaseLog.sol. The seller signs a digest of
(listingId, buyerAgentId, paymentHash, amount, chainId, contractAddress)
using EIP-191 personal-sign, and the buyer submits this signature
to PurchaseLog.logPurchase() for on-chain verification.

Key changes:
- New receipt_signer.py: compute_receipt_digest() and sign_receipt()
- SellerSigningConfig for endpoint-level configuration
- FastAPI middleware: auto-sign receipts after successful settlement
- paymentHash = SHA-256 of PAYMENT-SIGNATURE header (not tx hash)
- uint32 overflow validation for listingId and buyerAgentId
- validAfter -= 60s to prevent facilitator timing edge cases
- ReceiptSignatureData types added to Python and TypeScript SDKs
@M2M-TRC8004-Registry

Copy link
Copy Markdown
Author

⚠️ Important: Buyer must send X-Buyer-Agent-Id header

The buyer must include the X-Buyer-Agent-Id header (or whatever header name is configured via SellerSigningConfig.buyer_agent_id_header) when making the x402 payment request.

If this header is missing, buyer_agent_id defaults to 0 (anonymous), and the receipt digest will be signed with buyerAgentId=0. This means when the buyer later submits the receipt to PurchaseLog.logPurchase() with their actual agent ID, the on-chain ecrecover will recover a different address than the seller's — causing the purchase to be recorded as verified: false.

Buyer request example:

GET /data HTTP/1.1
PAYMENT-SIGNATURE: <base64-encoded-payment-payload>
X-Buyer-Agent-Id: 157

The header name is configurable:

SellerSigningConfig(
    private_key="...",
    listing_id=5,
    purchase_log_address="...",
    buyer_agent_id_header="X-Buyer-Agent-Id",  # default
)

@boboliu-1010

Copy link
Copy Markdown
Collaborator

Thanks for the contribution and for the detailed PurchaseLog receipt-signing work.

We’re going to close this PR as not planned for now. The feature is useful, but the implementation couples an application-specific PurchaseLog receipt format directly into the core x402 FastAPI middleware and shared SettleResponse types. Since the current 0.6.x direction is separating SDK / middleware / gateway responsibilities, we don’t want to merge this into the core protocol surface as-is.

If we revisit seller receipt signing, we’d likely bring it back as a separate extension or app-level helper with explicit tests and a design that works across the current Python and TypeScript middleware layers, rather than embedding PurchaseLog-specific behavior in the generic settlement response.

Thanks again for the thoughtful implementation and e2e validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants