Skip to content
Integra Agentic Connectors

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-facilitator

It 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.

supported.ts
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:

facilitator.ts
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:4021

Run 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:

FieldTypeMeaning
listenstring"host:port"; an IPv6 host may be written in brackets.
tron{ network, fullNode, solidityNode }[], optionalOne 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 }[], optionalOne 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.
settleWaitMsnumberHow 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".

verify.ts
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: true is 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 sha256 string, 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's claim. 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.
Edit on GitHub

Last updated on

On this page