Skip to content
DiegoPoveda01Public

About

x402 payments priced in local currency on Stellar: 24 currencies + UF, FxPay contract on mainnet

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Repository files navigation

Local402

Charge in your currency. Receive exact USDC.

npm Stellar mainnet CI tests License: MIT

English · Español

Local402 is x402 on Stellar for the rest of the world: a seller prices an API route in their own currency — "1 MXN", "5 INR", "70 NGN", "0.05 EUR", "0.01 UF" — and a payer settles it with whatever they hold: USDC, XLM or EURC. The seller always receives the exact USDC amount, in one transaction, with no manual currency swap on either side.

An agent pays 70 NGN with XLM on testnet: the 402, the signed FxPay swap, and the seller receiving exact USDC

Demo video (3 min) https://youtu.be/SNeQ8wZo3hg
Dashboard (testnet) https://local402.xyz
Live API (Stellar mainnet) https://local402-mainnet.vercel.app
Scheme spec docs/scheme_exact_fx_stellar.md
npm local402-pricing · local402-fx · local402-client · local402-server · local402-facilitator · local402-mcp
FxPay on mainnet CA6Z4E55YN6LZEXBQPFCWUIMUJGAV42RLHYWWZ6SNIP4E73R2I2PUGHD
Real payments through it XLM → USDC 0babefe1… and EURC → USDC bfe6a44d…, 2026-09-20 — each delivered exactly 0.0520721 USDC to the seller and refunded the unused input
An outside seller charging with it kuyfi prices each scan at 500 CLP — the full paid flow on video (28 s) · testnet tx 44ef0d00…
Security threat model · internal review — not audited, so mainnet settles only for the demo seller and from 0.01 USDC
October sprint (done) sprint plan: 1.0 packages, a deployable facilitator, the integration guide (followed from scratch by someone outside the project, #3), kuyfi on 1.0 (kuyfi#5)
Reproducible build release — wasm 69a12d89…, byte for byte what is deployed; Verified Build (SEP-55) on Stellar Lab
Bug found and fixed upstream x402#3491 — mainnet rejects the fee the SDK bids; our fix, x402#3503, is merged

The problem

x402 lets an HTTP resource charge per request and lets an AI agent pay on its own. Today, though, the price is a dollar amount settled in a single asset. Write price: "70 NGN" and the SDK fails.

That is not how commerce outside the US works. A Mexican API bills in pesos, an Indian one in rupees, a Nigerian one in naira; a Chilean rental contract is denominated in UF, an inflation-indexed unit that changes daily. And the customer pays with whatever is in their wallet, which is usually not the seller's asset.

Where it works today

Local402 prices in any currency the Reflector fiat oracle publishes on mainnet (checked 2026-09-16), plus USD and the UF:

Region Currencies
Latin America MXN, BRL, COP, PEN, ARS, CLP, CRC, VES
Africa NGN, KES, ZAR, CDF
Asia INR, JPY, CNY, KRW, PHP, HKD
Europe and North America EUR, GBP, TRY, RUB, CAD

A new currency needs no code: the day Reflector publishes it, localRoute("… XYZ") prices in it. The demo sells one route per region (/latam in MXN, /asia in INR, /africa in NGN) next to the euro and Chilean ones. Chile is the deepest example because of the UF, which shows that even an indexed unit that is not a currency works the same way.

What Local402 adds

Two independent layers. Either one is useful on its own.

1. Local-currency pricing (local402-pricing, local402-server). The price stays in the seller's currency. The Reflector on-chain oracle converts it to USDC at request time, rounding up so the seller never receives less than the local price. The quote travels inside the standard 402 response as extra.local402, and the requirement itself is a plain exact USDC requirement — any existing Stellar x402 client pays it unchanged.

import { paymentMiddleware } from "@x402/express";
import { localRoute, local402Server } from "local402-server";

app.use(paymentMiddleware(
  { "GET /africa": localRoute("70 NGN", { payTo: SELLER_ADDRESS }) },
  local402Server(FACILITATOR_URL),
));

The seller signs each quote with LOCAL402_QUOTE_SECRET (32+ characters, openssl rand -hex 32) and refuses to start without it. examples/quickstart is the runnable version, and docs/errors.md lists every error code.

2. exact-fx: pay with a different asset (local402-fx, contracts/fx-pay). A new x402 scheme. The payer signs a call to the FxPay Soroban contract, which swaps their XLM or EURC on Soroswap for exactly the USDC the seller asked for, delivers it, and refunds the unused input — all atomically. If the swap cannot deliver the exact amount within the payer's limit, the whole transaction reverts and nothing moves.

The seller never touches XLM. The facilitator pays the network fee. The payer signs once.

How it differs from x402 on Stellar today

Stellar's x402 docs support any SEP-41 token, USDC by default, through the Coinbase facilitator (testnet) and the OpenZeppelin Relayer plugin. Both settle x402's exact scheme: the price is an amount of one token, and the payer transfers that very token. The x402 projects on Stellar we could find in the ecosystem directories and past hackathons (checked 2026-09-22) — facilitators, MCP templates, paywalls, agent SDKs — build on that same scheme.

The three closest, as their own READMEs describe them (checked 2026-10-06):

Project What it solves The price is written in The payer pays with
Rail402 Who settles: an x402 facilitator that sponsors the fee, plus a Bazaar where agents find paid APIs and MCP tools USD, settled as USDC (price: "$0.01", scheme exact) USDC
REAPP Who may spend: AP2-style mandates and a Soroban registry that enforces an agent's budget, scope and revocation USDC (AGENT_PLAN_PRICE_USDC) USDC
x402 MCP Stellar Template How to start: Node, Python and Go templates for paid MCP servers, with optional on-chain daily caps USDC (x402({ price: 0.10 })) USDC
Local402 What the price is, and what the payer brings The seller's currency, converted by Reflector per request USDC, XLM or EURC

They answer different questions, so they can stack rather than compete. Local402's exact option is a plain USDC requirement, which is the kind of payment Rail402 settles and a REAPP mandate is written for. We have not tested either combination yet. What none of the three does is let a seller write "500 CLP", or let a payer settle it in XLM while the seller still receives exact USDC.

x402 on Stellar today Local402
The price is written in an amount of the settlement token the seller's currency — CLP, UF, NGN, EUR… — converted by Reflector at request time
The payer pays with the seller's token USDC, XLM or EURC; FxPay swaps inside the payment
The seller receives that token the exact USDC amount, or the transaction reverts
A stock x402 client pays still pays: every 402 keeps a plain exact option next to exact-fx

Local402 does not replace a facilitator or fork x402. It is six npm packages and one contract on top of it, and the one change it needed upstream — the fee bid — went back into x402 itself.

Where you'd use it

Every case below runs on the code in this repository, not on a roadmap.

  • A metered API priced in your own market. You quote 70 NGN or 50 CLP; the buyer is charged that, and you receive USDC. You never publish a dollar figure and never carry the exchange rate on your pricing page — local402-server is a one-liner over any x402 route.
  • Agents paying agents. The MCP server exposes each route as a tool; an agent reads the 402, quotes it against its own oracle, and pays per call with whatever asset it holds. No subscription, no account, no human in the loop.
  • Pay-per-request instead of a plan. A settled payment costs about 0.0024 XLM in network fee (measured, below). That makes a single API call, a single article, or a single inference worth charging for on its own.
  • Letting the payer bring their own asset. With exact-fx the buyer pays in XLM or EURC and you still receive the exact USDC you asked for — the swap and the refund happen inside the payment. One less "first, go get USDC" step between the buyer and the sale.
  • Billing in a unit that isn't a currency. UF is wired the same way as a fiat code, so a rent or an inflation-indexed contract can be priced in the unit it's actually written in.
  • Books that reconcile themselves. Every sale leaves a receipt with the local price, the rate and its source, the swap premium in basis points, and the transaction hash. GET /receipts.csv hands accounting a ledger, not a screenshot.

Someone else is already charging with it

▶ Watch it: a paid kuyfi scan, end to end on testnet (28 s video, on X) GET /scan/… → 402 at 500 CLP = 0.509682 USDC → payment to the kuyfi operator → 200 with the scan → tx 44ef0d00…c8abf42. The video is animated from the real request, response and transaction below.

kuyfi is an independent, MIT-licensed project — an OSINT scanner that maps the attack surface of a Soroban contract. It added an optional kuyfi-server that puts a price on each scan with local402-server: GET /scan/<contractId> answers 402 priced in Chilean pesos, and returns the read-only scan once the payment settles. That is the first integration of Local402 by a seller we did not write. The paywall itself is the same one line as the snippet above, a localRoute("500 CLP", …) call; the rest of the PR is the HTTP server kuyfi did not have before. We wrote that PR (kuyfi#1); kuyfi's maintainer reviewed and merged it, and the payments go to the maintainer's own account.

On testnet, a client paid one scan end to end:

The price 500 CLP, converted by Reflector to 0.509682 USDC at request time
What settled that exact USDC amount to the kuyfi operator, in one transaction; the payer paid 0 XLM, since the facilitator sponsors the fee
What came back the 200 scan of FxPay's own testnet deployment — five entry points over a 4821-byte wasm
Transaction 44ef0d00…c8abf42

For now the integration runs on testnet only. Moving it to mainnet (NETWORK=stellar:pubnet, real USDC) is planned, and is tracked in kuyfi#2. It needs one change on our side first: the mainnet facilitator settles only for the demo seller today (see What it does not do), so the kuyfi operator has to be added to its payee allowlist.

So the two projects now rely on each other both ways: Local402's own contract was scanned with kuyfi (see What it does not do), and kuyfi sells those scans with Local402.

Moving kuyfi onto the 1.0 packages, with paid testnet scans as the evidence, was the acceptance test of the October sprint (D5 in the sprint plan). It is done: kuyfi#5 is merged.

Try it in 60 seconds

No install, no wallet, no key. Ask the mainnet API for something and read what it answers:

# The 402 itself: the price is in naira, the requirement is in USDC
curl -si https://local402-mainnet.vercel.app/africa \
  | grep -i '^payment-required' | cut -d' ' -f2 | tr -d '\r' | base64 -d

# The same through the published client, which re-derives the price from its own oracle
npx -y local402-client quote https://local402-mainnet.vercel.app/africa
npx -y local402-client quote https://local402-mainnet.vercel.app/asia --with XLM
npx -y local402-client quote https://local402-mainnet.vercel.app/uf --with EURC

To pay for real, use the Varial (formerly Freighter) button in section 08 of the dashboard, or STELLAR_SECRET=S… npx -y local402-client pay <url> --with XLM --max "100 NGN". The --max limit can be in any supported currency, whatever the seller's, and the client refuses anything above it.

Quick start

npm install
npm run demo        # creates + funds throwaway testnet accounts, starts everything

Then open http://localhost:3001. The first run writes .demo-keys.local (gitignored); nothing else is needed — no faucet visit, no configuration.

To run the pieces separately, copy each .env.example and:

npm run facilitator   # :4022  settles exact and exact-fx, pays fees
npm run api           # :3001  demo seller + dashboard
npm run agent         #        a paying client
npm run mcp           #        MCP server: an agent discovers, quotes and pays

To charge in your own API from the npm packages, without this repository, follow the integration guide. Every function is in the API reference.

How a payment works

sequenceDiagram
    participant P as Payer / agent
    participant S as Seller (API)
    participant F as Facilitator
    participant N as Stellar

    P->>S: GET /africa
    S->>N: Reflector: 1 NGN = 0.00075233 USD
    S-->>P: 402 · accepts [exact, exact-fx] · extra.local402
    Note over P: Re-quotes from its OWN oracle<br/>rejects >2% overcharge<br/>bounds max_send, rejects >5% FX premium
    P->>F: signed FxPay.pay(...) authorization
    Note over F: 11 verification rules<br/>simulate · bid inclusion fee · fee-bump
    F->>N: submit
    N->>N: swap XLM on Soroswap for exactly 0.0526629 USDC
    N-->>S: 0.0526629 USDC — exact
    N-->>P: refund of the unused XLM
    F-->>S: tx hash → receipt
    S-->>P: 200 + the data
Loading

Measured, not estimated

Eleven real payments on Stellar mainnet between 15 and 20 September 2026, all of them successful. The last three went through the reproducible-build deployment. The latest ones are in the receipts.

What the seller received The exact USDC amount the 402 asked for, in all eleven payments.
Network fee, paid by the facilitator 0.0024 XLM for a USDC payment. For exact-fx, 0.081 and 0.044 XLM on a fresh deployment's first two swaps, then about 0.0055 once the contract's ledger entries are warm. Redeploying reproduced this exactly: 0.0807 and 0.0436.
What the swap actually spent 0.21–0.39% above the Reflector value of the price (the three payments on 15 September).
The most the payer authorized 2.29–3.17% above it. That includes the 2% slippage allowance, and whatever the swap does not use is refunded.
A 402 carrying a live quote About 0.41 s, warm.
A full payment on testnet: 402 → quote → sign → settle → 200 5.6 s with XLM, 6.8 s with USDC, 9.4 s with EURC (which routes through XLM).

Repository map

packages/
  pricing/   local currency → USDC.  Reflector oracle, UF source, x402 DynamicPrice
  fx/        the exact-fx scheme: client, resource server, facilitator, fee bidding
  client/    Local402Client — pays with USDC/XLM/EURC, with its own guards. Also a CLI
  server/    localRoute() and localToolPayment() — one line to price a route or an MCP tool
  facilitator/  verifies and settles, pays fees, serves the x402 Bazaar catalog. Runs with npx
apps/
  demo-api/      the seller: six paid routes, receipts, the dashboard
  agent/         minimal paying client
  mcp/           MCP server an AI agent uses to discover, quote and pay
  mcp-seller/    an MCP server whose tool is itself paid per call
contracts/
  fx-pay/        the Soroban contract, in Rust, with its tests
docs/
  scheme_exact_fx_stellar.md   the exact-fx spec, in the x402 spec format

Design decisions worth reading

These are the places where the obvious implementation is wrong.

Rounding is directional. quoteLocalPrice rounds up (ceilDiv). A seller asking 70 NGN must never receive 69.999 NGN worth of USDC because of integer truncation.

Quotes are median-of-five, not last-price. Reflector's CLP feed has published single 5-minute prints 0.65% away from their neighbours. ReflectorFiatOracle takes the median of the last five records, so one outlier tick cannot set a price. Stale rates (>15 min by default) are rejected outright. When the feed skipped periods, the history is cut at the gap first (recentRun): those five records would otherwise span hours while still reporting the newest timestamp, and the median would smooth in prices that are no longer relevant. A price backed by a single record says so, with a #single suffix on its source — that is one raw print, exactly the tick the median exists to absorb.

Quotes are HMAC-signed and frozen. x402 rebuilds the payment requirements when the paid retry arrives — and the oracle may have moved in between, which would make the amount the payer signed no longer match. localPrice signs the quote it issued; any server instance holding the same secret honours it while it is valid. This is what makes the flow correct behind a load balancer, not just on one process.

The client re-derives the price instead of trusting it. Local402Client quotes the same local price from its own oracle and refuses a quote more than 2% above it (maxOverchargeBps), and refuses an FX payment whose max_send is more than 5% above the oracle value of the price (maxFxPremiumBps). The FxPay contract address is pinned client-side and never taken from the server's response — a malicious contract could spend up to max_send.

Be precise about what that buys: by default both sides read the same Reflector feed, so this catches a seller who misquotes, not a feed that is wrong. Pass your own oracle to Local402Client for an independent reading. The UF adds a second shared dependency — the SII, or its two mirrors — and a stale UF is why a quote also carries oracleValueDate: the day the value is actually from, which the CLP timestamp cannot tell you.

The agent's budget is reserved, not checked. SpendingBudget is debited before the request and credited back on failure, so two concurrent payments cannot both pass a check-then-spend test and together overrun the limit.

The facilitator has eleven verification rules, and two are redundant on purpose. The authorization tree (rule 7) already bounds what the payer's signature permits. Rules 9 and 10 re-check the simulated events — that the seller was paid, and that no facilitator account was drained — as defence in depth. Since the facilitator pays the fees, a public deployment can also require an allowlisted seller and a minimum amount, and rate-limits verify/settle separately because they cost different things.

Fee bidding, and a bug we fixed upstream. @x402/stellar 2.25 always bids the 100-stroop base fee, which mainnet frequently rejects under Soroban surge pricing. feeBumpSigner re-wraps the fee bump with a bid of twice the network's recent p99, capped — without touching the inner transaction or its signatures. We reported it as x402#3491 and fixed it in x402#3503, merged into x402 main: the exact facilitator takes an inclusionFeeStroops option, released in @x402/stellar 2.27.0. Local402 still pins 2.25 and keeps the workaround until it moves to a release that has it.

Parallel settlement needs separate accounts. Two settlements from one Stellar account collide on the sequence number. ChannelPool hands each concurrent settlement its own fee-paying account, and queues the rest until one is free. It is an in-process lock, so it only separates settlements running in the same facilitator instance: two instances sharing one key still collide. That is why /settle retries once on a submission_failed that never reached the ledger, and why a horizontally scaled facilitator needs a disjoint FACILITATOR_PRIVATE_KEYS per instance.

The UF is a date, not a rate. It is defined per calendar day in Chile's timezone. UfRateSource reads the SII (the tax service), falls back to two other sources, times out at 4 s and keeps the last known value for up to three days — because an API that returns 402 with no price is worse than one priced on yesterday's UF.

A paid resource is never cached. Both the client probe and the API send no-store. A cached 200 would hand out a paid response for free; a cached 402 would hand out an expired quote.

When something goes wrong

Each of these fails before any money moves, or reverts the whole transaction. The facilitator codes below drop their invalid_exact_fx_payload_ prefix.

Situation What happens
The oracle rate is more than 15 minutes old, dated in the future, or zero The server refuses to quote. No requirement is issued, so nobody pays on a bad price.
The seller quotes more than 2% above the payer's own oracle Local402Client refuses before signing.
The swap could spend more than 5% above the oracle value The client refuses before signing (maxFxPremiumBps).
The payer does not hold max_send The client says how much has to be available and how much is there, before the wallet opens.
The pools moved and the route now needs more than max_send FxPay reverts with ExcessiveInput. Nothing moves.
No pool can deliver the seller's asset FxPay fails with NoRoute, a readable error rather than a trap.
The amount, recipient or asset differs from the 402 The facilitator rejects it: wrong_amount, wrong_recipient, wrong_asset.
The signature authorizes anything beyond pay and one transfer wrong_auth_root, wrong_auth_subinvocation or unexpected_auth_entries.
The transaction would move the facilitator's own funds moves_facilitator_funds, checked against the simulated events.
The deadline passes while the payer approves in the wallet deadline_expired. The client already allows 180 s of signing time.
Two settlements race for one account ChannelPool gives each its own. A submission that never reached the ledger is retried once.
All three UF sources are down The last known UF is used for up to three days. After that only the UF route stops quoting; the others keep working.

Deploying it somewhere real

npm run demo needs nothing. A deployment that more than one process serves, or that spends real money, needs these — each .env.example lists the rest.

Variable Where Why it matters
LOCAL402_QUOTE_SECRET seller Signs quotes. Without it, a paid retry that lands on another instance re-quotes and rejects a payment the payer already signed. Same value on every instance, at least 32 characters (openssl rand -hex 32). Since 1.0 the seller refuses to start without it; quoteSecret: false opts a single-instance seller out. npm run demo generates one per run.
KV_REST_API_URL, KV_REST_API_TOKEN seller Receipts, the demo payment caps and the last known UF value. Without them each serverless instance keeps its own, and they disappear when it does.
SELF_URL seller Where the demo agent buys from. Falls back to the Vercel project's production domain, and to the request's Host only when that is localhost, because Host is whatever the caller wrote.
CORS_ORIGIN seller Dashboard origins allowed to pay from the visitor's wallet, comma-separated.
PAY_TO_ALLOWLIST, MIN_AMOUNT facilitator Every settlement spends the facilitator's own fees. On stellar:pubnet it refuses to start without a seller allowlist (the demo deployment falls back to its own PAY_TO), and the minimum defaults to 100000 (0.01 USDC).
FACILITATOR_PRIVATE_KEYS facilitator One key per concurrent settlement. ChannelPool separates them within an instance only, so give each instance its own keys.
VERIFY_RATE_LIMIT, SETTLE_RATE_LIMIT, TRUST_PROXY facilitator Requests per client per minute (60 and 20), and how many proxies' X-Forwarded-For to believe when counting clients (0; 1 on the demo deployment, behind Vercel).
FX_CONTRACT both Required on mainnet: the FxPay deployment the payer's authorization is pinned to.

To run a facilitator of your own, for your sellers or someone else's, npx local402-facilitator is the whole install: docs/facilitator.md has every variable and the mainnet configuration, and the runbook covers keys, funding, top-ups and rotation.

Testing

npm test        # 140 tests across the packages (4 more need testnet, see below)
npm run typecheck
cd contracts && cargo test    # 17 contract tests against a mock Soroswap router

The contract tests cover the cases that matter: exact delivery with a refund of the unused input, quote agreeing with what pay actually spends, rejection when the route needs more than max_send, routing through the hub asset when there is no direct pool, picking the cheaper of two routes, and refusal without the payer's authorization. Two cover a router that answers with no amounts at all: the hub route still wins if it exists, and otherwise the payer gets NoRoute rather than a trap. The last one is a property test: 200 deterministic combinations of pools, amounts and limits, checking every time that the seller gets exactly dest_amount, the payer spends exactly the cheaper route, FxPay keeps nothing, and a max_send below the route moves nothing at all.

Nine TypeScript tests drive the facilitator's verification offline, over every rejection it reaches before it touches the network: a payload for another version, scheme or network, a transaction it cannot read, a call that is not FxPay's pay, a source or payer that is the facilitator itself, a send asset it does not accept, an asset, amount or recipient that is not what the seller asked for, and a deadline that has expired or reaches too far.

The tests cover 82.93% of the statements in what ships to npm; the counts and coverage before and after the October sprint are in the sprint plan.

Four further tests build a real exact-fx payload against testnet and run it through the facilitator's verification, including the rejections. They only run when FX_PAYER_SECRET is set, so a clean checkout needs no funded account to go green.

Reproducible builds

The contract on mainnet is not a local build someone uploaded. Its wasm is byte for byte a GitHub release artifact, compiled in CI by stellar-expert's reusable workflow. Check it without trusting this README:

curl -fsSL -O https://github.com/DiegoPoveda01/local402/releases/download/v0.1.1_contracts_fx-pay_cli27.0.0/fx-pay_v0.1.1.wasm
sha256sum fx-pay_v0.1.1.wasm
curl -s https://api.stellar.expert/explorer/public/contract/CA6Z4E55YN6LZEXBQPFCWUIMUJGAV42RLHYWWZ6SNIP4E73R2I2PUGHD | jq -r .wasm

Both print 69a12d89059be04ac195f5acf40982d7e9ee93fd08f4df33626789d00ffccbd2. The build is reproducible in the strict sense: two runs, from different commits and with the package version changed between them, produced identical artifacts. Each carries a SLSA provenance attestation signed through Sigstore and logged in Rekor. scripts/mainnet/deploy-fxpay.sh deploys the downloaded release asset and aborts if its hash is not the one above, so a local build cannot reach mainnet by accident.

stellar.expert lists FxPay as verified, matched to commit 268f32c (tag v0.1.1) of this repository. It did not for several weeks: its intake was dropping valid submissions until the bug was found and fixed on their side (soroban-build-workflow#9). Stellar Lab reads the SEP-55 attestation directly, and its contract explorer shows FxPay as Verified Build, linked to this repository.

What it does not do

  • It does not make the oracle right. By default the seller and the payer read the same Reflector feed, so Local402 catches a seller who misquotes, not a feed that is wrong. Pass your own oracle to Local402Client for an independent reading.
  • It does not hedge. The seller receives USDC. Turning it into their local currency is still their job: Local402 fixes the amount of the sale, not the exchange rate after it.
  • It does not search every venue. FxPay routes on Soroswap only, through the direct pool or through XLM. The SDEX quote on the dashboard is there for comparison and is never used to pay.
  • It does not hold funds. Input sits in FxPay only inside the transaction that swaps it. The contract keeps no balance between payments and has no admin or upgrade function.
  • It is not audited. The contract has 17 tests and has settled real payments, but no external audit — an internal review is in docs/security-review.md, and who could attack a payment, and what stops them, is in docs/threat-model.md. The wasm that runs on mainnet was also deployed to testnet as CD6PUDBJ… and scanned with kuyfi, an automated black-box fuzzer for Soroban: 37 vectors over the five entry points, zero findings. Every vector was rejected in simulation, so nothing reached the ledger — a surface scan, not an audit. That is why the mainnet facilitator only settles for the demo seller and for at least 0.01 USDC.
  • exact-fx is not part of x402 yet. It is a draft. A stock x402 client pays the exact option of the same 402, not the FX one.

Who trusts whom

Party Relies on Protected by
Seller The oracle to price fairly, and the facilitator to submit. The signed authorization fixes the amount, asset and recipient, and FxPay reverts unless it delivers that exact amount.
Payer The seller's quote, within 2% of its own oracle, and an FxPay address it pinned itself. It signs one bounded transfer (max_send) with a deadline, and the unused part comes back.
Facilitator Nothing the payer says. Eleven verification rules on the simulation, a refusal to act as the payer, and on mainnet an allowlisted seller and a minimum amount.

Status

Running on Stellar mainnet with real payments: XLM and EURC through the FxPay deployment linked above, and USDC directly through x402 exact. Every sale leaves a receipt recording the local price, the rate and its source, the swap premium in basis points, and the transaction hash. GET /receipts.csv hands it to accounting.

The integration guide for sellers shipped in October. Someone outside the project followed it from scratch on 2026-10-31, and every step worked as written (#3).

Recognition from Tellus Cooperative

Roadmap

When What Evidence that it is done
October 2026 sprint (done) 1.0 packages, a facilitator anyone can deploy, the integration guide, wider tests, and kuyfi on 1.0 The acceptance criteria in docs/sprint-plan.md
When kuyfi is ready kuyfi live on mainnet. The sprint already adds its payee to the allowlist, so this is kuyfi's call (kuyfi#2) Mainnet tx hashes for a paid kuyfi scan
Next An external audit of FxPay, which the Instaward does not fund. The sprint leaves the code audit-ready: security-review.md for the contract, threat-model.md for the whole payment path A published audit report, with every finding fixed or answered
After the audit Open the mainnet facilitator to any seller: drop the payee allowlist and revisit the minimum amount A mainnet payment settled for a seller nobody added by hand
Once the facilitator is open 5 external sellers settling on mainnet, kuyfi first Counted from FxPay's FxPaid events on mainnet
Ongoing exact-fx proposed upstream to x402 The open proposal; when it merges is up to x402

exact-fx is a draft scheme. It is written in the x402 spec format so it can be discussed as a proposal, and it could equally be expressed as an assetTransferMethod of exact; what does not change either way is the seller-facing guarantee and the facilitator's verification rules.

License

MIT — see LICENSE.

About

x402 payments priced in local currency on Stellar: 24 currencies + UF, FxPay contract on mainnet

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages