<!--
Sitemap:
- [What is elisym](/index)
- [How it works](/how-it-works)
- [Quickstart](/quickstart)
- [MCP server](/customers/mcp)
- [Web app](/customers/web-app)
- [File inputs & outputs](/customers/files)
- [Signing a capability's call](/customers/onchain-calls)
- [Agents as building blocks](/building-blocks/overview)
- [Use an agent as your LLM](/building-blocks/llm-inference)
- [Compose inside your agent](/building-blocks/compose)
- [Provider quickstart](/providers/quickstart)
- [Accept payments](/providers/accept-payments)
- [Skills](/providers/skills)
- [Bridge x402 services](/providers/bridge-x402)
- [On-chain calls](/providers/onchain-calls)
- [Delegated execution](/providers/delegated-execution)
- [Metered pricing](/providers/metered-pricing)
- [Policies](/providers/policies)
- [Verified identities](/providers/verified-identities)
- [Sell with Pay Core](/commerce/quickstart)
- [The widget](/commerce/widget)
- [Credit an account](/commerce/credit-an-account)
- [Protocol overview](/protocol/overview)
- [Discovery](/protocol/discovery)
- [Jobs](/protocol/jobs)
- [Messaging](/protocol/messaging)
- [Encryption](/protocol/encryption)
- [Payments](/protocol/payments)
- [Reputation](/protocol/reputation)
- [Event kinds](/protocol/event-kinds)
- [SDK installation](/sdk/installation)
- [Client & services](/sdk/client)
- [SDK payments](/sdk/payments)
- [Anatomy & categories](/agents/overview)
- [Networks](/reference/networks)
- [Constants](/reference/constants)
-->

# Sell with Pay Core

Pay Core is elisym's stablecoin checkout: it sells digital products from any web page. The buyer pays in USDC on Solana (or in a Tempo stablecoin), directly to your wallet; your backend learns of each payment from your node's signed webhook. No account is involved, on either side. You run a small node that holds your store's keys, and you add two lines of HTML to your page.

The pieces:

* **Your node** ([`@elisym/merchant-node`](https://www.npmjs.com/package/@elisym/merchant-node)) publishes your products to Nostr, takes orders, checks each payment on chain and completes the order. One node is one store, with any number of products.
* **The widget** (`<elisym-buy>`) runs on your page. It frames the checkout from `pay.elisym.network`, where the offer is verified and the buyer's wallet signs.
* **Nostr relays** carry the listings, the orders and the replies. Orders and replies are encrypted end to end between the buyer and your node, and no server of ours touches the money: the payment goes from the buyer's wallet to yours.

This page takes you from nothing to a working purchase on **mainnet**, with real money. To try it first with test tokens, see [Try it free on devnet](#try-it-free-on-devnet) at the end. [The widget](/commerce/widget) page has every attribute, the status events and the security model.

## Let your AI agent do it

A coding agent that can run commands on your machine can do every step below for you. Paste this prompt into it:

```txt
Add a Pay Core checkout to my website. Read https://docs.elisym.network/commerce/quickstart.md and follow it step by step: create my store node, publish my products, start the node and add the Buy button to my pages. Before you start, ask me for my store's name, my Solana wallet address and Solana RPC URL, my Tempo wallet address, and each product with its name, price and description. I may leave either wallet empty, but not both: add a payout only for each wallet I give, as the guide says, and ask for no Solana RPC URL without a Solana wallet. When you are done, tell me where the store home and its passphrase file are, how to start the node again after a restart, the commands to see my orders and to add a product, and how to make a small test purchase.
```

It asks for your store's name, your Solana and Tempo wallet addresses (leave out the one you do not use), a Solana RPC URL when you take Solana, and your products, then creates the store, publishes the products, starts the node and adds the Buy button. At the end it tells you where everything lives and which commands to run next. Read on to do it yourself, or to check what it did.

## 1. Create the store

You need Node.js 22.4 or newer (or Docker, see [the node's README](https://github.com/elisymlabs/elisym/tree/main/packages/merchant-node#docker)).

The store lives in its own folder (its home), and its keys are encrypted with a passphrase from the start. **Run this block in every new terminal before any `npx @elisym/merchant-node` command**, so each command works on this store:

```bash
export ELISYM_MERCHANT_HOME=~/.elisym-merchant-mainnet
export ELISYM_MERCHANT_PASSPHRASE_FILE=~/.elisym-merchant-mainnet-passphrase
```

Then create the passphrase (once; the line never overwrites an existing one) and the store:

```bash
[ -s ~/.elisym-merchant-mainnet-passphrase ] || (umask 077 && openssl rand -base64 32 > ~/.elisym-merchant-mainnet-passphrase)
npx @elisym/merchant-node init --network mainnet
```

`init` encrypts the store key and the owner key in `keys.json`, writes a `config.json` to edit, and scaffolds one product, `products/my-product/PRODUCT.md`. An existing home is kept; `init` never switches its network. A home made by merchant-node 0.7 or earlier is not upgraded: create a new home with `init`. The passphrase is never typed or printed: back up the home and the passphrase file off this machine, because without the passphrase the keys are lost. The node's README has the details, including `--owner-only` for a node that must run without it.

Open `$ELISYM_MERCHANT_HOME/config.json`, which holds what the whole store shares, and fill in:

```json
{
  "name": "My store",
  "network": "mainnet",
  "rpcUrl": "https://mainnet.helius-rpc.com/?api-key=<a server-side key>",
  "inboxRelays": ["wss://relay.elisym.network", "wss://nos.lol"],
  "payouts": [
    {
      "caip19": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "address": "<your Solana wallet address>"
    }
  ]
}
```

* `rpcUrl` is your own Solana RPC endpoint that keeps the full transaction history. The public mainnet one is rate-limited and not meant for a server, and a key restricted to a browser origin does not work from a server.
* `payouts[0].address` is the Solana wallet the money goes to, for every product. The `caip19` above is mainnet USDC.

**Only one network.** A store takes payment only on the networks it lists in `payouts`, and needs at least one. Without a Tempo wallet, leave the config as above and skip [Tempo payouts](#tempo-payouts). Without a Solana wallet, remove both the Solana payout and `rpcUrl` (a `rpcUrl` alone switches Solana payments on), and take Tempo only:

```json
{
  "name": "My store",
  "network": "mainnet",
  "tempo": { "network": "mainnet" },
  "inboxRelays": ["wss://relay.elisym.network", "wss://nos.lol"],
  "payouts": [
    {
      "caip19": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
      "address": "<your Tempo address, lowercase>"
    }
  ]
}
```

With both wallets, keep the Solana config above and add the Tempo payout with the `node -e` script in [Tempo payouts](#tempo-payouts) (only the script: you run `setup` in the next step).

Each product is a directory under `$ELISYM_MERCHANT_HOME/products`, named with the product's id: `products/my-product/PRODUCT.md`. Open it and fill in:

```markdown
---
title: Deposit 1 USD
priceUsd: "1"
---

Adds 1 USD to your account.
```

* `priceUsd` is paid 1:1 in USDC. Quote it: a bare number is refused.
* The body below the second `---` is the product's description.
* `setup` refuses the product while it is still `init`'s example (its title and text unchanged).

The store returns nothing to the buyer: once the node has checked the payment on chain, it completes the order and the checkout shows **Payment complete** with a receipt. What the buyer paid for happens on your side: your backend learns of each payment from the node's signed `order.paid` webhook ([below](#credit-an-account-on-your-backend)). A `PRODUCT.md` that still names a `delivery` (from a node older than 0.9.0) is refused: remove the key.

To sell a second product, add another directory with its own `PRODUCT.md`, for example `products/deposit-10/PRODUCT.md`. The directory name is the product's id: letters, digits, dots, dashes or underscores, at most 64.

**Real money.** Payments go straight from the buyer's wallet to these addresses: nothing holds them for you, and nothing can pull them back. Start with a small price, buy it yourself once, and only then share the page.

## 2. Publish it

```bash
npx @elisym/merchant-node setup
```

`setup` first checks that every inbox relay can do what the store needs. Then it publishes your payout list, a listing per product, the store profile, the inbox list and your authorization of the store. It prints one line per product, with its address:

```text
product my-product on sale published naddr1...
product deposit-10 on sale published naddr1...
```

After a change to `config.json` or to a product, stop the node, run `setup` again, then start the node again: `setup` refuses while the node runs. `setup` republishes only what changed (`unchanged` in its report).

**The protocol fee.** The store profile `setup` publishes declares that the node accepts payments split with the elisym protocol fee: the buyer pays the listed price, your wallet gets the price less the fee, an elisym treasury gets the fee. `setup` reads the fee config first (from the Solana RPC of the node's network: `feeConfigRpcUrl` in `config.json`, else `rpcUrl`, else the public endpoint) and declares support once it has read it; if it could not, it says so: run `setup` again once that RPC answers. Buyers refuse a store without the declaration while the fee is above 0, and `run` warns about it.

**Marked Tempo memos.** The store profile also declares (`["memo-marker", "1"]`) that the node accepts a Tempo payment under either form of the order's memo, and `setup` prints `memo    marked Tempo memos declared`. A Tempo buyer then pays under the marked form, which elisym's public statistics count; `run` and `check` warn when the profile the relays serve lacks the declaration (run `setup` again). A marked memo makes your Tempo payout and its sales volume publicly countable, as Solana payments already are through the protocol tag; no order data is revealed. See [Commerce payment markers](/protocol/payments#commerce-payment-markers).

**Upgrading the node.** After upgrading from 0.9.1 or earlier, stop the node, run `setup` once, then `run`. The upgrade converts the home's ledger, and there is no way back: older versions refuse the converted home. Open every copy of the home, a backup taken before the upgrade included, only with an upgraded version. The same holds for 0.11: the first 0.11 command that saves the ledger (`setup`, `run`, `webhook retry` / `resend`, `complete`, `refund`) converts it, and 0.10 and earlier refuse it from then on, so after upgrading stop the node, run `setup` once (it declares marked Tempo memos), then `run`.

**Stopping a product.** Add `onSale: false` to its `PRODUCT.md` and run `setup`: its listing is republished as sold out, and the checkout shows Sold out. An order a buyer already placed is still honored within the usual window. Never delete the directory of a product that was published: the node refuses to start without it, because its orders could be paid and never completed. A product id cannot change either: a new id is a new product, and the old one stays (stop it with `onSale: false`).

## 3. Run the node

```bash
npx @elisym/merchant-node run
```

Keep it running. It reads orders from your inbox relays, checks each payment on chain and completes the order. If it stops, it catches up on what it missed when it starts again. The node refuses to start if the published store offers buyers anything it would not honor, such as a payout on another network or a price it did not record. In another terminal (after the export block), `npx @elisym/merchant-node orders` lists the orders, paid or not.

## 4. Add the checkout to your page

```html
<elisym-buy product="naddr1..." network="mainnet" theme="dark"></elisym-buy>
<script
  src="https://pay.elisym.network/v3/embed.js"
  integrity="sha384-FPsJfAuhwQU0mlPKTKLwCTp1YGK6aFRh0pT+Gl1T3UML2jlIK7EkW8FW5ue2qFJX"
  crossorigin="anonymous"
></script>
```

The page shows a Buy button that opens the checkout in a modal. For several products, put one `<elisym-buy>` per product, each with its own `naddr`; the script is included once. Add `display="inline"` to show it in the page instead, `theme="light"` for a light page, and `label="..."` to change the button's text (see [the widget](/commerce/widget)). A React app can use [`@elisym/react`](https://www.npmjs.com/package/@elisym/react) instead. The `integrity` hash pins the exact loader your page runs. The file under `/v3/` never changes, so the hash stays valid. A new loader would ship under a new path.

No page yet? The repository has a ready one: [`examples/demo-store`](https://github.com/elisymlabs/elisym/tree/main/examples/demo-store) serves a store page on localhost that takes the `naddr` and the network from its URL, and logs the checkout's status events.

## 5. Buy it

Serve the page over http(s), for example with `npx serve .` on localhost. A page opened from disk (`file://`) has no origin, and the checkout refuses it. Open it in a browser with a Solana wallet that supports Wallet Standard (Phantom, Solflare, ...) holding the price in USDC and a little SOL for the fee. To pay on Tempo (the only choice on a Tempo-only store), use MetaMask holding a little more USDC.e on Tempo than the price.

Click Buy: the checkout opens and shows the product, the price and who is selling it. Pay, and within seconds the node logs the order and the payment, and the checkout shows Payment complete with a receipt.

## See your orders

The node serves an admin page on your machine. Print the store key, then start the page and paste the key into it:

```bash
npx @elisym/merchant-node store-key --yes
npx @elisym/merchant-node admin
```

Open [http://127.0.0.1:5199/](http://127.0.0.1:5199/). The page reads your inbox relays with the store key and lists each order: its product, the total the buyer claims, the email, whether it was paid and completed, and the transaction, with totals per coin, net of the elisym fee. A product list shows each ordered product, on sale or sold out. The key stays in the tab: it is never stored or sent. With encrypted keys, `store-key` reads the passphrase like `run` does (`ELISYM_MERCHANT_PASSPHRASE` or `ELISYM_MERCHANT_PASSPHRASE_FILE`). `--yes` lets it print without a terminal; leave it out to have it refuse when its output would land in a log. Run `admin` with `npx`, not in Docker.

The same view is hosted at [https://admin.elisym.network](https://admin.elisym.network): paste the store key there instead of running `admin`. It also lists every product of the store, the store's inbox relays and payout networks, and exports the loaded orders as CSV. The key stays in that tab's memory, but the page's code is served by Elisym on each visit, while the local page runs the version you installed. Browser extensions can read what you type into any page: use a browser profile without extensions you do not trust, or the local page.

## Level A: let your domain vouch for the store

Without more setup the store is **level C**: no domain vouches for it, and the checkout shows no trust chip. To reach **level A** ("Verified: your-domain" in the checkout):

1. Add `"nip05": "_@your-domain.com"` to `config.json`, stop the node, run `setup` again and start the node.
2. Serve the `nostr.json` that `setup` writes in your store's home (`$ELISYM_MERCHANT_HOME`) at `https://your-domain.com/.well-known/nostr.json`, with the header `Access-Control-Allow-Origin: *`.
3. Run `npx @elisym/merchant-node check`. It fetches the file and says whether your domain now vouches for the store.

At level A, the checkout sells only on your domain: a page served over `https:` from `your-domain.com` or one of its subdomains. It refuses any other page, including `localhost`, so test a level A store on your real site (see [the widget](/commerce/widget#where-it-sells)).

## Tempo payouts

The store can also take a Tempo stablecoin, paid with an EIP-6963 wallet such as MetaMask. The node verifies Tempo payments itself, so it needs a `tempo` block naming the Tempo network that matches the node's `network`: `mainnet` on mainnet, `moderato` (Tempo's testnet) on devnet. A Tempo payout is refused without it, and so is a Tempo network that does not match.

Upgrade the node **before** your payout list names a Tempo address: an older node would refuse to start on an offer it cannot honor, and a buyer could pay it. Once the node has seen a Tempo order, its ledger is in a newer format, and an older node refuses to start on it: do not downgrade.

To add a Tempo USDC.e payout to the mainnet store (stop the node first, run the export block, and put your own Tempo address, lowercase):

```bash
node -e '
const fs = require("fs");
const home = process.env.ELISYM_MERCHANT_HOME;
if (!home) throw new Error("run the export block first");
const path = `${home}/config.json`;
const config = JSON.parse(fs.readFileSync(path, "utf8"));
config.tempo = { network: "mainnet" };
config.payouts.push({
  caip19: "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
  address: "<your Tempo address, lowercase>",
});
fs.writeFileSync(path, JSON.stringify(config, null, 2));
'
npx @elisym/merchant-node setup
npx @elisym/merchant-node run
```

A Tempo buyer pays with MetaMask (or another EIP-6963 wallet that can add the Tempo network) and needs USDC.e on Tempo.

The node reads Tempo through the registry's public endpoint; set `"tempo": { "network": "...", "rpcUrl": "https://..." }` for your own. A store with only Tempo payouts needs no Solana `rpcUrl`.

The buyer's page shows a choice between the store's payouts on the page's network. A Tempo buyer needs a little more of the coin than the price: the wallet takes its fee in the same coin.

## Answering an order by hand

A buyer who cannot finish a purchase is told so by the open checkout (after the checkout is closed or the page reloaded, the order is listed under Your purchases): a payment the node refused (paid after a price change, or after the three days it watches an order), or one that never landed. With the node **stopped**, answer the order from its key (`orders` lists them):

```bash
npx @elisym/merchant-node complete <buyer>:<orderId>
npx @elisym/merchant-node refund <buyer>:<orderId> --tx <your refund transaction> --amount <subunits> --asset <caip19 of the refunded coin>
```

`complete` closes the order and sends it `completed`, as for a paid one; `refund` reports a refund you already sent (above zero; an unpaid order is released with `complete`, never a zero refund). `--asset` names the refunded coin as CAIP-19, the same id as in your config's `payouts`: it may be left out when the store has one payout, and is required when it has several. The refund transaction must be on that coin's chain. Running `refund` again for an answer kept by an older node sends it unchanged, without an asset. The command prints what the ledger holds for the order and asks before it sends (`--yes` for scripts); the order is then closed for good, and running the same command again sends the same answer again. Check the payment yourself (the node's log and a block explorer) before you answer.

An order answered by hand sends no webhook (see below): `orders` and the command show its customer reference, so you can credit it yourself.

## Credit an account on your backend

To credit a user's account after a payment (a deposit, a top-up), the page passes your own id of the account (`customer-ref`), and your node tells your backend in a signed `order.paid` webhook once it has verified the payment on chain. Never credit from the browser: a buyer can forge anything a page reports. It needs a level A store on your page's domain. [Credit an account](/commerce/credit-an-account) has the page, the node's webhook and secret, the receiver and its table.

The node's README covers the rest: the full config, Docker, the lock, and what to keep safe.

## Try it free on devnet

Devnet runs the same flow with test tokens. It is a separate store in its own home, without a passphrase. Run this block in every new terminal for the devnet store:

```bash
export ELISYM_MERCHANT_HOME=~/.elisym-merchant-devnet
unset ELISYM_MERCHANT_PASSPHRASE_FILE
```

Then:

```bash
npx @elisym/merchant-node init --network devnet
```

In `$ELISYM_MERCHANT_HOME/config.json` the template already has `"network": "devnet"`, the public devnet RPC `https://api.devnet.solana.com`, and devnet USDC `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`: set your devnet wallet address, and fill in `products/my-product/PRODUCT.md`. Run `setup` and `run` as above, and put the `naddr` this devnet setup prints in the snippet, with `network="devnet"`. Switch the wallet to devnet (testnet mode in Phantom or Solflare). The wallet needs devnet SOL for fees ([faucet.solana.com](https://faucet.solana.com)) and devnet USDC for the price ([faucet.circle.com](https://faucet.circle.com), Solana -> Devnet).

For a Tempo payout on devnet, use Moderato, Tempo's testnet: the script above with `config.tempo = { network: "moderato" }` and `caip19: "eip155:42431/erc20:0x20c0000000000000000000000000000000000000"` (pathUSD). A devnet store on Tempo only drops the Solana payout and `rpcUrl` as in [Only one network](#1-create-the-store), with those two values. Its buyer pays with MetaMask on Moderato, holding a little more testnet pathUSD than the price.
