Run the profile facilitator
Start the x402 facilitator for the Tron and Polkadot LCP profiles, configure its networks and store, and call it.
@integraledger/profile-facilitator is an x402 facilitator for
x402/exact/tron/lcp-trc20-memo and x402/exact/polkadot/lcp-assets-remark. It verifies and settles payments whose
payer-signed transaction carries the ATR hash (H), holds no key and pays no fee. This guide runs it.
Install
npm install @integraledger/profile-facilitatorIt needs Node.js >=26.10.0, Postgres (the tests run on Postgres 18), and an HTTP endpoint for each network you
serve: a Tron FullNode and Solidity node, or a Polkadot Asset Hub RPC node.
Start it and read what it supports
Start the facilitator for Tron mainnet and read what it supports. The node URLs below are java-tron's default HTTP
ports on your own node; the facilitator calls them only when a payment arrives. DATABASE_URL is your Postgres
connection string.
import { } from "@integraledger/profile-facilitator";
const = await ({
: "127.0.0.1:4020",
: [{ : "tron:728126428", : "http://127.0.0.1:8090", : "http://127.0.0.1:8091" }],
: [{ : "polkadot:68d56f15f85d3136970ec16946040bc1", : "http://127.0.0.1:9944" }],
: { : .. ?? "postgres://postgres@127.0.0.1:5432/postgres" },
: 30_000,
});
const = await (await ("http://127.0.0.1:4020/supported")).();
.(.(, null, 2));
await .();{
"kinds": [
{
"x402Version": 2,
"scheme": "exact",
"network": "tron:728126428",
"extra": {
"assetTransferMethod": "lcp-trc20-memo"
}
},
{
"x402Version": 2,
"scheme": "exact",
"network": "polkadot:68d56f15f85d3136970ec16946040bc1",
"extra": {
"assetTransferMethod": "lcp-assets-remark"
}
}
],
"extensions": [],
"signers": {}
}Run it as a service
The package exports one function that starts the server; it has no command-line entry. A service is a short module that reads its configuration and stops cleanly on a signal:
import { } from "@integraledger/profile-facilitator";
const = .. ?? "127.0.0.1:4021";
const = await ({
,
: [
{
: "tron:728126428",
: .. ?? "http://127.0.0.1:8090",
: .. ?? "http://127.0.0.1:8091",
},
],
: { : .. ?? "postgres://postgres@127.0.0.1:5432/postgres" },
: (.. ?? 30_000),
});
for (const of ["SIGINT", "SIGTERM"] as ) {
.(, () => {
void .().(() => .(0));
});
}
.(`facilitator listening on ${}`);facilitator listening on 127.0.0.1:4021Run it with node facilitator.ts: Node.js 26 runs TypeScript directly. close() stops accepting requests, closes
open connections, disconnects from the Polkadot nodes and closes the Postgres pool.
Configuration
serveProfileFacilitator(config) takes a ProfileFacilitatorConfig:
| Field | Type | Meaning |
|---|---|---|
listen | string | "host:port"; an IPv6 host may be written in brackets. |
tron | { network, fullNode, solidityNode }[], optional | One entry per Tron network. network is tron:<chain id in decimal>. fullNode serves /wallet/… and solidityNode serves /walletsolidity/…; give each its base URL. |
polkadot | { network, rpc }[], optional | One entry per network, polkadot:68d56f15f85d3136970ec16946040bc1 or polkadot:67f9723393ef76214df0118c34bbbd3d. rpc is an HTTP JSON-RPC endpoint that serves state_call and author_submitExtrinsic. |
store | { url } | The Postgres connection string. Postgres holds the deduplication table only. |
settleWaitMs | number | How long /settle waits for inclusion before it answers settlement_pending. A value that is not a positive finite number means 30 000. |
The facilitator serves only the networks it is configured with. A request for any other network is answered
invalid_network. At start it creates its table if it does not exist; a Polkadot node's runtime metadata is loaded on
the first payment for that network, and loaded again whenever the node's runtime version changes.
Call it
Both POST /verify and POST /settle take the x402 facilitator request:
{x402Version: 2, paymentPayload, paymentRequirements}, with paymentPayload.x402Version 2 and
paymentRequirements.scheme "exact".
import { } from "@integraledger/profile-facilitator";
const = await ({
: "127.0.0.1:4022",
: [{ : "tron:728126428", : "http://127.0.0.1:8090", : "http://127.0.0.1:8091" }],
: { : .. ?? "postgres://postgres@127.0.0.1:5432/postgres" },
: 30_000,
});
const = {
: 2,
: { : 2, : { : "0a02" } },
: {
: "exact",
: "tron:3448148188",
: "10000",
: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
: "TCwX1UeSkVfu43HD5xNMmFqDR2Xgjdxihx",
: 60,
: { : "lcp-trc20-memo" },
},
};
for (const of ["/verify", "/settle"]) {
const = await (`http://127.0.0.1:4022${}`, {
: "POST",
: { "content-type": "application/json" },
: .(),
});
.(, ., .(await .()));
}
await .();/verify 200 {"isValid":false,"invalidReason":"invalid_network"}
/settle 200 {"success":false,"errorReason":"invalid_network","transaction":"","network":"tron:3448148188"}The rail specifics are in the Tron and Polkadot guides, and every answer and reason in the facilitator reference.
Pending and repeated settles
When the wait ends first, /settle answers {success: false, errorReason: "settlement_pending", transaction} with
the transaction id or extrinsic hash; x402 requires that transaction not be empty. Call /settle again with the
same request: it reads the chain from where it stopped, never submits twice, and returns the final answer once there
is one. An answer with an empty transaction means nothing was submitted. When the store cannot be read, the
facilitator cannot know whether the payment was submitted, so it answers settlement_pending with the id.
Security
- No key, no fee. The facilitator signs nothing. It submits exactly the bytes the payer signed, and the payer pays every fee, as both profiles require.
- Settled once. Settlements are deduplicated by transaction id or extrinsic hash, atomically, until the validity window ends and for 24 hours after.
- Success only from the chain.
success: trueis given only when a node shows the transaction in a block with the profile's success conditions. - What it does not check. It checks that the payment carries an LCP
sha256string, not which H the seller issued. The resource server refuses a payment whose H it did not issue for that request (each profile's rule 6), for example through the seller door'sclaim. It checks the amount, asset and payee against the requirements it is given because x402 requires that of a facilitator, and carries no business or legal logic beyond that.
Last updated on