Lotvs PayEarly access

Docs

Charge agents per request

Put a price on a route. A request without payment is told the price; an agent pays and asks again; your code runs, and the money goes from the agent straight to you.

  • Nothing else to install
  • Express, Next.js, Node
  • Both versions of x402

Quickstart

Four steps, and the last one is watching it work.

  1. Install

    Terminal
    npm install @lotvs/pay
  2. Say where the money goes

    Payments arrive in USDC, a digital dollar, at a Solana address you control. Any wallet works, and so does the address of your Lotvs account.

    .env
    # The Solana address that gets paid
    LOTVS_PAY_TO=YourSolanaAddress

    The address must already hold USDC, or have held it once. A payment cannot open the account it lands in. Send yourself a dollar of USDC first and this is taken care of.

  3. Put a price on a route

    One line, in front of the code you already have.

    server.js
    import express from "express";
    import { lotvsPay } from "@lotvs/pay";
    
    const app = express();
    
    app.get("/report", lotvsPay({ price: "0.10" }), (req, res) => {
      res.json({ report: "Markets were open today." });
    });
    
    app.listen(3000);
  4. Watch it answer

    Ask without paying and you get the price back. This site runs a paid route of its own; the first two steps below are real, sent from your browser.

    pay.lotvs.ai/api/demo/fortune$0.01 a request
    RequestReady
    GET /api/demo/fortune HTTP/1.1
    Host: pay.lotvs.ai

    No payment, no key, no account. This is what any agent sends the first time it meets a paid route. Send it from your browser and see what comes back.

When the buyer is charged

There are two orders to do things in. The first is the default, and it is the one to keep unless your code is slow.

Charge after success

Default
  1. Check the payment
  2. Run your code
  3. Collect the money
  4. Answer

A request that fails costs the buyer nothing: any answer with a status of 400 or above is not charged, and the same payment can be sent again. Your code has about a minute, which is how long a signed payment lasts.

Collect first

flow: "upfront"
  1. Collect the money
  2. Run your code
  3. Answer

For anything that streams its answer or takes more than a few seconds. If your code fails after the money has moved, the refund is yours to make, and onFailure tells you when.

Collect first
app.get("/research", lotvsPay({ price: "1.00", flow: "upfront" }), async (req, res) => {
  for await (const chunk of longRunningWork()) res.write(chunk);
  res.end();
});

Receipts and your ledger

Inside a paid handler, paymentOf gives you the payment that came with the request.

Who paid
import { lotvsPay, paymentOf } from "@lotvs/pay";

app.get("/report", lotvsPay({ price: "0.10" }), (req, res) => {
  const payment = paymentOf(req);
  res.json({ report, for: payment.payer });
});

To keep a record, take every receipt as it happens. A receipt is a plain object, ready to store as it is.

Recording payments
lotvsPay({
  price: "0.10",
  onPayment: async (receipt) => {
    await db.payments.insert(receipt);
  },
  onFailure: (failure) => {
    log.warn(failure.stage, failure.reason, failure.charged);
  },
});
transaction
The payment's id. Public and permanent: anyone can look it up.
payer
The buyer's address.
amount
What was paid, as a decimal: "0.1". amountAtomic has it in the token's smallest unit.
resource
The route that was bought. method has the verb.
settledAt
When the money moved.
test
True on the test network, where no real money moved.

A failure says whether the buyer's money moved: charged is "no", "yes" or "unknown". Unknown means the payment was sent and could not be confirmed. Look the transaction up before doing anything else.

Prices per request

A price can be worked out from what is being asked for, and a payment can carry a reference of yours, such as an order number.

A price and a reference from the request
lotvsPay({
  // The same request must always get the same price
  price: (request) => (request.url.endsWith("/full") ? "2.50" : "0.10"),

  // Written into the payment itself, to match against your own records
  memo: (request) => "order-" + new URL(request.url).pathname,
});

Test mode

One setting moves everything to Solana's test network, where the dollars are not real. Prices, payments and receipts behave the same, and payment.test is true.

.env
LOTVS_PAY_NETWORK=solana-devnet

More than one server

A payment is remembered for two minutes so that it cannot buy two answers. By default that memory lives in each process. Behind a load balancer, or on serverless, give your servers one memory to share.

With Redis
const replayStore = {
  claim: async (key, ttlSeconds) => (await redis.set(key, "1", "NX", "EX", ttlSeconds)) === "OK",
  release: async (key) => void (await redis.del(key)),
};

lotvsPay({ price: "0.10", replayStore });

Options

A mistake in these stops your server from starting, with a message that says which one. The exception is a missing address: the route then answers 503 until it has one.

OptionDefaultWhat it does
pricerequiredWhat one request costs, in dollars: "0.10". Or a function of the request.
payToLOTVS_PAY_TOThe Solana address that gets paid.
network"solana""solana" for real money, "solana-devnet" for none. Also read from LOTVS_PAY_NETWORK.
descriptionnoneShown to the buyer's agent before it pays. Worth writing well: it is your shop window.
flow"authorization""authorization" collects after your code succeeds. "upfront" collects first.
memononeA reference written into the payment, up to 256 bytes. A string, or a function of the request.
onPaymentnoneCalled once for every payment collected, with the receipt.
onFailurenoneCalled when a payment was refused, failed, or could not be confirmed.
pendingSettlement"withhold"What to do when a payment was sent but is not confirmed yet: "withhold" the response or "deliver" it.
facilitatorspublic onesWho checks and settles payments. The defaults need no account.
replayStorein memoryRemembers payments already used. Share one if you run several servers.
legacytrueAlso accept clients on the first version of x402.
baseUrlfrom the requestYour public address, when it cannot be worked out from the request: "https://api.example.com".

What a buyer is told

Every answer that is not your own, and what it means. The body is JSON, with the reason in error.

StatuserrorWhen
402the reason, in wordsNo payment came with the request, or the one that came was refused, failed or already used. The answer carries the terms, so the buyer can pay and ask again.
402settlement_pendingThe payment went out but is not confirmed. The buyer is told the transaction and told not to pay twice.
400invalid_paymentThe payment header could not be read at all.
503payments_unavailableThe payment could not be checked just now. Nothing was charged, and trying again is safe.
503payments_not_configuredThe route has no address to be paid to yet. It refuses rather than work for free.

Questions

Where is my money?

At the address you set, by the time the buyer reads your answer. Lotvs Pay never holds it. To move it to a bank account, send it on from a Lotvs account or any service that takes USDC.

Which agents can pay?

Any client that speaks x402 and holds USDC on Solana, including an agent with a Lotvs account. x402 is an open standard, and both of its versions are answered.

What does the buyer pay on top?

Nothing. They pay the price you set. The fee for moving the money is covered for them.

Can I refund a payment?

Yes, by sending the amount back to the payer on the receipt. A refund is an ordinary transfer from your wallet.

What if my code fails after the buyer paid?

Under the default it cannot happen: the money is collected only after your code succeeds. With flow set to "upfront" it can, and onFailure tells you, with the payment's id, so you can put it right.