Docs

Run it yourself

Node 20 or newer. Nothing else - no Rust, no local node. The probe and the fork test need no keys and spend nothing.

Quickstart

git clone https://github.com/Ritapossible/Scrip.git
cd Scrip
npm install

npm run probe     # read-only, no keys, no gas
npm run web       # http://127.0.0.1:8080

Use npm run web rather than a plain static server. These pages link to ./docs without an extension, which production resolves through Vercel's cleanUrls. A plain server returns 404 for it, so every menu entry looks broken when the site is fine.

The commands

CommandNeedsWhat it does
npm run probe nothing Resolves FXRP from the registry and verifies the EIP-712 domain reconstructs.
npm run test:fork anvil, forge build Settles a real invoice against a fork of live Coston2, including the attacks.
npm run price nothing Reads the live XRP/USD FTSO feed and prices a USD invoice in FXRP.
npm run setup RELAYER_PK Generates a fresh payer that has never held gas, and writes .env.
npm run fund a funded relayer Moves FXRP to the payer. Refuses if the payer holds any C2FLR.
npm run settle a funded .env Signs and settles one invoice gaslessly against the deployed facilitator.
npm run serve:x402 a funded .env Runs the facilitator (/verify, /settle) and a $0.25 paid endpoint.
npm run agent a funded payer Hits the paid endpoint, gets a 402, pays it, gets the resource.
npm run typecheck nothing Typechecks every script and module.
npm run web nothing Serves this site locally with production URL rules.

The probe

Read-only, and it fails loudly if signatures would not verify. It reads rather than infers, because FXRP ships a vendored ERC20Permit rather than stock OpenZeppelin - anything taken from upstream documentation is a guess.

Three results worth repeating from the live run:

The fork test

Settles a real invoice against the real FXRP contract on a fork of live Coston2, and runs the payee-substitution, replay and front-runner cases as attacks. No keys, no funds, nothing on a real chain.

anvil --fork-url https://coston2-api.flare.network/ext/C/rpc --port 8546
forge build
npm run test:fork

What it proves, and how the payer is funded on a fork, is on the status page.

A gasless payment

Copy .env.example to .env and fill in three throwaway wallets. Generate them fresh - never reuse a key that has ever touched mainnet.

PAYER

Holds FXRP and deliberately zero C2FLR. That zero is the proof: it is what makes the payment gasless from the agent's side.

RELAYER

Holds C2FLR only. It pays every gas fee.

PAYEE

Just receives. It needs nothing at all.

Fund one wallet from the Coston2 faucet, which dispenses both C2FLR and FXRP, then let the setup script do the rest:

RELAYER_PK=0x<that wallet's key> npm run setup
npm run fund -- 2
Why the payer is generated, not reused.

settle.ts asserts the payer's native balance is exactly zero, and no wallet can be drained to exactly zero - the drain transaction burns gas and leaves dust behind. Only an account that has never received C2FLR satisfies that equality, so the payer is created fresh and funded with FXRP alone.

npm run settle           # pays 0.5 FXRP
npm run settle -- 1.25   # pays 1.25 FXRP

It resolves FXRP through the registry, checks its locally built EIP-712 digest against the facilitator's own intentDigest() before signing anything, and refuses to run if the payer is short, the relayer has no gas, or the invoice was already settled. A revert comes back as a sentence rather than a stack trace.

Charging for a route

The middleware turns any Express route into a paid one. The price is in dollars; the FXRP amount is computed at the live FTSO rate when the quote is issued.

app.get(
  "/api/haiku",
  requirePayment({ facilitator, payee, usd: "0.25" }),
  (req, res) => res.json({ haiku: HAIKU }),
);

An unpaid request gets a 402 carrying the price, the amount, the rate it was derived from, and the invoice ID to sign. A request with a valid X-PAYMENT header is settled on chain and then handed to the route as though it had been free, with the receipt in X-PAYMENT-RESPONSE.

The scheme is exact-permit2612.

Every x402 implementation in the wild is written against USDC, whose EIP-3009 authorisation names its recipient inside the signed message. FXRP implements EIP-2612 permit, which does not - so this rail needs a second signature and says so in the scheme name. A USDC-shaped client finds out from the name rather than from a revert.

Environment

VariablePurpose
RPC_URLCoston2 endpoint. Defaults to the public one.
PAYER_PKSigns both messages. Holds FXRP, no gas token.
RELAYER_PKSends the transaction and pays for it.
PAYEE_ADDRESSReceives the FXRP.
FACILITATOR_ADDRESSThe deployed contract. Already filled in.
DEADLINE_SECONDSSignature lifetime, default 3600.
INVOICE_IDOptional fixed invoice ID. Random per run otherwise.
PORTPort for the x402 service, default 8402.
SCRIP_INVOICE_SECRETSigns invoice IDs. Without it, quotes do not survive a restart.

Deploying your own

The facilitator is already deployed - see the contract page. To put up your own, keep the key in an encrypted keystore rather than a shell variable:

cast wallet import scrip-deployer --interactive

forge create contracts/ScripFacilitator.sol:ScripFacilitator \
  --rpc-url coston2 \
  --account scrip-deployer \
  --broadcast \
  --constructor-args <FXRP address>

--constructor-args is variadic and will silently eat any flag placed after it, so keep it last. And if verification fails, do not re-run forge create - that deploys a second contract and leaves you unsure which is canonical. Retry with forge verify-contract against the address you already have.

Two environment notes

The public Coston2 RPC times out on a cold call well past viem's 10 second default, so the transport here is set to 45 seconds with retries. If a call seems to hang, it is usually warming rather than down.

Windows PowerShell 5.1 negotiates TLS 1.0 by default, which Flare's RPC rejects. Force TLS 1.2 before using Invoke-WebRequest against it. Node's fetch is unaffected.