Skip to content

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

Open
M2M-TRC8004-Registry wants to merge 1 commit intoBofAI:mainfrom
M2M-TRC8004-Registry:feature/seller-receipt-signing
Open

feat: seller receipt signing (EIP-191) for PurchaseLog#63
M2M-TRC8004-Registry wants to merge 1 commit intoBofAI: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
)

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.

1 participant