-
Notifications
You must be signed in to change notification settings - Fork 286
Add x402 quickstart guide #2306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "label": "x402 on Stellar", | ||
| "position": 74, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these two are already defined in the frontmatter of the |
||
| "collapsed": false | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| --- | ||
| title: x402 Quickstart Guide | ||
| description: Build a paid x402 API with Node.js, Express, and Stellar settlement. | ||
| sidebar_position: 10 | ||
| --- | ||
|
|
||
| This tutorial shows how to build the simplest possible paid API with Node.js and Express using the x402 packages with settlement on the Stellar network. | ||
|
|
||
| To follow this guide, you will need [Node.js](https://nodejs.org/) installed locally. Node.js 18 or newer is recommended so you can use the built-in fetch API. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick-adjacent: i know that for the purpose of the "you need |
||
|
|
||
| ## Create A Project | ||
|
|
||
| Create a new folder for the tutorial and initialize a Node.js project: | ||
|
|
||
| ```bash | ||
| mkdir x402-quickstart-guide | ||
| cd x402-quickstart-guide | ||
| npm init -y | ||
| npm pkg set type=module | ||
| ``` | ||
|
|
||
| The `type=module` setting lets you run the `import` syntax used in the example files below. | ||
|
|
||
| Install the npm packages used by the server and client examples: | ||
|
|
||
| ```bash | ||
| npm install express dotenv @stellar/stellar-sdk @x402/core @x402/express @x402/fetch @x402/stellar | ||
| ``` | ||
|
|
||
| This creates a `package.json` and installs everything needed for both `server.js` and `client.js`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doesn't |
||
|
|
||
| ## Create `server.js` | ||
|
|
||
| Create a file named `server.js` and paste in the following code: | ||
|
|
||
| ```js title="server.js" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'd love to see some more explanatory comments throughout big chunks like this. so as to explain some of the logic, as you go |
||
| import express from "express"; | ||
| import { paymentMiddlewareFromConfig } from "@x402/express"; | ||
| import { HTTPFacilitatorClient } from "@x402/core/server"; | ||
| import { ExactStellarScheme } from "@x402/stellar/exact/server"; | ||
|
|
||
| const PORT = "3001"; | ||
| const ROUTE_PATH = "/my-service"; | ||
| const PRICE = "$0.01"; | ||
| const NETWORK = "stellar:testnet"; | ||
| const FACILITATOR_URL = "https://www.x402.org/facilitator"; | ||
| const PAY_TO = "GABC123...YOURADDRESS...123ABC"; | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.get("/", (_, res) => | ||
| res.json({ route: ROUTE_PATH, price: PRICE, network: NETWORK }), | ||
| ); | ||
|
|
||
| app.use( | ||
| paymentMiddlewareFromConfig( | ||
| { | ||
| [`GET ${ROUTE_PATH}`]: { | ||
| accepts: { | ||
| scheme: "exact", | ||
| price: PRICE, | ||
| network: NETWORK, | ||
| payTo: PAY_TO, | ||
| }, | ||
| }, | ||
| }, | ||
| new HTTPFacilitatorClient({ url: FACILITATOR_URL }), | ||
| [{ network: NETWORK, server: new ExactStellarScheme() }], | ||
| ), | ||
| ); | ||
|
|
||
| app.get(ROUTE_PATH, (_, res) => res.json({ secret: "valuable content" })); | ||
|
|
||
| app.listen(Number(PORT), () => { | ||
| console.log(`x402 server listening on http://localhost:${PORT}${ROUTE_PATH}`); | ||
| }); | ||
| ``` | ||
|
|
||
| Update `PAY_TO` with the Stellar address that should receive funds whenever a client pays for your service. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you may need to include a note that the |
||
|
|
||
| `FACILITATOR_URL` points to a managed facilitator endpoint that handles verification and settlement. In this example, the facilitator is Coinbase's testnet facilitator. | ||
|
|
||
| Start the API locally: | ||
|
|
||
| ```bash | ||
| node server.js | ||
| ``` | ||
|
|
||
| When a client requests your endpoint, your server responds with a `402 Payment Required` status. The response includes headers that describe the payment requirements so that a compliant client can build a signed payment payload and retry the request. | ||
|
|
||
| Behind the scenes, the facilitator handles verification and onchain settlement. You do not need to run your own blockchain nodes or build raw transactions yourself. A managed facilitator service takes the signed payment payload from the client, verifies it against the specified network and asset, settles the transaction, and returns verification to your server so it can complete the request. | ||
|
|
||
| The client generates a signed payment payload that authorizes a stablecoin transfer, the facilitator verifies and settles that payload on the Stellar network, and the server returns a `200` response with the protected data. | ||
|
|
||
| The funds settle directly to the wallet address in `PAY_TO`. | ||
|
|
||
| ## Create `client.js` | ||
|
|
||
| With your Express server running, create a file named `client.js` and paste in the following code: | ||
|
|
||
| ```js title="client.js" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same note as above, could use some explanatory comments throughout this chunk |
||
| import dotenv from "dotenv"; | ||
| import { Transaction, TransactionBuilder } from "@stellar/stellar-sdk"; | ||
| import { x402Client, x402HTTPClient } from "@x402/fetch"; | ||
| import { createEd25519Signer, getNetworkPassphrase } from "@x402/stellar"; | ||
| import { ExactStellarScheme } from "@x402/stellar/exact/client"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| dotenv.config({ | ||
| path: fileURLToPath(new URL("./.env", import.meta.url)), | ||
| quiet: true, | ||
| }); | ||
|
|
||
| const STELLAR_PRIVATE_KEY = process.env.STELLAR_PRIVATE_KEY; | ||
| const RESOURCE_SERVER_URL = "http://localhost:3001"; | ||
| const ENDPOINT_PATH = "/my-service"; | ||
| const NETWORK = "stellar:testnet"; | ||
| const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; | ||
|
|
||
| async function main() { | ||
| const url = new URL(ENDPOINT_PATH, RESOURCE_SERVER_URL).toString(); | ||
| const signer = createEd25519Signer(STELLAR_PRIVATE_KEY, NETWORK); | ||
| const rpcConfig = STELLAR_RPC_URL ? { url: STELLAR_RPC_URL } : undefined; | ||
| const client = new x402Client().register( | ||
| "stellar:*", | ||
| new ExactStellarScheme(signer, rpcConfig), | ||
| ); | ||
| const httpClient = new x402HTTPClient(client); | ||
|
|
||
| console.log(`Target: ${url}\nClient address: ${signer.address}`); | ||
|
|
||
| const firstTry = await fetch(url, { method: "GET" }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think |
||
| console.log(`Payment requested: ${firstTry.status}`); | ||
|
|
||
| const paymentRequired = httpClient.getPaymentRequiredResponse((name) => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. especially here, we could use an explanation of what the |
||
| firstTry.headers.get(name), | ||
| ); | ||
|
|
||
| let paymentPayload = await client.createPaymentPayload(paymentRequired); | ||
| const networkPassphrase = getNetworkPassphrase(NETWORK); | ||
| const tx = new Transaction( | ||
| paymentPayload.payload.transaction, | ||
| networkPassphrase, | ||
| ); | ||
| const sorobanData = tx.toEnvelope().v1()?.tx()?.ext()?.sorobanData(); | ||
|
|
||
| if (sorobanData) { | ||
| paymentPayload = { | ||
| ...paymentPayload, | ||
| payload: { | ||
| ...paymentPayload.payload, | ||
| transaction: TransactionBuilder.cloneFrom(tx, { | ||
| fee: "1", | ||
| sorobanData, | ||
| networkPassphrase, | ||
| }) | ||
| .build() | ||
| .toXDR(), | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| const paymentHeaders = | ||
| httpClient.encodePaymentSignatureHeader(paymentPayload); | ||
| const paidResponse = await fetch(url, { | ||
| method: "GET", | ||
| headers: paymentHeaders, | ||
| }); | ||
| const text = await paidResponse.text(); | ||
| const paymentResponse = httpClient.getPaymentSettleResponse((name) => | ||
| paidResponse.headers.get(name), | ||
| ); | ||
|
|
||
| console.log("Settlement response:", paymentResponse); | ||
| console.log(`Access Granted! ${paidResponse.status} "${text}"`); | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error("Client failed:", error); | ||
| process.exit(1); | ||
| }); | ||
| ``` | ||
|
|
||
| Next, create a fresh account and fund it with testnet XLM and testnet USDC using Stellar Lab: | ||
|
|
||
| - [Fund a testnet account in Stellar Lab](https://lab.stellar.org/account/fund) | ||
|
Comment on lines
+194
to
+196
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you might need just a little bit more detail on how this process works. specifically about the trustline that's needed. i think the process goes like
would be nice to include some of those details. |
||
|
|
||
| Create a local environment file for the client and add your Stellar testnet secret key: | ||
|
|
||
| ```bash title=".env" | ||
| STELLAR_PRIVATE_KEY=S... | ||
| ``` | ||
|
|
||
| Edit `.env` and replace `S...` with the secret key for the account that will sign transactions and pay for the resource. | ||
|
|
||
| :::caution | ||
|
|
||
| Secret keys provide full access to any digital assets held within the wallet. Use `.env` files only for hot wallets in testnet deployments. | ||
|
|
||
| ::: | ||
|
|
||
| ## Run The Client | ||
|
|
||
| Once the account is funded and the secret key is in `.env`, run the client in a second terminal: | ||
|
|
||
| ```bash | ||
| node client.js | ||
| ``` | ||
|
|
||
| This sends `$0.01` of testnet USDC from the client account to the server account. | ||
|
|
||
| The client then receives the protected JSON data: ```{ secret: "valuable content" }``` | ||
|
|
||
|
|
||
| The server is set up as an MVP for machine-to-machine payments. If you want to enable human payments, the next step is to build a paywall. Take a look at the demo code for examples: https://github.com/stellar/x402-stellar/tree/main/examples/simple-paywall | ||
|
|
||
| ## Additional Documentation | ||
|
|
||
| - [OpenZeppelin x402 Facilitator Plugin Source Code](https://github.com/OpenZeppelin/relayer-plugin-x402-facilitator) | ||
| - [OpenZeppelin x402 Facilitator Docs](https://docs.openzeppelin.com/relayer/1.4.x/guides/stellar-x402-facilitator-guide) | ||
| - [OpenZeppelin Stellar Relayer SDK](https://github.com/OpenZeppelin/openzeppelin-relayer-sdk) | ||
| - [OpenZeppelin Stellar Relayer Docs](https://docs.openzeppelin.com/relayer/1.4.x) | ||
| - [OpenZeppelin Smart Account](https://docs.openzeppelin.com/stellar-contracts/accounts/smart-account) | ||
|
|
||
| ## Learn more | ||
|
|
||
| - [x402 on Stellar (Landing page)](https://stellar.org/x402) - Stellar x402 overview and resources | ||
| - [x402 on Stellar (Blog)](https://stellar.org/blog/foundation-news/x402-on-stellar) - Foundation news and announcement | ||
| - [x402 protocol (Coinbase Developer Platform)](https://docs.cdp.coinbase.com/x402) - Official x402 protocol overview and spec | ||
| - [x402 protocol specification](https://www.x402.org/) - x402 Specification and Whitepaper | ||
| - [Coinbase x402 GitHub](https://github.com/coinbase/x402) - Official x402 Protocol GitHub Repo | ||
| - [x402-stellar (npm)](https://www.npmjs.com/package/x402-stellar) - npm package for x402 on Stellar | ||
| - [Signing Soroban invocations](../../guides/transactions/signing-soroban-invocations.mdx) - Auth-entry signing and transaction signing on Stellar | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this maaaay not be necessary. it's kindof weird how docusaurus works with these. a file at
path/to/directory/README.mdxmight end up with an actual filepath ofpath/to/directory.htmlonce the site gets built... 🤔 (i might be mixing up things in my memory, though)if you've run
yarn buildsuccessfully, it should be fine as-isThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same would (or wouldn't lol) hold true for the other filepaths modified in this file