Skip to content

Cardano Pythathon: Factura Ya — Invoice Factoring Marketplace#97

Open
qmarquez wants to merge 8 commits intopyth-network:mainfrom
qmarquez:cardano-factura-ya
Open

Cardano Pythathon: Factura Ya — Invoice Factoring Marketplace#97
qmarquez wants to merge 8 commits intopyth-network:mainfrom
qmarquez:cardano-factura-ya

Conversation

@qmarquez
Copy link
Copy Markdown

@qmarquez qmarquez commented Mar 22, 2026

Pyth Examples Contribution

Team Name: Facturas Ya
Submission Name: Factura Ya
Team Members: Dario Fasolino, Macarena Carabajal
Contact: dario.a.fasolino@gmail.com, macacarabajal3@gmail.com

Type of Contribution

  • New Example Project (Adding a new example to demonstrate Pyth integration)
  • Bug Fix (Fixing an issue in existing examples)
  • Documentation Update (Improving README, comments, or guides)
  • Enhancement (Improving existing functionality or adding features)
  • Hackathon Submission (Submitting a project from a hackathon)

Project Information

Project/Example Name: Factura Ya

Pyth Product Used:

  • Pyth Price Feeds
  • Pyth Entropy
  • Multiple Products
  • Other: ___________

Blockchain/Platform:

  • Cardano
  • Ethereum/EVM
  • Solana
  • Aptos
  • Sui
  • Fuel
  • Starknet
  • TON
  • Other: ___________

Description

What does this contribution do?

Factura Ya is an on-chain invoice factoring marketplace built on Cardano. Latin American SMEs tokenize their outstanding invoices as NFTs, deposit collateral as a good-faith guarantee, and sell the collection rights to investors at a discount. The SME gets paid immediately; the investor collects the full amount at maturity.

How does it integrate with Pyth?

Pyth is central to the entire valuation pipeline:

  • On-chain (Aiken): pyth_oracle.ak calls pyth.get_updates() to read the verified ADA/USD price feed (feed ID 16) from the Pyth withdraw-script redeemer. Price freshness is enforced (max 60s). usd_to_lovelace() converts invoice values using the real-time price.
  • Marketplace listing: When an invoice is listed, the current Pyth price is snapshotted into the listing datum as a reference for investors.
  • Off-chain (TypeScript): PythPriceClient subscribes to Pyth Pro WebSocket for live updates. Transaction construction uses @pythnetwork/pyth-lazer-cardano-js (getPythState(), getPythScriptHash()) with the PreProd Policy ID d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6.

What problem does it solve or demonstrate?

SMEs in Argentina wait 60-90 days to collect on invoices. Traditional factoring charges 3-5% monthly. Factura Ya enables instant liquidity through on-chain invoice tokenization with transparent pricing powered by Pyth oracles — no banks, no intermediaries.

Architecture

  • 3 Aiken validators: invoice_mint (NFT minting policy), escrow (collateral lock/release/forfeit), marketplace (list/purchase/delist)
  • Pyth oracle module: price feed consumer with freshness validation + USD-to-lovelace conversion
  • Off-chain: TypeScript tx builders using Evolution SDK + Pyth Cardano JS SDK
  • Indexer: Oura (TxPipe) pipeline + Express REST API
  • Frontend: React + Vite with CIP-30 wallet connection (Nami/Eternl/Lace)
  • 32 Aiken tests passing

Testing & Verification

Prerequisites

  • Aiken v1.1.21+
  • Node.js 18+
  • Pyth API key

Setup & Run Instructions

cd lazer/cardano/factura-ya

# Run indexer
cd indexer && npm install && npm start

# Run frontend
cd frontend && npm install && npm run dev

Deployment Information

Target network: Cardano PreProd
Pyth Policy ID: d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6

Checklist

Code Quality

  • Code follows existing patterns in the repository
  • Proper error handling implemented
  • No hardcoded values (use environment variables where appropriate)

Testing

  • Tested locally and works as expected
  • All existing functionality still works (no breaking changes)

Notes for Reviewers

Hackathon submission for the Cardano Pythathon (2026-03-22). This creates the first lazer/cardano/ directory in the repo.


Open with Devin

qmarquez and others added 8 commits March 22, 2026 11:34
Hackathon submission for the Cardano Pythathon.

Factura Ya is an on-chain invoice factoring marketplace on Cardano.
SMEs tokenize outstanding invoices as NFTs, deposit collateral, and
sell collection rights to investors at a discount. Pyth's ADA/USD
price feed provides real-time currency conversion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +150 to +155
fn get_validity_start(tx: Transaction) -> Int {
when tx.validity_range.lower_bound.bound_type is {
Finite(time) -> time
_ -> 0
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 get_validity_start returns 0 for non-Finite bounds, disabling due-date validation in invoice minting

When a transaction does not set a finite lower validity bound (e.g., NegInf), get_validity_start returns 0 (invoice_mint.ak:153). The mint validation check at line 53 becomes datum.due_date_posix_ms > 0, which is trivially true for any positive timestamp. This effectively disables the "due date must be in the future" invariant, allowing invoices with past due dates to be minted. The function should fail or expect Finite(time) instead of silently returning 0.

Note: the same function in escrow.ak:72 is safe because there the check direction is reversed (lower >= due_date), so returning 0 makes the check stricter, not weaker.

Suggested change
fn get_validity_start(tx: Transaction) -> Int {
when tx.validity_range.lower_bound.bound_type is {
Finite(time) -> time
_ -> 0
}
}
fn get_validity_start(tx: Transaction) -> Int {
expect Finite(time) = tx.validity_range.lower_bound.bound_type
time
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +497 to +506
const INVOICE = {
amount: ${params.amount},
days: ${params.days},
debtor: "${params.debtor}",
contact: "${params.contact}",
policyId: "${scripts.invoiceMint.hash}",
escrowHash: "${scripts.escrow.hash}",
marketplaceHash: "${scripts.marketplace.hash}",
invoiceMintCbor: "${scripts.invoiceMint.cbor}",
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 XSS via unsanitized user input interpolated into JavaScript in registerPage

In registerPage, URL query parameters debtor and contact are interpolated directly into JavaScript string literals without escaping. A crafted URL like /register?debtor=";alert(document.cookie);// would break out of the JS string and execute arbitrary JavaScript in the user's browser. While this server runs on localhost, it's accessible to any page that can make requests to it.

Vulnerable template interpolation
const INVOICE = {
  amount: ${params.amount},      // numeric injection possible
  days: ${params.days},          // numeric injection possible
  debtor: "${params.debtor}",    // XSS
  contact: "${params.contact}",  // XSS
};
Prompt for agents
In lazer/cardano/factura-ya/offchain/src/deploy-server.ts, the registerPage function (starting around line 461) interpolates user-supplied URL query parameters directly into a JavaScript template string at lines 497-506. The params.debtor, params.contact, params.amount, and params.days values come from URL query parameters and need to be sanitized before interpolation into JavaScript.

Add a helper function to escape strings for safe JavaScript string literal inclusion, escaping characters like backslash, quotes, angle brackets, and newlines. Apply it to params.debtor and params.contact at lines 500-501. For params.amount and params.days (lines 498-499), parse them as numbers with parseInt/parseFloat and provide safe defaults (e.g., 0) to prevent code injection through numeric fields.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +55 to +57
app.get("/invoices/all", (_req, res) => {
res.json({ invoices: [...invoices.values()], total: invoices.size });
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Express route /invoices/all is shadowed by /invoices/:id and unreachable

The route GET /invoices/all at line 55 is registered after GET /invoices/:id at line 45. Express matches routes in registration order, so a request to /invoices/all matches /invoices/:id with req.params.id = "all", which looks up "all" in the invoices Map and returns a 404. The /invoices/all handler is never reached.

Prompt for agents
In lazer/cardano/factura-ya/indexer/src/api.ts, move the route definition for GET /invoices/all (currently at lines 55-57) to BEFORE the GET /invoices/:id route (currently at lines 45-52). Express matches routes in registration order, so the more specific /invoices/all must come before the parameterized /invoices/:id.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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