<!--
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)
-->

# The widget

`<elisym-buy>` is the checkout on your page. The only code that runs on your page is `embed.js`, a small loader. It frames the checkout from `https://pay.elisym.network`. Everything else runs inside that frame, on elisym's origin: the offer is verified, the order is placed, the buyer's wallet signs and the payment is followed there.

```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 element shows a Buy button. A click opens the checkout in a modal over your page: a centered card on a wide screen, a sheet at the bottom on a phone. The buyer closes it with its close button, Escape, a click on the dimmed backdrop, or the checkout's own Done button once the purchase is complete. Focus goes back to the button. With `display="inline"` the checkout sits in the page instead, where the element is, with no button.

Each product has its own `naddr`: to sell several, put one `<elisym-buy>` per product on the page.

## Attributes

| Attribute       | Value                          | Meaning                                                                                                                                     |
| --------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `product`       | `naddr1...`                    | The product, as `setup` printed it. Required.                                                                                               |
| `network`       | `mainnet` or `devnet`          | Only payouts on this network are offered (`devnet` is Tempo's Moderato on Tempo). Without it, the network of the first payout the store lists is used. |
| `strict-origin` | (no value)                     | Also refuse a store no domain vouches for (level C). A level A store is always refused off its domain (see [Where it sells](#where-it-sells)). |
| `collect-email` | (no value)                     | Ask the buyer for an email (optional for them). It goes to your node with the order, and `orders` lists it. The node never sends email.     |
| `theme`         | `auto`, `light` or `dark`      | The checkout's colors. `auto` (the default) follows the buyer's system; the snippets use `dark`.                                          |
| `display`       | `modal` or `inline`            | `modal` (the default): a Buy button opens the checkout in a modal. `inline`: the checkout sits in the page.                                |
| `label`         | text                           | The Buy button's text. Without it, or when empty, the button reads "Buy". Changing it never reloads the checkout.                          |
| `customer-ref`  | your account id                | The account a payment credits, through your node's webhook (see [Credit an account](/commerce/credit-an-account)). Honoured only for a level A store on its own domain, in the top-level page; it turns `strict-origin` on. Empty means "not known yet": the button shows disabled and nothing is framed. |

Changing any other attribute re-frames the checkout when what the checkout receives changes. A different `customer-ref` is a different account: its orders and its history in the browser are its own, and the checkout never shows or continues another reference's order. An open modal stays open, unless the change is to `display="inline"` or removes `product` (or makes it something other than an `naddr1...`), which close it. A purchase that is in progress is not lost: the checkout keeps it in the browser, follows it in the background and lists it under Your purchases; the re-framed checkout starts at its first step (a page where the checkout refuses to sell still shows it).

## What the buyer sees

The checkout is one panel inside the frame that grows as the purchase goes on:

1. The product and its price.
2. **Pay with**: a dropdown of the coins and networks the store takes, when there are several (a label when there is one).
3. The email field, when you set `collect-email`.
4. The wallets the buyer's browser has, at once under **Pay with**. The payout and the email above stay editable until a payment starts. After a price change, the wallets close and the buyer presses **Choose wallet** to open them again, so the new price is seen before any wallet can be clicked.
5. Once the buyer picks a wallet, the payment's progress takes the wallets' place, and the payment shows read-only above it.
6. **Payment complete** (or **Order complete** when the receipt shows no payment, such as an order the store completed by hand), or the refund, with a receipt: what the order cost, the order number (to give the store if the buyer needs help), and the transaction this checkout sent once the chain says it went through, shown shortened as its explorer link ("Paid" when this checkout confirmed the payment itself; "Copy receipt" copies the full id), or how the order ended. **Buy again** starts a new purchase. Nothing is delivered through the checkout: the store acts on its node's webhook (see [Credit an account](/commerce/credit-an-account)). The receipt stays until the buyer leaves. In a modal, closing it at any point returns the checkout to its first step: the page hears `ready` (except on a page where the checkout refuses to sell, which keeps its refusal and the order it shows; the page then keeps `refused`). A payment already sent goes on and shows under **Your purchases**. A press while it may still land says so and waits; it never asks the wallet for a second payment. Earlier purchases are under **Your purchases**.

While the wallet connects, the buyer can cancel and choose another wallet.

If the wallet does not answer a Solana payment request, after a few seconds the checkout counts down to when the request expires (about a minute and a half), then offers Start over. Rejecting the request in the wallet ends it at once. If the wallet fails on Solana without the buyer declining (its window never opened, say), the checkout offers to open it again (**Open Phantom again**, with the wallet's own name): the same wallet is asked again at once, inside the same attempt (the same transaction, the same expiry), with a countdown to when another wallet can be tried. Declining that second request keeps the attempt (the page hears no `ordered`), and the button stays. It is not offered once a transaction for the order reached the network, once the request is about to expire, or after the wallet signed an answer the checkout did not send.

There is no confirmation step and no box to tick. The product's name heads the checkout the whole time, with the store's name under it and its trust level when it has one (level A or B), unless the store no longer offers the product on this page, and a devnet checkout says "Test network". The buyer always has something to do or knows how long to wait: when the buyer declines in the wallet, the checkout is back at once and the same order can be paid again; when a wallet failed in another way, the checkout says "you can retry in about M:SS" while the attempt may still land, then offers the retry and Start over once it provably cannot. If the buyer closes the checkout meanwhile, the attempt goes on in the background and shows under Your purchases; a new press asks the wallet for nothing while it may still land: it says so, with a countdown when one applies. An immediate retry after a decline relies on the wallet not sending a transaction it reports as declined, and opening the same wallet again relies on it not sending one when it reports a failure: the checkout signs with `signTransaction` only and sends the transaction itself, and before it asks again and before it sends the answer it checks that nothing for the order reached the network since the attempt began. Pages that still use `v1/embed.js` get the same checkout, inline, with no change to their snippet.

The buyer always pays the listed price. When the elisym protocol fee is above 0, it comes out of the store's share (the treasury gets the fee, the store the rest), and a purchase under **Your purchases** shows "Includes elisym fee X". The checkout takes such a payment only for a store whose node declares it can (an updated node, after its `setup` is run again): any other store is refused with a note that its payment node must be updated, and nothing is ordered. If the fee cannot be read (`fee_config_unavailable`), the checkout says so and nothing is sent; the buyer tries again. If the fee configuration it reads is unusable (`fee_config_invalid`), the checkout says so and nothing is sent; trying again does not help.

## Status events

The element fires `elisym-status` (it bubbles) whenever the purchase moves on. The event carries the state name only:

```js
document.querySelector('elisym-buy').addEventListener('elisym-status', (event) => {
  console.log(event.detail.state);
});
```

| State       | Meaning                                                                      |
| ----------- | ---------------------------------------------------------------------------- |
| `ready`     | The verified offer is shown: at every load of a page the checkout accepts, and after any close of the modal. |
| `ordered`   | The order reached the store's inbox relays. Nothing is paid yet. Posted when it happens in this session; a page loaded over an earlier unpaid order hears `ready`. |
| `paying`    | A payment was requested from the buyer's wallet. It may still land, or expire. When the buyer declines in the wallet, the order is `ordered` again (a page that already heard `paying` hears `ordered`; not after the checkout was closed: the page then hears nothing more about that order): nothing was paid, and the same order can be paid again. A decline when the checkout opens the same wallet again is not that: the attempt stays live, and the page hears no `ordered`. |
| `paid`      | The payment is confirmed on chain. Waiting for the store to confirm.        |
| `completed` | The store completed the order while the checkout showed it. Not sent for an order completed after the modal was closed or the page reloaded, and never on a page that hears `refused`. A reload after a finished purchase starts a new one: the page hears `ready`, not `completed` again. |
| `refunded`  | The store cancelled the order and refunded it.                               |
| `ended`     | The buyer left an order that was never paid.                                 |
| `refused`   | The checkout will not sell here (see below). On a page without `customer-ref`, when the buyer has an earlier order of this product, the checkout still shows it to them inside a frame that keeps the refusal's height (the order scrolls within it), and the page hears `refused` only: a page never learns what a visitor bought. A refusal on a page with `customer-ref` shows no order at all. A page whose network this checkout does not serve hears `refused` whether or not the visitor bought before. |

Use them for analytics, or to show your own "thank you" next to the checkout. Outcomes of orders followed in the background (after a close or a reload) are not posted: act on the webhook. Never unlock or credit anything on an event: it tells you a state, not who paid. Your backend learns from your node's signed `order.paid` webhook ([Credit an account](/commerce/credit-an-account)).

A returning buyer with an earlier unpaid order of the product sees the same checkout as a new visitor, and their order is continued silently when they pay. An email typed then goes with a new order instead.

In modal display the element also fires `elisym-open` when the modal opens and `elisym-close` when it closes (both bubble). `elisym-close` carries the element in `event.detail.element`. If your page removes the element while its modal is open, the element no longer bubbles to your page, so `elisym-close` is also fired once on `document`.

## In React

[`@elisym/react`](https://www.npmjs.com/package/@elisym/react) renders the element and adds the pinned v3 loader once per document (React 18 and 19). Props map to the attributes (`collectEmail` to `collect-email`, `customerRef` to `customer-ref`), and the events to `onOpen`, `onClose`, `onStatus` and `onPaid` (on `completed`, for your UI only, while the checkout shows that order; it does not fire for an order completed after the modal was closed or the page reloaded, nor on a page that hears `refused`. Never use it for fulfillment: your node's signed webhook is the source of truth):

```tsx
import { ElisymBuy } from '@elisym/react';

<ElisymBuy product="naddr1..." network="mainnet" theme="dark" onStatus={(state) => console.log(state)} />;
```

## Styling

The button and the modal are in the element's shadow root, styled by the loader. Your CSS can restyle them through two parts:

```css
elisym-buy::part(button) {
  background: #0f766e;
}
elisym-buy::part(dialog) {
  border-radius: 8px;
}
```

The checkout inside the frame is never styled by your page: only `theme` changes its colors. The snippets use `theme="dark"`, which we recommend. Use `theme="light"` for a light page; without `theme`, the checkout follows the buyer's system setting.

## What your page never learns

The frame tells your page state names, its own height and, in a modal, when to close, and nothing else. Your page does not learn the buyer's key, the order id, the wallet address or the email. A script on your page, including a compromised third-party one, cannot read them or act in the buyer's name. The frame accepts nothing from your page but the initial handshake.

## Your purchases

Under every purchase, the checkout offers **Your purchases**: this browser's payments at this store (completed, refunded, paid and waiting for the store, cancelled by the store with no refund stated, in progress, blocked), each with its receipt, and a **Download CSV** export (UTF-8, opens in a spreadsheet; no delivery). On a page with `customer-ref`, it lists only that account's purchases.

* Your page learns nothing of it: the button is the same with or without purchases, the list keeps one height whatever it holds, and no event is fired.
* The records live in this browser, for this site only. Browsers keep a framed checkout's storage per site, so another site embedding the same store, or another browser, shows other purchases.

## Trust levels and warnings

Before it shows a price, the checkout verifies the offer:

* the store's signed product;
* its profile, and the owner who authorizes the store;
* the owner's signed payout list, which is the only source of an address it ever pays.

It then shows the buyer how far the store is vouched for:

* **Level A**: the store's domain serves a `nostr.json` naming both the store and its owner (see [Level A](/commerce/quickstart#level-a-let-your-domain-vouch-for-the-store)).
* **Level B**: the store has a hosted name, but no website of its own vouches for it.
* **Level C**: no domain vouches for it. The buyer pays on trust.

Only the trust chip in the header shows the level ("Verified: your-domain" or "Named store"; an unverified store shows none). The checkout still works out its warnings (a payout list less than three days old, a payout unknown from earlier purchases, no wallet proof, no domain), but it shows none of them and asks the buyer to tick nothing. What protects the payment is unchanged: the address comes only from the owner's signed payout list, the offer is verified again just before paying, and a changed price or payout is shown to the buyer before anything is paid.

## Where it sells

A level A store vouches for one domain, and the checkout sells it only on a page served over `https:` from that domain or one of its subdomains. It refuses any other page, including `localhost`. A level C store sells on any page, with no trust chip. `strict-origin` refuses a level C store too, so a page with it sells only level A stores on their own domain.

## When the checkout refuses

The checkout shows no Pay button when:

* it is not framed by a page, or the page never started it (a page opened from disk, with no origin, is one). The page receives no event then;
* the product cannot be found or does not verify;
* the page is not on a level A store's domain, or `strict-origin` is set and the store is level C;
* the store has no payout on the `network` the attribute names;
* the store drops the payout network during a purchase: the checkout refuses rather than switching networks during a purchase. Set `network` for this to hold across page reloads too; without it, a reload uses the store's first-listed network;
* the network is not served by this checkout yet;
* the browser blocks storage for the checkout, for example with all site data blocked. The order record is what keeps a buyer from paying twice, so without it nothing is sold.

With `customer-ref`, the checkout also refuses when:

* the store is not level A, or the page is not on its domain (this holds whatever `strict-origin` says);
* the page is not the top-level window (the element sits in a frame).

The loader itself frames nothing, and logs an error, when the element with `customer-ref` sits in a frame or the value is not a valid reference.

In every case but the first, and those the loader refuses itself, the page receives `refused`.

A product the store has stopped selling shows as **Sold out** instead of the store or domain refusals above: no Pay button, and a note that an order already paid is still completed. The page receives `refused`, as for any refusal.

On a page without `customer-ref` that refuses to sell, a buyer with an order already paid or in progress still sees that order to its end, even if the offer is refused later. Elsewhere, an order in progress is followed in the background and shown under Your purchases; the Buy button always starts a new purchase, and waits while that order may still be paid. On a page with `customer-ref` a refusal is final: it shows no order. That order is still followed and answered by your node and shows under Your purchases; a later visit the checkout accepts follows it in the background instead of showing it.

## Paying on Tempo

A store with a Tempo payout is paid from an EIP-6963 wallet (MetaMask). The checkout asks the wallet to switch to the right Tempo network (and to add it when the wallet does not know it), then sends one transfer. The buyer needs a little more of the coin than the price, for the wallet's fee. Wallets that cannot add a custom network are not offered on Tempo; if a wallet refuses the network, the checkout says so and the buyer picks another.

Every transfer carries the order's memo. For a store whose profile declares `["memo-marker", "1"]` (merchant-node 0.11 and later), it is the **marked** memo: the commerce magic `0xfab22e7e`, then 28 bytes of the order hash, which makes the payment countable in elisym's public statistics and, like any Solana commerce payment, makes the store's Tempo payout and sales volume publicly enumerable and links the buyer's wallet to an elisym purchase (no order data is revealed). For any other store it is the legacy memo. The form is fixed when the order is placed: an order resumed later pays the memo it was placed with. On Solana the payment carries the SPL Memo `elisym:commerce`; a wallet that drops it still pays. See [Commerce payment markers](/protocol/payments#commerce-payment-markers).

With the elisym protocol fee above 0, the payment has two legs, the store's share and the fee, and the checkout sends them as **one atomic batch** (`wallet_sendCalls`): one confirmation, and either both legs land or neither does. A wallet that cannot batch atomically is refused before anything is ordered, and the buyer picks another wallet. MetaMask on Moderato cannot batch, so on Moderato it is refused while the fee is above 0. While the wallet works on an approved batch the checkout shows the payment as confirming; if the wallet reports the batch failed, the order counts down like a request that was never answered. After a close or a reload, the checkout asks the wallet that approved the batch about it; if that wallet cannot answer without connecting, the order offers **Check in wallet**.

A Tempo wallet request never expires, so the checkout never retries a Tempo payment by itself:

* While the wallet has not answered (the checkout open on it), it counts down to the request's late deadline, about 40 minutes: "Your wallet has not answered. You can start over in M:SS.", then "Checking whether the request was approved…", then, with no countdown left, "This is taking long. If it does not resolve, contact the store.".
* When the wallet call failed with no hash, or on a page that shows only an existing order whose request is still open, the checkout says "the checkout stops waiting for this request in about M:SS; if you do not want to pay, reject it in your wallet", and if that stays unresolved, **this is taking long: check your wallet activity, or contact the store**.
* A press after a close or a reload: a request from this page that may still be approved blocks a new Tempo payment of that product ("Your wallet already has a request open. Answer or reject it there, or try again in m:ss.") until it is answered or its late deadline passes, and the old-prompt question still guards the next payment.
* A declined request ends the order: nothing was signed.
* A request that timed out ends the order, but the wallet may still show it. Before any new payment of the product the buyer is asked to reject it: **approving the old request pays that order as well**.
* If the recipient's account refuses the coin, the payment is **blocked**: the money is held, not delivered. Contact the store.
* A sent payment that fails on chain (for example, the wallet's fee left too little of the coin) holds the product in that browser until the store answers: contact the store, which releases it with `complete`.

Two cases can only be settled by hand, and the open checkout tells the buyer to contact the store; after the checkout is closed or the page reloaded, the order is listed under Your purchases (on a page that shows only an existing order, it is still shown, with the note):

* an old request approved more than three days after the order, or more than 45 minutes after the store changed its price or payout: the money reaches the store, and the node does not complete it on its own;
* a payment the store's node has not answered 30 minutes after it was found (on either rail).

The store answers such an order with its node's `complete` or `refund` command (see [the quick start](/commerce/quickstart#answering-an-order-by-hand)). The checkout also re-checks, on each visit, Tempo orders that ended in the last three days: a late payment found there is stored and shows under Your purchases (on a page that shows only an existing order, as a note on whatever is on screen).

## One loader, pinned

The checkout never changes the bytes of a published loader, so the `integrity` hash in the snippet stays valid. A changed loader ships under a new path with its own hash. The earlier loaders keep working unchanged: `https://pay.elisym.network/v1/embed.js` shows the checkout inline only, and `v2/embed.js` is v3 without `customer-ref`.

To move a page to v3, replace its `<script>` with the v3 one. Never load two: whichever loads first defines `<elisym-buy>` for every element on the page. v3 logs a warning in the console when it finds the element already defined, and an error when an element on the page carries `customer-ref`, which an older loader ignores.
