โš’Anvil
Sign in

clo / monocle public

README.md

Monocle

Monocle is what a site needs in order to host somebody else's application behind tollbooths and pay its author: the record of each deployment and its terms, the declaration of a toll, the seam a payment provider plugs into, the books, and the manifest an application declares its tolls in.

It is a small library in plain Common Lisp (ASDF, yason, bordeaux-threads). It serves no page, compiles no application and moves no money; the site that uses it does those, and calls Monocle to keep the terms and the accounts straight.

The idea

An author's application says what it charges for. The house that hosts it takes the payment, keeps its fee, and owes the author the rest. The application carries no payment code.

  • A toll is a plist: (:key :drawing :label "SVG drawing" :mites 200), with :uses n (the payment is good for n uses) or :seconds n (for that long) when wanted. Its price is in the house's unit: a house's own currency, which visitors buy in bundles and which turns into money only when an author is paid -- :mites on common-lisp.net, :rivets on Genworks, each a currency of its own at its own rate -- or :cents for a house that prices in money. The price goes by the unit's name: :mites 200, :rivets 200, :cents 200.
  • A deployment is a directory, <root>/<runtime>/<name>/, holding deployment.json: the name, the owner, where the owner's share is paid, and the terms as they were the day it was deployed (the fee does not change under an author afterwards).
  • The books are one file, <root>/revenue.jsonl, a line appended per payment: the gross, what the payment cost to take by card (off the top), the house's fee, the author's share, and the runtime it ran on. Fees are exact on every line and rounded only where a sum is paid or reported.
  • A house is whoever hosts: its root directory, its runtime's name, its unit, its fee table, its payment provider and, if it keeps one, its community pot. Every function takes the house it works for, so two houses can share one image.
  • The fee is the house's share of every payment: its hosting charge. A house with a community pot may let authors add to it: an author chooses how many more points of every payment go to the pot (a slider from the fee upwards), frozen into the deployment's record like the fee, and booked apart from it. An author may also give some of what they are owed to the pot at any time (contribute!).
  • An author's share is held in the house's unit until it is paid out; payout-amount values it at that day's rate, less any spread.

Two ways to run an application

Monocle does not run anything, so it does not mind how a deployment runs. Two hosts use it today or are meant to:

  • In the host's own image. The application is Lisp source compiled into the image that serves it, and it places its own tollbooths on its page. The Gendl prompt lab (genworks/demos, prompt-lab/) works this way.
  • As a program of its own, in a container behind the house's gate. The application declares its tolls and the paths they stand on in a manifest, monetize.sexp, and the gate answers 402 Payment Required on those paths until the toll is paid. PROFILE.md is the contract such an application meets; source/manifest.lisp reads and checks the manifest.

Using it

(asdf:load-system :monocle)

(defparameter *house*
  (monocle:make-house :root "/var/lib/my-site/deployed/"
                      :runtime "sbcl"
                      :unit :mites
                      :fee-percents '(:open 25)
                      :pot-percent 0       ; a community pot, nothing added unless the author does
                      :provider :test))

;; a deployment and its terms
(monocle:save-record!
 *house*
 (monocle:make-record *house* :name "plate-sizer" :title "Plate sizer"
                              :owner "an-owner-key" :payee "author@example.org"
                              :pot-percent 10))   ; the author adds 10 points for the pot

;; a toll taken (the :test provider grants it and books a test line)
(monocle:charge-toll! *house* "plate-sizer"
                      '(:key :drawing :label "SVG drawing" :mites 200)
                      :reference "visit-42")

;; a real payment settled elsewhere, booked here
(monocle:book-revenue! *house* "plate-sizer" 1000 :reference "jar-123")

(monocle:earnings *house* "plate-sizer")     ; by quarter, for the author
(monocle:payables *house*)                   ; what each author is owed
(monocle:contribute! *house* "plate-sizer" 50)          ; a gift to the pot
(monocle:payout-amount (monocle:held *house* "plate-sizer") 1 :spread 1/50)
(monocle:revenue-report *house* :year 2026 :quarter 4)   ; by runtime

A payment provider

monocle:take-toll is a generic function of the house's provider. The :test method grants every toll and books it marked as a test, so the whole flow can be exercised with no money moving; the reports leave test lines out. A real provider is a method that takes the money and then books the line:

(defmethod monocle:take-toll ((provider (eql :my-gateway)) house name toll reference)
  (let ((payment (debit-somehow reference (monocle:toll-price toll))))
    (when payment
      (monocle:book-revenue! house name (monocle:toll-price toll)
                             :toll (string-downcase (getf toll :key))
                             :card (payment-cost payment)   ; 0 when the unit was bought in bundles
                             :reference (payment-id payment))
      t)))

A toll is granted on a settled payment, never on a browser's word, and its price is read from the application's declaration, never from the request.

A manifest

(multiple-value-bind (manifest reason) (monocle:read-manifest "monetize.sexp")
  (if manifest
      (or (monocle:manifest-faults manifest)       ; a list of strings, or nil
          (monocle:toll-for-path manifest "/export/svg?size=a3"))
      reason))

A manifest is data from a stranger. It is read with the reader's evaluation off and checked against a closed list of fields before anything acts on it.

A house of containers

house/ holds the two programs a host needs to run applications as programs of their own. Neither is part of the system; each is loaded after it.

  • house/render.lisp reads the house's apps.sexp (one entry per application: its name, its image, the house's copy of its manifest, its payee and its slider) and writes a Compose file with a read-only container per application, each on a network of its own with no way out, and the gate's configuration: each application's name to its container, with its tolls. An entry with faults is left out, with its reasons.
  • house/keeper.lisp keeps the house's records and books behind a small door, POST /book, where the gate books each toll it takes; GET /earnings?app= answers an application's quarters. It runs on a network only the gate shares.

The gate itself is not here: it is whatever reverse proxy the house runs, configured from what render writes. common-lisp.net's apps host (clo/cl-site, clnet/deploy/apps/) is the first kit built on them.

Checks

ci/test.lisp runs the library against a scratch directory:

(asdf:load-system :monocle)
(load "ci/test.lisp")

Status

The records, tolls, the provider's seam, the books and the manifest are here and in use, and so are the renderer and the keeper for a house of containers. Not here yet: providers that move money, and the check that builds and runs an application against the profile.

Licence

GNU Affero General Public License, version 3 or later; see LICENSE.