# SUMMON Agent Build Guide, read this whole document, then build exactly what it says.

You are helping me (a non-developer) build and deploy a tiny paid web service called an
"agent", and list it on a marketplace called SUMMON. Follow EVERY step in order. Do not
skip steps. Do not invent your own payment code, copy the files below EXACTLY as written.
If something is missing, re-read this document; everything you need is here. When you finish
a step, tell me in one plain sentence and move to the next.

WHAT WE ARE BUILDING
--------------------
One web address that:
  1. Does a small task (I will tell you what in one sentence).
  2. Charges a small fee in USDG on Robinhood Chain using SUMMON's payment system.
  3. Only charges when the task succeeds. If it fails, the caller pays nothing.

WHAT I WILL GIVE YOU
--------------------
  - My PUBLIC wallet address (starts with 0x). This is safe to share, it only RECEIVES money.
  - NEVER ask me for, and NEVER write down, a private key or seed phrase. If you think you
    need one, STOP and tell me, you do not need one to receive payments.

===================================================================================
STEP 1, Create a new project folder and put THREE files in it, exactly as written.
===================================================================================

------- FILE 1 of 3:  package.json -------
{
  "name": "my-summon-agent",
  "version": "1.0.0",
  "type": "module",
  "engines": { "node": ">=20" },
  "scripts": { "start": "node server.mjs" },
  "dependencies": { "express": "^4.21.0" }
}

------- FILE 2 of 3:  seller-middleware.mjs  (copy this ENTIRE file exactly, do not change one character) -------
/**
 * seller-middleware.mjs, how an agent earns USDG on Robinhood Chain via SUMMON.
 *
 * Drop-in for any express-style server. The contract, same as every SUMMON rail:
 *   1. no payment header  -> reply 402 with PAYMENT-REQUIRED terms (permit2)
 *   2. payment header     -> facilitator /verify -> run YOUR work
 *   3. your work succeeded -> facilitator /settle -> reply 2xx (+ receipt header)
 *   4. your work failed    -> reply 4xx/5xx and DO NOT settle: caller pays nothing
 *
 * usage (sync agent, answers in one request):
 *   import { summonPaywall } from "./seller-middleware.mjs";
 *   app.post("/v1/run",
 *     summonPaywall({ priceUsdg: "0.05", payTo: "0xYourWallet", facilitator: "https://fac.summon.example" }),
 *     async (req, res) => { const out = await doTheWork(req.body); res.json(out); }
 *   );
 *
 * usage (job agent, accepts now, delivers later; e.g. video render):
 *   DO NOT settle at accept time. Pass { defer: true } and call req.summonSettle()
 *   only when the job actually succeeds. Job dies -> never settle -> the buyer was
 *   never charged. The buyer's authorization stays redeemable for maxTimeoutSeconds
 *   (default 1h), so set it to comfortably outlast your longest job.
 *
 *   app.post("/v1/reel",
 *     summonPaywall({ priceUsdg: "3.00", payTo: "0x…", facilitator: "…", defer: true, maxTimeoutSeconds: 7200 }),
 *     async (req, res) => {
 *       const job = await queueRender(req.body, {
 *         onSuccess: async () => { await req.summonSettle(); },   // charge at DELIVERY
 *         // onFailure: do nothing, no settle means no charge, nothing to refund
 *       });
 *       res.status(202).json({ job_id: job.id });
 *     }
 *   );
 */

const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64");

export function summonPaywall({ priceUsdg, payTo, facilitator, description = "", defer = false, maxTimeoutSeconds = 3600, feeBps = 0 }) {
  // seller sets their NET price; the buyer is quoted net + fee. feeBps=0 => classic
  // direct settle (no fee). feeBps>0 => buyer pays the facilitator collector, which
  // forwards net to the seller and keeps the fee (SUMMON's take-rate on its own rail).
  const net = Math.round(Number(priceUsdg) * 1e6);
  const fee = Math.round((net * feeBps) / 10000);
  const total = net + fee;
  const amount = String(total);
  let spenderPromise = null;
  const getSpender = () => {
    spenderPromise ??= fetch(`${facilitator}/health`)
      .then((r) => r.json())
      .then((h) => {
        if (!h?.spender) throw new Error("facilitator /health returned no spender");
        return h.spender;
      })
      .catch((e) => { spenderPromise = null; throw e; });
    return spenderPromise;
  };

  return async function paywall(req, res, next) {
    try {
      const spender = await getSpender();
      const header = req.headers["payment-signature"] || req.headers["x-payment"];

      if (!header) {
        const terms = {
          x402Version: 2,
          error: "Payment required",
          resource: { url: `${req.protocol}://${req.get?.("host") ?? req.headers.host}${req.originalUrl ?? req.url}`, description },
          accepts: [{
            scheme: "exact",
            network: "eip155:4663",
            asset: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", // USDG
            amount,
            // with a fee, the buyer pays the facilitator collector (spender), which
            // forwards the seller's net and keeps the fee. Without a fee, pay seller directly.
            payTo: fee > 0 ? spender : payTo,
            maxTimeoutSeconds,
            extra: { name: "Global Dollar", assetTransferMethod: "permit2", facilitatorSpender: spender },
          }],
        };
        res.setHeader("PAYMENT-REQUIRED", b64(terms));
        return res.status(402).json({ error: "Payment required", meta: { cost_usdg: priceUsdg, network: "eip155:4663" } });
      }

      // verify BEFORE doing any work
      const v = await (await fetch(`${facilitator}/verify`, {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ payment: header }),
      })).json();
      if (!v.valid) return res.status(402).json({ error: `payment invalid: ${v.reason}` });

      let settledOnce = false;
      const settleNow = async () => {
        if (settledOnce) return { ok: true, already: true };
        const s = await (await fetch(`${facilitator}/settle`, {
          method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify(fee > 0 ? { payment: header, forwardTo: payTo, forwardAmount: String(net) } : { payment: header }),
        })).json();
        if (s.ok) settledOnce = true;
        return s;
      };

      // job agents: charge at DELIVERY, not at accept. handler calls req.summonSettle().
      if (defer) {
        req.summonSettle = settleNow;
        return next();
      }

      // sync agents: settle AFTER the handler succeeds, patch res to settle on 2xx
      const origJson = res.json.bind(res);
      res.json = async (body) => {
        const code = res.statusCode || 200;
        if (code >= 200 && code < 300 && !settledOnce) {
          const s = await settleNow();
          if (!s.ok) {
            // work is done but money didn't move: do NOT ship the result for free
            res.status(502);
            return origJson({ error: `settlement failed: ${s.reason}. You were not charged.` });
          }
          res.setHeader("PAYMENT-RESPONSE", b64({ status: "settled", transaction: s.txHash, amount, network: "eip155:4663" }));
        }
        return origJson(body);
      };

      return next();
    } catch (e) {
      return res.status(500).json({ error: `paywall error: ${e.message}` });
    }
  };
}

------- FILE 3 of 3:  server.mjs  (change only the two marked spots) -------
import express from "express";
import { summonPaywall } from "./seller-middleware.mjs";

const app = express();
app.use(express.json());

// Health check so the marketplace can see the agent is awake. Keep this.
app.get("/", (_req, res) => res.send("ok"));

app.post(
  "/run",
  summonPaywall({
    priceUsdg: "0.02",                                   // what to charge, in dollars
    payTo: "PUT_MY_PUBLIC_WALLET_ADDRESS_HERE",          // <-- CHANGE 1: my 0x address
    facilitator: "https://summon-facilitator-production.up.railway.app",
    feeBps: 200,                                         // 2% goes to SUMMON's rail
  }),
  async (req, res) => {
    // ================= CHANGE 2: DO THE WORK HERE =================
    // req.body holds the caller's input. Compute the result and return it as JSON.
    // Only reach this line on success. If the task cannot be done, instead do:
    //     return res.status(422).json({ error: "why it failed" });
    const input = req.body || {};
    const result = { message: "REPLACE THIS with your real output", youSent: input };
    // =============================================================
    res.json(result);
  }
);

app.listen(process.env.PORT || 3000, () => console.log("agent listening"));

===================================================================================
STEP 2, Fill in the two marked spots in server.mjs
===================================================================================
  - CHANGE 1: replace PUT_MY_PUBLIC_WALLET_ADDRESS_HERE with the 0x address I give you.
  - CHANGE 2: replace the marked block with code that does the task I describe.
    (Ask me for the task now if I have not told you: "What should this agent do?")

===================================================================================
STEP 3, Install and test it locally
===================================================================================
Run:
    npm install
    npm start
Then in another terminal:
    curl -X POST http://localhost:3000/run -H "Content-Type: application/json" -d "{}"
You should get HTTP 402 and a "Payment required" message. THAT IS CORRECT, it means the
payment wall works. (You are not charged; 402 just means "payment needed to continue".)

===================================================================================
STEP 4, Deploy it so it is ALWAYS ONLINE  (very important)
===================================================================================
An agent can only be hired while its web address is awake. Many FREE hosting tiers put your
service to SLEEP after a few minutes, or stop it entirely, then buyers get an "offline"
error and cannot pay. So:
  - Prefer an ALWAYS-ON host. Good free, always-on options: Vercel, Cloudflare Workers,
    Render (paid instance), Railway, or Zo Computer's always-on (paid) plan.
  - If the host you are on shows "offline", "asleep", "subscribe for always-on", or stops
    after idle: TELL ME clearly, and either switch to an always-on host or upgrade.
Deploy, then give me the public https address (it should look like https://something/run).

===================================================================================
STEP 5, List it on SUMMON
===================================================================================
Tell me to open  https://summon.app/robinhood  and click "List on Robinhood Chain".
I paste the public URL, fill a few fields, and it is live. Humans and other agents can then
hire it immediately, and I keep the payment minus SUMMON's small fee.

===================================================================================
RULES FOR YOU (the AI), do not break these
===================================================================================
  - Do NOT write your own x402 / payment logic. Use seller-middleware.mjs EXACTLY as given.
  - Do NOT invent network names. The middleware already uses the correct Robinhood settings.
  - Do NOT ask for or store a private key or seed phrase. Public 0x address only.
  - If a command errors "cannot find module express", run: npm install
  - If unsure, re-read this document. Everything you need is above. Then continue.
