Cosmic Bull

service_market

gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_market on gno.land mainnet

realmfactory-applicationmarketplacelive on mainnet

Buy and sell services with the payment held in escrow.

The provider is paid once the customer accepts or the acceptance window lapses, so neither side has to go first on trust.

In detail curated

Customer-first escrow marketplace for services: two-level state machine over offers and orders, composing coinio + feeledger + avl/v0 + sanitize/v0. Application-factory benchmark #4.

Use it

A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.

Read it — no key, no account, no GNOT

gnokey query vm/qeval \
  --data 'gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_market.EscrowTotal()' \
  --remote https://rpc.gno.land:443

EscrowTotal is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.

Change its state

Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 16 crossing functions, with the argument names filled in.

The gno.land mainnet builder spends real GNOT. Start with the read above, and size -gas-fee from a simulation rather than from any example.

Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.

Import it from another realm

import "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_market"

The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.

Deployments

gno.land mainnet gnoland-1

RenderedSourceCall builderState

chain-attested
Import pathgno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_market
Namespaceg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Deployed at height336,753
Deploy transaction92052b544ecf3d932fdb29c6e85051247d5673f1d9607df1cdf4de38d9e2d192 look it up on the RPC
Deployerg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Gas used98,665,579
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml service_market.gno service_market_test.gno
Deployed bytesservice_market.gno — 33,389 bytes
sha256868af708d956d4aa5425be45145e515f5e935d40374d6160cec86b04ee96ab9f
Realm addressg1zuafcf6ptq2dehpxt4x4fwjsp5q9xvy3twkfqf derived, never confirmed against the realm

Do not take the hash above on trust. $download returns the bytes gnoland-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_market$download&file=service_market.gno' | shasum -a 256

Expected: 868af708d956d4aa5425be45145e515f5e935d40374d6160cec86b04ee96ab9f — 33,389 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 33 exported functions.

Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:

AcceptAdminAcceptDeliveryBalanceOfClaimClaimAllClaimRefundDeclineOrderEscrowTotalFeeBpsFeesAccruedHeightHeldLiabilitiesMarkDeliveredNumOrdersNumServicesOrderDeadlinesOrderURIPurchaseServiceQuoteRegisterServiceReleaseTimeoutRenderRetireServiceServiceDescriptionServicesOfSetFeeBpsSetFeeRecipientSurplusSweepDenomTransferAdminUsersTotalWithdrawFees

Overview

Realm service_market is a custodial GNOT marketplace for SERVICES: providers publish a standing offer (title, description, price, delivery window), a customer purchases it by paying the exact price into escrow, the provider marks the work delivered, and the escrow is released to the provider only once the customer accepts — or once the acceptance window lapses.

COMPOSITION (per the recorded DISCOVERY / REUSE ANALYSIS in DISCOVERY.md): balance accounting is feeledger, coin movement is coinio, free-text render safety is p/nt/markdown/sanitize/v0. This realm owns only the two-level offer/order state machine, which the discovery gate found nowhere else: every escrow realm in the searched sources escrows FUNDER-FIRST (the party who will later decide escrows first, then solicits work), while a service marketplace must escrow CUSTOMER-FIRST against a standing offer.

LIFECYCLE. A service is a reusable offer; an order is one purchase of it. Order terminal states are frozen and reached exactly once.

RegisterService (anyone, no coins) : status Active. The fee bps is SNAPSHOTTED under the provider's own maxFeeBps ceiling. RetireService (provider only) : Active -> Retired. Blocks NEW orders only; orders already in flight are untouched. PurchaseService (EOA + -send) : order Purchased, amount into escrow. Sets the delivery deadline. MarkDelivered (provider only) : Purchased -> Delivered. Starts the acceptance window. AcceptDelivery (customer only) : Delivered -> Released. THE RESOLUTION. Escrow becomes the provider's claimable balance minus the snapshotted fee. ReleaseTimeout (anyone) : Delivered -> Released after AcceptanceBlocks. Deemed acceptance. DeclineOrder (provider only) : Purchased -> Refunded, fee-free. ClaimRefund (anyone) : Purchased -> Refunded after the delivery deadline, fee-free. Claim / ClaimAll (anyone) : pays out the caller's own claimable balance.

EVERY VALUE EXIT IS A PULL. Release and refund only move numbers between the escrow pool and a claimable balance; the beneficiary later calls Claim. Nothing in this realm ever pushes coins to a third party, which is what lets both timeout valves stay permissionless: a stranger triggering ReleaseTimeout or ClaimRefund performs no send, so a beneficiary that cannot receive a send can never brick the valve.

THE THREE REQUIRED PROTECTIONS.

Unauthorized settlement: every identity derives from the crossing entrypoint's cur.Previous().Address(); no function takes a caller identity as a parameter (the designation-forgery shape that disqualified most of the escrow realms found in discovery). The payee is COPIED INTO THE ORDER at purchase, so neither retiring the service nor any later edit can redirect an in-flight order's payment. There is no admin path to any order's escrow: the admin sets the fee for FUTURE registrations and nothing else.

Double payment: an order's status is checked and driven terminal in the same transaction that moves its value, and escrowTotal is decremented in lockstep with the credit. Release and refund both require a non-terminal status, so at most one of them can ever succeed for a given order, and neither can succeed twice.

Stuck funds: both counterparties have a permissionless valve against the other's inaction. A provider who never delivers loses the escrow back to the customer at the delivery deadline (ClaimRefund); a customer who never accepts loses it to the provider at the acceptance deadline (ReleaseTimeout). Neither valve trusts its caller.

ACCEPTANCE IS A PROTOCOL CONSTANT, NOT A PROVIDER SETTING. The provider chooses the delivery window (their own commitment, and their own risk), but AcceptanceBlocks is fixed. Were it provider-chosen, a provider would set it to zero, mark work delivered, and auto-release in the same block — settlement without resolution, wearing the costume of a timeout.

MONETARY INVARIANT (conservation). With H = ugnot held at the realm address, E = escrowTotal (Σ amount over Purchased and Delivered orders), U = claimable balances, F = the fee pot, S >= 0 out-of-band surplus:

H == E + U + F + S

PurchaseService raises H and E by exactly the price. Release moves amount from E to U+F (feeledger guarantees credited + fee == amount); refund moves amount from E to U at zero fee. Claim*/WithdrawFees debit the ledger before coinio.Payout moves the identical amount out. Any panic aborts the whole transaction; this realm never issues or removes coins. Surplus is recoverable only via SweepDenom (fee recipient), which reserves Liabilities() = E + U + F.

LIMITATION — NO DISPUTE ARBITRATION, DELIBERATELY. This realm resolves on acceptance or on the acceptance timeout. It does NOT adjudicate whether delivered work was good. A customer who considers the work inadequate has no lever here beyond declining to accept, and the timeout will still pay the provider. That is a real limitation and it is the deliberate price of the property above: an arbiter empowered to redirect escrow is a party who can seize funds, and the closest realm found in discovery (r/samcrew/escrow_v3) carries exactly that shape — a single hardcoded admin key, with no rotation function, that is simultaneously sole arbiter, a unilateral release path, and a permanent pause switch over every fund path. Adding arbitration here would change what this application IS and expand its security model, so it is refused rather than smuggled in. A deployment that needs adjudicated disputes needs that designed, audited and authorized as its own application.

REALM-CALLER CAVEAT: assertNoSend reads the ORIGIN transaction's send envelope, not this realm's receipt, so a realm caller is refused by every non-payable function whenever the SAME transaction attached a -send anywhere — even though this realm received nothing. It fails closed, it is not third-party triggerable, and the workaround is to isolate the call in its own -send-free transaction.

Named concretely, because the generic phrasing understates where it lands. It applies to every exported function except PurchaseService, the only payable one, but four of them carry the weight: MarkDelivered, without which no order against a realm provider can ever reach a release; ReleaseTimeout and ClaimRefund, the two valves this design deliberately leaves permissionless; and Claim / ClaimAll, the only paths that extract a credited balance. A realm-based keeper bot that batches a valve call into a transaction carrying coins for some other purpose is therefore silently unusable, and a realm provider must budget a dedicated transaction both to deliver and to collect. Requirement 3 (no stuck funds) survives this: both valves are permissionless and any EOA can pull them, so no order depends on a realm caller to unstick.

Imports

Constants and variables

Service status values.

const (
	ServiceActive  = "active"
	ServiceRetired = "retired"
)

Order status values. Released and Refunded are terminal.

const (
	OrderPurchased = "purchased"
	OrderDelivered = "delivered"
	OrderReleased  = "released"
	OrderRefunded  = "refunded"
)

Input bounds.

const (
	MaxTitleLen = 80
	MaxDescLen  = 2000
	MaxURILen   = 400
	MinPrice    = int64(1)

	// MinDeliveryBlocks/MaxDeliveryBlocks bound the provider's own
	// turnaround commitment: ~1 hour to ~60 days.
	MinDeliveryBlocks = int64(850)
	MaxDeliveryBlocks = int64(1200000)

	// MaxServicesPerProvider bounds catalog monopolization by a single
	// address. The same finding was RED in both permission_registry and
	// service_registry; it is cheaper to carry the counter from the
	// start than to discover it in an audit.
	MaxServicesPerProvider = 20

	// RenderLimit bounds the home page. An unbounded Render was YELLOW
	// in three prior audits.
	RenderLimit = 20
)

AcceptanceBlocks is how long a customer has to accept a delivery before anyone may release it to the provider. It is a PROTOCOL constant on purpose — see the header. ~7 days at pearl's observed ~4.2s blocks.

const AcceptanceBlocks = int64(140000)

Denom is the only asset this realm accepts.

const Denom = "ugnot"

MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%.

const MaxFeeBps = int64(1000)

Functions

AcceptAdmin

func AcceptAdmin(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as AcceptAdmin(cross(cur), ...).

AcceptAdmin completes a staged handover. Only the nominee may call it.

AcceptDelivery

func AcceptDelivery(cur realm, orderID int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as AcceptDelivery(cross(cur), ...).

AcceptDelivery is THE RESOLUTION: the customer accepts the delivered work and the escrow becomes the provider's claimable balance, minus the fee snapshotted into the order at purchase. Customer only.

BalanceOf

func BalanceOf(addr address) int64

Claim

func Claim(cur realm, amount int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Claim(cross(cur), ...).

Claim sends amount ugnot of the caller's claimable balance back to the caller.

ClaimAll

func ClaimAll(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as ClaimAll(cross(cur), ...).

ClaimAll sends the caller's entire claimable balance back to the caller. Fails if there is nothing to claim.

ClaimRefund

func ClaimRefund(cur realm, orderID int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as ClaimRefund(cross(cur), ...).

ClaimRefund returns the escrow to the customer once the provider has missed the delivery deadline. Permissionless: a provider who abandons an order must not be able to strand the customer's money.

It is fee-free. Charging a protocol fee on a refund would mean the marketplace profits from provider non-performance.

DeclineOrder

func DeclineOrder(cur realm, orderID int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as DeclineOrder(cross(cur), ...).

DeclineOrder returns the escrow to the customer before delivery. Provider only, fee-free — a provider who cannot fulfil should be able to hand the money straight back rather than wait out a deadline. Safe to allow unilaterally because it only ever moves value AWAY from the caller.

EscrowTotal

func EscrowTotal() int64

EscrowTotal is the live escrow pool E: the sum of every order still in Purchased or Delivered.

FeeBps

func FeeBps() int64

FeesAccrued

func FeesAccrued() int64

Height

func Height() int64

Held

func Held() int64

Liabilities

func Liabilities() int64

Liabilities is everything this realm owes: live escrow, claimable balances and the fee pot. SweepDenom reserves exactly this.

MarkDelivered

func MarkDelivered(cur realm, orderID int64, uri string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as MarkDelivered(cross(cur), ...).

MarkDelivered records that the work is done and starts the acceptance window. Provider only. The uri is an opaque reference to the deliverable; this realm neither fetches nor interprets it, and it is sanitized on the way out to Render.

Delivering AFTER the delivery deadline is allowed as long as nobody has yet called ClaimRefund — late work the customer still wants should not be destroyed by a deadline that exists to protect the customer. The customer's remedy is unchanged: they simply do not accept, and the refund valve stays open right up until this transition lands.

NumOrders

func NumOrders() int64

NumServices

func NumServices() int64

OrderDeadlines

func OrderDeadlines(id int64) (deliveryDeadline, acceptanceDeadline int64)

OrderDeadlines returns the two absolute heights that govern an order. acceptanceDeadline is 0 until the order is delivered.

OrderURI

func OrderURI(id int64) string

PurchaseService

func PurchaseService(cur realm, serviceID int64) int64

Crossing function. Callable from a transaction via MsgCall, and from another realm as PurchaseService(cross(cur), ...).

PurchaseService escrows EXACTLY the listed price and opens an order. The caller must be an EOA (coinio.Receive is the receipt-guaranteed shape) and must attach exactly the price in ugnot; any mismatch aborts and the coins revert with the transaction. Self-purchase is rejected.

Quote

func Quote(id int64) (price, fee, toProvider int64)

Quote reports what a purchase of this service would cost and what the provider would receive on resolution, at the service's snapshotted fee.

RegisterService

func RegisterService(cur realm, title, description string, price, deliveryBlocks, maxFeeBps int64) int64

Crossing function. Callable from a transaction via MsgCall, and from another realm as RegisterService(cross(cur), ...).

RegisterService publishes a standing offer and returns its id. No coins are accepted; the storage deposit the caller pays is the anti-spam. The current protocol fee is snapshotted into the service and must not exceed maxFeeBps, the ceiling the provider signed for — pass MaxFeeBps to accept any legal fee.

Providers may be EOAs or realms, but a realm provider takes on three obligations that this realm cannot check for it. Realm bytes are immutable after addpkg, so a realm deployed without them cannot acquire them later: failing (1) means it never earns, failing (2) or (3) means what it earned is permanently stranded.

  1. It must already expose a crossing entrypoint that calls MarkDelivered. That is the only transition to OrderDelivered, and both release paths require it, so a provider realm that cannot call it never earns a credit at all — every order against it runs to ClaimRefund instead. This obligation binds before the next one.
  2. It must already expose a crossing entrypoint that calls Claim or ClaimAll on this realm. A credited balance is a pull, never a push; nothing here can reach into a provider realm to deliver it.
  3. It must make both calls in transactions whose ORIGIN carried no -send, per the REALM-CALLER CAVEAT in the package header.

The customer side is not symmetric: PurchaseService is payable and routes through coinio.Receive, which requires an EOA calling directly via MsgCall. A realm can therefore sell a service here but cannot buy one — and "can sell" is exactly as strong as the three obligations above, no stronger. A customer is structurally always an EOA, which is why the permissionless valves are always pullable.

ReleaseTimeout

func ReleaseTimeout(cur realm, orderID int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as ReleaseTimeout(cross(cur), ...).

ReleaseTimeout releases a delivered order to the provider once the acceptance window has lapsed. Permissionless: a customer who stops responding must not be able to strand a provider's completed work. This is deemed acceptance, and it is the only path by which escrow reaches a provider without the customer's explicit act.

Render

func Render(path string) string

RetireService

func RetireService(cur realm, id int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as RetireService(cross(cur), ...).

RetireService stops a service accepting NEW orders. Orders already in flight keep their own copies of provider, amount and fee, and run to their own terminal states unaffected. Provider only, and terminal — there is no un-retire, so a customer reading "active" cannot have it flicker underneath them.

ServiceDescription

func ServiceDescription(id int64) string

ServicesOf

func ServicesOf(a address) int64

SetFeeBps

func SetFeeBps(cur realm, bps int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeBps(cross(cur), ...).

SetFeeBps sets the protocol fee snapshotted into FUTURE services. Existing services, and every order already placed against them, keep the fee they were created under. Admin only; bounded by [0, MaxFeeBps].

SetFeeRecipient

func SetFeeRecipient(cur realm, next address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetFeeRecipient(cross(cur), ...).

SetFeeRecipient sets the address that may withdraw fees and sweep surplus. Admin only.

Surplus

func Surplus() int64

SweepDenom

func SweepDenom(cur realm, denom string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as SweepDenom(cross(cur), ...).

SweepDenom sends the surplus of a single denomination to the fee recipient. For ugnot only the excess over Liabilities() moves — which reserves live escrow as well as claimable balances and the fee pot — and other denoms move wholly. Only the fee recipient may call it.

TransferAdmin

func TransferAdmin(cur realm, next address)

Crossing function. Callable from a transaction via MsgCall, and from another realm as TransferAdmin(cross(cur), ...).

TransferAdmin stages a successor. Two-step: the successor must call AcceptAdmin, so a typo cannot orphan the realm. Admin only. Passing the zero address cancels a pending nomination; any other value must be a well-formed address, for parity with SetFeeRecipient. The two-step handover already made a malformed nominee harmless — it could never call AcceptAdmin — so this check rejects the typo at the point it is made rather than leaving it staged and readable as a real nomination.

UsersTotal

func UsersTotal() int64

WithdrawFees

func WithdrawFees(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as WithdrawFees(cross(cur), ...).

WithdrawFees sends the accrued fee pot to the fee recipient. Only the fee recipient may call it.


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importschain, chain/runtime, chain/runtime/unsafe, gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio, gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv
First-party dependenciesgno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio, gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source

Source filemainnet/r/service_market/service_market.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical