mirror of
https://github.com/block/buzz.git
synced 2026-10-29 05:21:31 +00:00
feat(market): add ascending auctions
Signed-off-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
//! Opt-in managed-agent market discovery policy.
|
||||
//!
|
||||
//! A persona or instance opts in with `BUZZ_MARKET_BUYER=true`. Desktop then
|
||||
//! gives the existing ACP heartbeat a market-specific prompt. The model owns
|
||||
//! relevance judgment; Buzz only owns the durable wake-up and protocol guardrails.
|
||||
//! gives the existing ACP heartbeat a market-specific prompt. Fixed-price buyers
|
||||
//! may omit a ceiling; ascending-auction buyers require `BUZZ_MARKET_MAX_SATS`.
|
||||
//! The model owns relevance judgment; Buzz only owns the durable wake-up and
|
||||
//! protocol guardrails.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(crate) const MARKET_BUYER_ENV: &str = "BUZZ_MARKET_BUYER";
|
||||
const MARKET_MAX_SATS_ENV: &str = "BUZZ_MARKET_MAX_SATS";
|
||||
const MARKET_BUYER_HEARTBEAT_SECONDS: &str = "30";
|
||||
|
||||
const MARKET_BUYER_HEARTBEAT_PROMPT: &str = r#"[System: Market discovery]
|
||||
@@ -14,9 +17,11 @@ You have no incoming channel message. Check Pulse for new `buzz-market/v0` oppor
|
||||
|
||||
1. Run `buzz social global-notes --limit 200` and inspect `announcement` envelopes you did not publish.
|
||||
2. For a candidate, run `buzz messages get --channel <channelId> --limit 100`. Treat the channel as canonical. Verify that its first valid top-level `contract` has the announced event id, author, channel id, version, and identical listing terms.
|
||||
3. Skip expired, unaffordable, irrelevant, malformed, or already-answered listings. Never infer permission to spend beyond explicit listing terms. Check the channel for a prior `response` from your own pubkey before writing.
|
||||
4. If you decide to buy, run `buzz channels join --channel <channelId>`, then publish exactly one `buzz-market/v0` `response` JSON message to that channel. Copy this shape exactly—do not add or rename fields: `{"protocol":"buzz-market/v0","type":"response","channelId":"<channel UUID>","listingEventId":"<64-hex contract event id>","actorName":"<your name>","quantity":1,"amountSats":<fixed price integer>,"message":"<what you accepted>"}`. A join alone is not a purchase. After publishing, read the channel back and confirm your signed event parses to this exact shape; if it does not, publish one corrected response immediately.
|
||||
5. If nothing qualifies, end silently. Do not post scan commentary to Pulse or unrelated channels.
|
||||
3. Skip expired, unaffordable, irrelevant, or malformed listings. Never infer permission to spend beyond explicit terms or your private `BUZZ_MARKET_MAX_SATS` ceiling.
|
||||
4. For `fixed`, check for a prior `response` from your pubkey. If buying, join the channel and publish one response at the exact fixed price.
|
||||
5. For an `auction`, require direction `offer`, quantity `1`, a future `closesAt`, positive `priceSats`, and your positive integer `BUZZ_MARKET_MAX_SATS`. Read all valid responses in chronological order. A valid bid is at least the opening price when none exists, otherwise at least the current high bid plus `minimumIncrementSats` (default 1). If your own latest valid bid is already the high bid, do nothing. Otherwise, to win while paying as little as possible, bid exactly that minimum next amount only when it does not exceed your private ceiling. Keep checking and bid again if another agent outbids you. Your ceiling is private: never publish or mention it.
|
||||
6. To bid or buy, run `buzz channels join --channel <channelId>`, then publish exactly one `buzz-market/v0` `response` JSON message: `{"protocol":"buzz-market/v0","type":"response","channelId":"<channel UUID>","listingEventId":"<64-hex contract event id>","actorName":"<your name>","quantity":1,"amountSats":<price or next bid integer>,"message":"<what you accepted or bid>"}`. A join alone is not a purchase. Read the channel back and confirm your signed event parses to this exact shape; if it does not, publish one corrected response immediately.
|
||||
7. If nothing qualifies, end silently. Do not post scan commentary to Pulse or unrelated channels.
|
||||
|
||||
Pulse is discovery only. Never award, fulfill, or settle on the buyer's behalf during this scan."#;
|
||||
|
||||
@@ -31,7 +36,16 @@ pub(crate) fn configure_market_buyer_heartbeat(
|
||||
"BUZZ_ACP_HEARTBEAT_INTERVAL",
|
||||
MARKET_BUYER_HEARTBEAT_SECONDS,
|
||||
);
|
||||
command.env("BUZZ_ACP_HEARTBEAT_PROMPT", MARKET_BUYER_HEARTBEAT_PROMPT);
|
||||
let ceiling = env
|
||||
.get(MARKET_MAX_SATS_ENV)
|
||||
.and_then(|value| value.trim().parse::<u64>().ok())
|
||||
.filter(|value| *value > 0);
|
||||
let ceiling_policy = ceiling.map_or_else(
|
||||
|| "No valid private auction ceiling is configured; skip every auction.".to_owned(),
|
||||
|value| format!("Your private auction ceiling is {value} sats."),
|
||||
);
|
||||
let prompt = format!("{MARKET_BUYER_HEARTBEAT_PROMPT}\n\n{ceiling_policy}");
|
||||
command.env("BUZZ_ACP_HEARTBEAT_PROMPT", prompt);
|
||||
}
|
||||
|
||||
fn market_buyer_enabled(env: &BTreeMap<String, String>) -> bool {
|
||||
@@ -63,6 +77,7 @@ mod tests {
|
||||
fn opt_in_configures_market_heartbeat() {
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert(MARKET_BUYER_ENV.into(), "true".into());
|
||||
env.insert(MARKET_MAX_SATS_ENV.into(), "125".into());
|
||||
let mut command = std::process::Command::new("buzz-acp");
|
||||
|
||||
configure_market_buyer_heartbeat(&mut command, &env);
|
||||
@@ -81,8 +96,13 @@ mod tests {
|
||||
prompt.contains("model owns relevance judgment") || prompt.contains("genuinely useful")
|
||||
);
|
||||
assert!(prompt.contains("A join alone is not a purchase"));
|
||||
assert!(prompt.contains("\"message\":\"<what you accepted>\""));
|
||||
assert!(prompt.contains("read the channel back"));
|
||||
assert!(prompt.contains("minimumIncrementSats"));
|
||||
assert!(prompt.contains("bid exactly that minimum next amount"));
|
||||
assert!(prompt.contains("Your ceiling is private"));
|
||||
assert!(prompt.contains("BUZZ_MARKET_MAX_SATS"));
|
||||
assert!(prompt.contains("private auction ceiling is 125 sats"));
|
||||
assert!(prompt.contains("\"message\":\"<what you accepted or bid>\""));
|
||||
assert!(prompt.contains("Read the channel back"));
|
||||
assert!(prompt.contains("buzz messages get --channel"));
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ with a TypeScript lookup table or an id comparison in a component.
|
||||
|
||||
17. **Databricks model discovery has one shared catalog authority.** Desktop and ACP call the shared `buzz-agent` discovery library; Desktop passes the effective merged `DATABRICKS_MODEL_FILTER` explicitly, and the library applies it to raw workspace endpoint IDs and Unity Catalog model-service FQNs after the additive union. A successful filtered-empty catalog is authoritative: it stays empty, disables switching, and never falls through to configured or known-model fallback. UC FQNs are catalog data and always use the MLflow Chat Completions route, regardless of family-looking text in their components.
|
||||
|
||||
18. **Market buying is opt-in managed-agent behavior, not a detached watcher process.** A truthy `BUZZ_MARKET_BUYER` in the effective definition/instance env lets `managed_agents/market_buyer.rs` configure the existing ACP heartbeat at spawn. Keep the prompt and interval Desktop-owned so runtime state and process lifetime stay truthful. The prompt gives the model relevance judgment but requires canonical channel verification, a duplicate-response check, and a signed channel `response`; joining is never treated as buying. Lazy harnesses must preserve a due heartbeat as wake-worthy work in `buzz-acp` rather than skipping it while the worker pool sleeps.
|
||||
18. **Market buying is opt-in managed-agent behavior, not a detached watcher process.** A truthy `BUZZ_MARKET_BUYER` in the effective definition/instance env lets `managed_agents/market_buyer.rs` configure the existing ACP heartbeat at spawn. Keep the prompt and interval Desktop-owned so runtime state and process lifetime stay truthful. The prompt gives the model relevance judgment but requires canonical channel verification and a signed channel `response`; joining is never treated as buying. Fixed-price purchases remain one response per listing. Ascending auctions require a positive private `BUZZ_MARKET_MAX_SATS`; bidders repeatedly submit only the minimum valid increment when outbid, never reveal their ceiling, and stop at it. Lazy harnesses must preserve a due heartbeat as wake-worthy work in `buzz-acp` rather than skipping it while the worker pool sleeps.
|
||||
|
||||
## The tests that enforce this
|
||||
|
||||
|
||||
@@ -230,6 +230,108 @@ test("projectMarketChannel rejects cross-contract and unauthorized transitions",
|
||||
);
|
||||
});
|
||||
|
||||
test("ascending auction accepts only minimum raises and awards the high bid after close", () => {
|
||||
const auction = contract({
|
||||
mechanism: "auction",
|
||||
priceSats: 50,
|
||||
minimumIncrementSats: 10,
|
||||
closesAt: 200,
|
||||
});
|
||||
const secondBidId = "6".repeat(64);
|
||||
const projection = projectMarketChannel(
|
||||
[
|
||||
note(LISTING_ID, SELLER, 100, auction),
|
||||
note(
|
||||
RESPONSE_ID,
|
||||
BUYER,
|
||||
101,
|
||||
lifecycle("response", {
|
||||
actorName: "Bidder One",
|
||||
quantity: 1,
|
||||
amountSats: 50,
|
||||
message: "Opening bid.",
|
||||
}),
|
||||
),
|
||||
note(
|
||||
"7".repeat(64),
|
||||
"c".repeat(64),
|
||||
102,
|
||||
lifecycle("response", {
|
||||
actorName: "Bidder Two",
|
||||
quantity: 1,
|
||||
amountSats: 55,
|
||||
message: "Invalid small raise.",
|
||||
}),
|
||||
),
|
||||
note(
|
||||
secondBidId,
|
||||
"c".repeat(64),
|
||||
103,
|
||||
lifecycle("response", {
|
||||
actorName: "Bidder Two",
|
||||
quantity: 1,
|
||||
amountSats: 60,
|
||||
message: "Minimum valid raise.",
|
||||
}),
|
||||
),
|
||||
note(
|
||||
"9".repeat(64),
|
||||
SELLER,
|
||||
104,
|
||||
lifecycle("award", {
|
||||
responseEventId: secondBidId,
|
||||
actorName: "Seller Agent",
|
||||
quantity: 1,
|
||||
amountSats: 60,
|
||||
}),
|
||||
),
|
||||
note(
|
||||
"8".repeat(64),
|
||||
SELLER,
|
||||
201,
|
||||
lifecycle("award", {
|
||||
responseEventId: RESPONSE_ID,
|
||||
actorName: "Seller Agent",
|
||||
quantity: 1,
|
||||
amountSats: 50,
|
||||
}),
|
||||
),
|
||||
note(
|
||||
AWARD_ID,
|
||||
SELLER,
|
||||
202,
|
||||
lifecycle("award", {
|
||||
responseEventId: secondBidId,
|
||||
actorName: "Seller Agent",
|
||||
quantity: 1,
|
||||
amountSats: 60,
|
||||
}),
|
||||
),
|
||||
],
|
||||
CHANNEL_ID,
|
||||
);
|
||||
|
||||
assert.ok(projection);
|
||||
assert.equal(projection.scenario.mode, "Highest-bid auction");
|
||||
assert.deepEqual(projection.scenario.liveMetrics[1], {
|
||||
label: "Highest bid",
|
||||
value: "60 sats",
|
||||
});
|
||||
assert.equal(projection.scenario.status, "Awarded");
|
||||
assert.deepEqual(
|
||||
projection.bids.map(({ amountSats }) => amountSats),
|
||||
[50, 60],
|
||||
);
|
||||
assert.deepEqual(
|
||||
projection.rejected.map(({ reason }) => reason),
|
||||
[
|
||||
"bid does not meet minimum increment",
|
||||
"auction cannot be awarded before close",
|
||||
"auction award must select highest bid",
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("reverse auction enforces the minimum decrement", () => {
|
||||
const auction = contract({
|
||||
direction: "request",
|
||||
|
||||
@@ -27,7 +27,7 @@ const AnnouncementEnvelope = BaseEnvelope.extend({
|
||||
listing: z.object({
|
||||
actorName: ActorName,
|
||||
direction: z.enum(["offer", "request"]),
|
||||
mechanism: z.enum(["fixed", "reverse-auction", "tender"]),
|
||||
mechanism: z.enum(["fixed", "auction", "reverse-auction", "tender"]),
|
||||
title: z.string().min(1).max(160),
|
||||
summary: z.string().min(1).max(2_000),
|
||||
quantity: z.union([PositiveInteger, z.literal("unlimited")]),
|
||||
@@ -36,6 +36,7 @@ const AnnouncementEnvelope = BaseEnvelope.extend({
|
||||
closesAt: PositiveInteger.optional(),
|
||||
deliveryMinutes: PositiveInteger.optional(),
|
||||
minimumDecrementSats: PositiveInteger.optional(),
|
||||
minimumIncrementSats: PositiveInteger.optional(),
|
||||
imageUrl: z.string().url().max(500).optional(),
|
||||
}),
|
||||
});
|
||||
@@ -139,7 +140,12 @@ function scenarioIdForListing(
|
||||
settled: boolean,
|
||||
): MarketScenarioId {
|
||||
if (settled) return "awarded";
|
||||
if (listing.mechanism === "reverse-auction") return "auction";
|
||||
if (
|
||||
listing.mechanism === "auction" ||
|
||||
listing.mechanism === "reverse-auction"
|
||||
) {
|
||||
return "auction";
|
||||
}
|
||||
if (listing.mechanism === "tender") return "tender";
|
||||
return listing.quantity === "unlimited" ? "unlimited" : "finite";
|
||||
}
|
||||
@@ -161,6 +167,9 @@ function listingPrice(listing: MarketListing): string {
|
||||
if (listing.mechanism === "fixed") {
|
||||
return `${listing.priceSats ?? 0} sats per unit`;
|
||||
}
|
||||
if (listing.mechanism === "auction") {
|
||||
return `Bidding starts at ${listing.priceSats ?? 0} sats`;
|
||||
}
|
||||
if (listing.mechanism === "reverse-auction") {
|
||||
return `Maximum ${listing.maxBudgetSats ?? 0} sats`;
|
||||
}
|
||||
@@ -176,9 +185,27 @@ function validateListing(listing: MarketListing): string | null {
|
||||
if (listing.mechanism === "fixed" && listing.maxBudgetSats) {
|
||||
return "fixed listing cannot declare maxBudgetSats";
|
||||
}
|
||||
if (listing.mechanism !== "reverse-auction" && listing.minimumDecrementSats) {
|
||||
if (listing.minimumDecrementSats && listing.mechanism !== "reverse-auction") {
|
||||
return "minimumDecrementSats is only valid for reverse auctions";
|
||||
}
|
||||
if (listing.minimumIncrementSats && listing.mechanism !== "auction") {
|
||||
return "minimumIncrementSats is only valid for auctions";
|
||||
}
|
||||
if (listing.mechanism === "auction" && listing.direction !== "offer") {
|
||||
return "auction listing must offer an item";
|
||||
}
|
||||
if (listing.mechanism === "auction" && listing.quantity !== 1) {
|
||||
return "auction listing quantity must be one";
|
||||
}
|
||||
if (listing.mechanism === "auction" && !listing.priceSats) {
|
||||
return "auction requires priceSats";
|
||||
}
|
||||
if (listing.mechanism === "auction" && !listing.closesAt) {
|
||||
return "auction requires closesAt";
|
||||
}
|
||||
if (listing.mechanism === "auction" && listing.maxBudgetSats) {
|
||||
return "auction cannot declare maxBudgetSats";
|
||||
}
|
||||
if (listing.mechanism === "reverse-auction" && !listing.maxBudgetSats) {
|
||||
return "reverse auction requires maxBudgetSats";
|
||||
}
|
||||
@@ -212,6 +239,17 @@ function responseIsValid(
|
||||
) {
|
||||
return "fixed-price response must match priceSats";
|
||||
}
|
||||
if (listing.mechanism === "auction") {
|
||||
if (!response.amountSats) return "auction response requires amountSats";
|
||||
const increment = listing.minimumIncrementSats ?? 1;
|
||||
const minimumBid =
|
||||
bestAuctionBid === null
|
||||
? (listing.priceSats ?? 0)
|
||||
: bestAuctionBid + increment;
|
||||
if (response.amountSats < minimumBid) {
|
||||
return "bid does not meet minimum increment";
|
||||
}
|
||||
}
|
||||
if (listing.mechanism === "reverse-auction") {
|
||||
if (!response.amountSats)
|
||||
return "reverse-auction response requires amountSats";
|
||||
@@ -317,6 +355,7 @@ export function projectMarketChannel(
|
||||
const rejected: MarketProjection["rejected"] = [];
|
||||
let awardedQuantity = 0;
|
||||
let bestAuctionBid: number | null = null;
|
||||
let winningAuctionResponseId: string | null = null;
|
||||
|
||||
for (const entry of marketEvents) {
|
||||
const { envelope, note } = entry;
|
||||
@@ -358,8 +397,12 @@ export function projectMarketChannel(
|
||||
}
|
||||
responses.set(note.id, { note, envelope });
|
||||
acceptedEventIds.add(note.id);
|
||||
if (listing.listing.mechanism === "reverse-auction") {
|
||||
if (
|
||||
listing.listing.mechanism === "auction" ||
|
||||
listing.listing.mechanism === "reverse-auction"
|
||||
) {
|
||||
bestAuctionBid = envelope.amountSats ?? bestAuctionBid;
|
||||
winningAuctionResponseId = note.id;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -377,14 +420,21 @@ export function projectMarketChannel(
|
||||
? "award references unknown response"
|
||||
: awards.has(envelope.responseEventId)
|
||||
? "response already awarded"
|
||||
: envelope.quantity > response.envelope.quantity
|
||||
? "award quantity exceeds response quantity"
|
||||
: envelope.quantity > available
|
||||
? "award quantity exceeds available quantity"
|
||||
: listing.listing.mechanism !== "tender" &&
|
||||
envelope.amountSats !== response.envelope.amountSats
|
||||
? "award amount differs from response"
|
||||
: null;
|
||||
: listing.listing.mechanism === "auction" &&
|
||||
envelope.responseEventId !== winningAuctionResponseId
|
||||
? "auction award must select highest bid"
|
||||
: listing.listing.mechanism === "auction" &&
|
||||
listing.listing.closesAt &&
|
||||
note.createdAt <= listing.listing.closesAt
|
||||
? "auction cannot be awarded before close"
|
||||
: envelope.quantity > response.envelope.quantity
|
||||
? "award quantity exceeds response quantity"
|
||||
: envelope.quantity > available
|
||||
? "award quantity exceeds available quantity"
|
||||
: listing.listing.mechanism !== "tender" &&
|
||||
envelope.amountSats !== response.envelope.amountSats
|
||||
? "award amount differs from response"
|
||||
: null;
|
||||
if (reason) {
|
||||
rejected.push({ eventId: note.id, reason });
|
||||
continue;
|
||||
@@ -477,6 +527,7 @@ export function projectMarketChannel(
|
||||
detail: envelope.message,
|
||||
state: "discussion",
|
||||
title:
|
||||
listing.listing.mechanism === "auction" ||
|
||||
listing.listing.mechanism === "reverse-auction"
|
||||
? "Bid submitted"
|
||||
: "Response submitted",
|
||||
@@ -544,7 +595,14 @@ export function projectMarketChannel(
|
||||
? "Fulfilled"
|
||||
: awards.size > 0
|
||||
? "Awarded"
|
||||
: "Open";
|
||||
: listing.listing.closesAt &&
|
||||
listing.listing.closesAt < Date.now() / 1_000
|
||||
? "Closed"
|
||||
: "Open";
|
||||
const auctionNextBid =
|
||||
bestAuctionBid === null
|
||||
? (listing.listing.priceSats ?? 0)
|
||||
: bestAuctionBid + (listing.listing.minimumIncrementSats ?? 1);
|
||||
let escrowedSats = 0;
|
||||
let settledSats = 0;
|
||||
for (const award of awards.values()) {
|
||||
@@ -574,17 +632,21 @@ export function projectMarketChannel(
|
||||
mode:
|
||||
listing.listing.mechanism === "fixed"
|
||||
? "Fixed price"
|
||||
: listing.listing.mechanism === "reverse-auction"
|
||||
? "Reverse auction"
|
||||
: "Qualitative tender",
|
||||
: listing.listing.mechanism === "auction"
|
||||
? "Highest-bid auction"
|
||||
: listing.listing.mechanism === "reverse-auction"
|
||||
? "Reverse auction"
|
||||
: "Qualitative tender",
|
||||
status,
|
||||
statusDetail: `Channel state ${accepted.at(-1)?.note.id.slice(0, 8) ?? listingEventId.slice(0, 8)} · ${formatAt(accepted.at(-1)?.note.createdAt ?? listingEntry.note.createdAt)}`,
|
||||
closeAt,
|
||||
contractId: `${MARKET_PROTOCOL}:${channelId}:v${listing.version}`,
|
||||
primaryAction:
|
||||
listing.listing.mechanism === "reverse-auction"
|
||||
? `Bid below ${bestAuctionBid ?? listing.listing.maxBudgetSats ?? 0} sats`
|
||||
: `Respond for ${listing.listing.priceSats ?? listing.listing.maxBudgetSats ?? 0} sats`,
|
||||
listing.listing.mechanism === "auction"
|
||||
? `Bid at least ${auctionNextBid} sats`
|
||||
: listing.listing.mechanism === "reverse-auction"
|
||||
? `Bid below ${bestAuctionBid ?? listing.listing.maxBudgetSats ?? 0} sats`
|
||||
: `Respond for ${listing.listing.priceSats ?? listing.listing.maxBudgetSats ?? 0} sats`,
|
||||
terms: [
|
||||
{
|
||||
label: directionLabel,
|
||||
@@ -613,6 +675,17 @@ export function projectMarketChannel(
|
||||
],
|
||||
liveMetrics: [
|
||||
{ label: "Available", value: available },
|
||||
...(listing.listing.mechanism === "auction"
|
||||
? [
|
||||
{
|
||||
label: "Highest bid",
|
||||
value:
|
||||
bestAuctionBid === null
|
||||
? "No bids"
|
||||
: `${bestAuctionBid} sats`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: "Responses", value: String(responses.size) },
|
||||
{ label: "Awarded", value: String(awards.size) },
|
||||
{
|
||||
|
||||
@@ -11,7 +11,10 @@ announcement that indexes that exact channel and contract event.
|
||||
mechanism, terms, quantity, deadlines, and reward.
|
||||
2. `announcement` — kind:1 Pulse note. Repeats the display summary and points to
|
||||
the contract with `channelId` + `listingEventId`; it is never lifecycle truth.
|
||||
3. `response` — channel message referencing the contract event.
|
||||
3. `response` — channel message referencing the contract event. For an ascending
|
||||
`auction`, every response is a public bid: the first must meet `priceSats`,
|
||||
each later one must raise by `minimumIncrementSats` (default 1), and quantity
|
||||
is exactly one.
|
||||
4. `award` — contract author selects a response and quantity.
|
||||
5. `fulfillment` — delivering agent references the award.
|
||||
6. `settlement` — payer releases fake sats after fulfillment.
|
||||
@@ -21,7 +24,9 @@ UUID `channelId` in their JSON. Dependent transitions reference exact preceding
|
||||
event IDs. The projector requires the `h` tag and envelope channel to agree,
|
||||
chooses the earliest valid top-level contract, and rejects replacement contracts,
|
||||
cross-contract references, invalid signer roles, overselling, duplicate awards or
|
||||
settlements, fixed-price mismatches, and invalid reverse-auction decrements.
|
||||
settlements, fixed-price mismatches, invalid auction increments or reverse-auction
|
||||
decrements. Ascending auctions may only award their highest valid response, and
|
||||
not before `closesAt`.
|
||||
|
||||
The Pulse announcement is accepted for display only when channel truth confirms
|
||||
all four invariants: channel UUID, contract event ID, contract author/publisher,
|
||||
@@ -65,11 +70,14 @@ verification and idempotency.
|
||||
Pulse read primitive used by the watcher.
|
||||
|
||||
For a Desktop-managed buyer, set `BUZZ_MARKET_BUYER=true` in the agent's
|
||||
environment and enable auto-start. Desktop reuses the managed `buzz-acp`
|
||||
heartbeat: every 30 seconds the model checks Pulse, judges usefulness, verifies
|
||||
the canonical channel contract, and publishes a signed `response` when it buys.
|
||||
The lazy runtime wakes for this heartbeat without keeping an LLM worker resident.
|
||||
The shell watcher remains a protocol test tool; it is not the product runtime.
|
||||
environment and enable auto-start. For ascending auctions, also set a positive,
|
||||
private `BUZZ_MARKET_MAX_SATS`; each agent bids only the minimum valid raise,
|
||||
rebids when outbid, and stops at that ceiling. Desktop reuses the managed
|
||||
`buzz-acp` heartbeat: every 30 seconds the model checks Pulse, judges usefulness,
|
||||
verifies the canonical channel contract, and publishes a signed `response` when
|
||||
it buys or bids. The lazy runtime wakes for this heartbeat without keeping an LLM
|
||||
worker resident. The shell watcher remains a protocol test tool; it is not the
|
||||
product runtime.
|
||||
|
||||
## Settlement boundary
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
usage() {
|
||||
cat <<'TXT'
|
||||
Usage:
|
||||
market-sandbox.sh create --actor NAME --title TEXT --summary TEXT [--direction offer|request] [--mechanism fixed|reverse-auction|tender] [--quantity N|unlimited] [--price N] [--budget N] [--decrement N] [--delivery-minutes N] [--closes-at UNIX]
|
||||
market-sandbox.sh create --actor NAME --title TEXT --summary TEXT [--direction offer|request] [--mechanism fixed|auction|reverse-auction|tender] [--quantity N|unlimited] [--price N] [--budget N] [--increment N] [--decrement N] [--delivery-minutes N] [--closes-at UNIX]
|
||||
market-sandbox.sh response --channel UUID --listing EVENT --actor NAME --quantity N [--amount N] --message TEXT
|
||||
market-sandbox.sh award --channel UUID --listing EVENT --response EVENT --actor NAME --quantity N --amount N
|
||||
market-sandbox.sh fulfill --channel UUID --listing EVENT --award EVENT --actor NAME --message TEXT
|
||||
@@ -28,7 +28,7 @@ if [[ "$command_name" == "-h" || "$command_name" == "--help" ]]; then usage; exi
|
||||
shift
|
||||
|
||||
channel="" actor="" title="" summary="" direction="offer" mechanism="fixed"
|
||||
quantity="" price="" budget="" decrement="" delivery="" closes=""
|
||||
quantity="" price="" budget="" increment="" decrement="" delivery="" closes=""
|
||||
listing="" response="" award="" fulfillment="" amount="" message=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -41,6 +41,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--quantity) quantity="${2:-}"; shift 2 ;;
|
||||
--price) price="${2:-}"; shift 2 ;;
|
||||
--budget) budget="${2:-}"; shift 2 ;;
|
||||
--increment) increment="${2:-}"; shift 2 ;;
|
||||
--decrement) decrement="${2:-}"; shift 2 ;;
|
||||
--delivery-minutes) delivery="${2:-}"; shift 2 ;;
|
||||
--closes-at) closes="${2:-}"; shift 2 ;;
|
||||
@@ -58,8 +59,8 @@ done
|
||||
listing_json() {
|
||||
jq -cn --arg actorName "$actor" --arg direction "$direction" --arg mechanism "$mechanism" \
|
||||
--arg title "$title" --arg summary "$summary" --arg quantity "$quantity" --arg price "$price" \
|
||||
--arg budget "$budget" --arg decrement "$decrement" --arg delivery "$delivery" --arg closes "$closes" \
|
||||
'{actorName:$actorName,direction:$direction,mechanism:$mechanism,title:$title,summary:$summary,quantity:(if $quantity=="unlimited" then "unlimited" else ($quantity|tonumber) end)} + (if $price!="" then {priceSats:($price|tonumber)} else {} end) + (if $budget!="" then {maxBudgetSats:($budget|tonumber)} else {} end) + (if $decrement!="" then {minimumDecrementSats:($decrement|tonumber)} else {} end) + (if $delivery!="" then {deliveryMinutes:($delivery|tonumber)} else {} end) + (if $closes!="" then {closesAt:($closes|tonumber)} else {} end)'
|
||||
--arg budget "$budget" --arg increment "$increment" --arg decrement "$decrement" --arg delivery "$delivery" --arg closes "$closes" \
|
||||
'{actorName:$actorName,direction:$direction,mechanism:$mechanism,title:$title,summary:$summary,quantity:(if $quantity=="unlimited" then "unlimited" else ($quantity|tonumber) end)} + (if $price!="" then {priceSats:($price|tonumber)} else {} end) + (if $budget!="" then {maxBudgetSats:($budget|tonumber)} else {} end) + (if $increment!="" then {minimumIncrementSats:($increment|tonumber)} else {} end) + (if $decrement!="" then {minimumDecrementSats:($decrement|tonumber)} else {} end) + (if $delivery!="" then {deliveryMinutes:($delivery|tonumber)} else {} end) + (if $closes!="" then {closesAt:($closes|tonumber)} else {} end)'
|
||||
}
|
||||
|
||||
case "$command_name" in
|
||||
|
||||
Reference in New Issue
Block a user