Lock web catalog to fixed SKUs and enforce solo/workspace entitlement gates

This commit is contained in:
Joshua 2026-02-18 13:13:59 -08:00
parent 889db8ea3b
commit 5a857f5554
17 changed files with 1610 additions and 107 deletions

View File

@ -12,9 +12,9 @@ SECRET_API_QUOTE_TTL_SECONDS=900
SECRET_API_DOMAIN_NAME=EDUT Designation SECRET_API_DOMAIN_NAME=EDUT Designation
SECRET_API_VERIFYING_CONTRACT=0x0000000000000000000000000000000000000000 SECRET_API_VERIFYING_CONTRACT=0x0000000000000000000000000000000000000000
SECRET_API_MEMBERSHIP_CONTRACT=0x0000000000000000000000000000000000000000 SECRET_API_MEMBERSHIP_CONTRACT=0x0000000000000000000000000000000000000000
SECRET_API_MINT_CURRENCY=ETH SECRET_API_MINT_CURRENCY=USDC
SECRET_API_MINT_AMOUNT_ATOMIC=5000000000000000 SECRET_API_MINT_AMOUNT_ATOMIC=100000000
SECRET_API_MINT_DECIMALS=18 SECRET_API_MINT_DECIMALS=6
SECRET_API_INSTALL_TOKEN_TTL_SECONDS=900 SECRET_API_INSTALL_TOKEN_TTL_SECONDS=900
SECRET_API_LEASE_TTL_SECONDS=3600 SECRET_API_LEASE_TTL_SECONDS=3600

View File

@ -86,9 +86,9 @@ Company-first sponsor path is also supported:
- `SECRET_API_DOMAIN_NAME` - `SECRET_API_DOMAIN_NAME`
- `SECRET_API_VERIFYING_CONTRACT` - `SECRET_API_VERIFYING_CONTRACT`
- `SECRET_API_MEMBERSHIP_CONTRACT` - `SECRET_API_MEMBERSHIP_CONTRACT`
- `SECRET_API_MINT_CURRENCY` (default `ETH`) - `SECRET_API_MINT_CURRENCY` (default `USDC`)
- `SECRET_API_MINT_AMOUNT_ATOMIC` (default `5000000000000000`) - `SECRET_API_MINT_AMOUNT_ATOMIC` (default `100000000`)
- `SECRET_API_MINT_DECIMALS` (default `18`) - `SECRET_API_MINT_DECIMALS` (default `6`)
### Governance install ### Governance install

View File

@ -37,6 +37,11 @@ func (a *app) routes() http.Handler {
mux.HandleFunc("/secret/membership/quote", a.withCORS(a.handleMembershipQuote)) mux.HandleFunc("/secret/membership/quote", a.withCORS(a.handleMembershipQuote))
mux.HandleFunc("/secret/membership/confirm", a.withCORS(a.handleMembershipConfirm)) mux.HandleFunc("/secret/membership/confirm", a.withCORS(a.handleMembershipConfirm))
mux.HandleFunc("/secret/membership/status", a.withCORS(a.handleMembershipStatus)) mux.HandleFunc("/secret/membership/status", a.withCORS(a.handleMembershipStatus))
mux.HandleFunc("/marketplace/offers", a.withCORS(a.handleMarketplaceOffers))
mux.HandleFunc("/marketplace/offers/", a.withCORS(a.handleMarketplaceOfferByID))
mux.HandleFunc("/marketplace/checkout/quote", a.withCORS(a.handleMarketplaceCheckoutQuote))
mux.HandleFunc("/marketplace/checkout/confirm", a.withCORS(a.handleMarketplaceCheckoutConfirm))
mux.HandleFunc("/marketplace/entitlements", a.withCORS(a.handleMarketplaceEntitlements))
mux.HandleFunc("/governance/install/token", a.withCORS(a.handleGovernanceInstallToken)) mux.HandleFunc("/governance/install/token", a.withCORS(a.handleGovernanceInstallToken))
mux.HandleFunc("/governance/install/confirm", a.withCORS(a.handleGovernanceInstallConfirm)) mux.HandleFunc("/governance/install/confirm", a.withCORS(a.handleGovernanceInstallConfirm))
mux.HandleFunc("/governance/install/status", a.withCORS(a.handleGovernanceInstallStatus)) mux.HandleFunc("/governance/install/status", a.withCORS(a.handleGovernanceInstallStatus))
@ -286,6 +291,15 @@ func (a *app) handleMembershipQuote(w http.ResponseWriter, r *http.Request) {
writeErrorCode(w, http.StatusForbidden, "sponsor_not_authorized", "sponsor entitlement is not active") writeErrorCode(w, http.StatusForbidden, "sponsor_not_authorized", "sponsor entitlement is not active")
return return
} }
hasWorkspaceCore, entErr := a.store.hasActiveEntitlement(r.Context(), payerAddress, offerIDWorkspaceCore, sponsorOrgRoot)
if entErr != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to validate sponsor entitlement")
return
}
if !hasWorkspaceCore {
writeErrorCode(w, http.StatusForbidden, "sponsor_not_authorized", "workspace core entitlement required for sponsored member onboarding")
return
}
sponsorshipMode = "sponsored_company" sponsorshipMode = "sponsored_company"
} else { } else {
writeError(w, http.StatusForbidden, "distinct payer requires ownership proof") writeError(w, http.StatusForbidden, "distinct payer requires ownership proof")
@ -842,6 +856,20 @@ func (a *app) handleGovernanceOfflineRenew(w http.ResponseWriter, r *http.Reques
writeErrorCode(w, http.StatusForbidden, "entitlement_inactive", "entitlement inactive") writeErrorCode(w, http.StatusForbidden, "entitlement_inactive", "entitlement inactive")
return return
} }
isSoloRoot := strings.EqualFold(strings.TrimSpace(principal.OrgRootID), defaultSoloOrgRootID(wallet))
requiredOfferID := offerIDWorkspaceSovereign
if isSoloRoot {
requiredOfferID = offerIDSoloCore
}
hasRequired, entErr := a.store.hasActiveEntitlement(r.Context(), wallet, requiredOfferID, principal.OrgRootID)
if entErr != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve sovereign entitlement")
return
}
if !hasRequired {
writeErrorCode(w, http.StatusForbidden, "sovereign_entitlement_required", "sovereign entitlement required for offline renew")
return
}
renewedUntil := time.Now().UTC().Add(a.cfg.OfflineRenewTTL) renewedUntil := time.Now().UTC().Add(a.cfg.OfflineRenewTTL)
principal.AccessClass = "sovereign" principal.AccessClass = "sovereign"
principal.AvailabilityState = "active" principal.AvailabilityState = "active"
@ -1267,8 +1295,8 @@ func (a *app) resolveOrCreatePrincipal(ctx context.Context, wallet, orgRootID, p
OrgRootID: orgRootID, OrgRootID: orgRootID,
PrincipalID: principalID, PrincipalID: principalID,
PrincipalRole: principalRole, PrincipalRole: principalRole,
EntitlementID: "gov_" + wallet[2:10], EntitlementID: "",
EntitlementStatus: "active", EntitlementStatus: "inactive",
AccessClass: "connected", AccessClass: "connected",
AvailabilityState: "active", AvailabilityState: "active",
LeaseExpiresAt: &leaseExpires, LeaseExpiresAt: &leaseExpires,

View File

@ -112,6 +112,23 @@ func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) {
}); err != nil { }); err != nil {
t.Fatalf("seed governance principal: %v", err) t.Fatalf("seed governance principal: %v", err)
} }
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:sponsor:workspace-core",
QuoteID: "seed-q-sponsor-workspace-core",
OfferID: offerIDWorkspaceCore,
Wallet: companyPayerAddr,
OrgRootID: "org_company_b",
PrincipalID: "principal_company_owner",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: now,
TxHash: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
}); err != nil {
t.Fatalf("seed sponsor workspace core entitlement: %v", err)
}
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{ intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr, Address: ownerAddr,
@ -227,6 +244,37 @@ func TestGovernanceInstallOwnerOnlyAndConfirm(t *testing.T) {
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil { if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed membership: %v", err) t.Fatalf("seed membership: %v", err)
} }
now := time.Now().UTC()
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:install:workspace-core",
QuoteID: "seed-q-install-workspace-core",
OfferID: offerIDWorkspaceCore,
Wallet: ownerAddr,
OrgRootID: "org_root_a",
PrincipalID: "principal_owner",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: cfg.GovernancePolicyHash,
IssuedAt: now,
TxHash: "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}); err != nil {
t.Fatalf("seed workspace core entitlement: %v", err)
}
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: ownerAddr,
OrgRootID: "org_root_a",
PrincipalID: "principal_owner",
PrincipalRole: "org_root_owner",
EntitlementID: "ent:install:workspace-core",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "active",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed owner principal: %v", err)
}
tokenRes := postJSONExpect[governanceInstallTokenResponse](t, a, "/governance/install/token", governanceInstallTokenRequest{ tokenRes := postJSONExpect[governanceInstallTokenResponse](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: ownerAddr, Wallet: ownerAddr,
@ -289,6 +337,54 @@ func TestGovernanceLeaseAndOfflineRenew(t *testing.T) {
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil { if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed membership: %v", err) t.Fatalf("seed membership: %v", err)
} }
now := time.Now().UTC()
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:lease:workspace-core",
QuoteID: "seed-q-lease-workspace-core",
OfferID: offerIDWorkspaceCore,
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: now,
TxHash: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
}); err != nil {
t.Fatalf("seed workspace core entitlement: %v", err)
}
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:lease:workspace-sovereign",
QuoteID: "seed-q-lease-workspace-sovereign",
OfferID: offerIDWorkspaceSovereign,
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "sovereign",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: now,
TxHash: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}); err != nil {
t.Fatalf("seed workspace sovereign entitlement: %v", err)
}
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
PrincipalRole: "org_root_owner",
EntitlementID: "ent:lease:workspace-core",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "active",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed owner principal: %v", err)
}
_ = postJSONExpect[governanceInstallTokenResponse](t, a, "/governance/install/token", governanceInstallTokenRequest{ _ = postJSONExpect[governanceInstallTokenResponse](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: ownerAddr, Wallet: ownerAddr,
@ -487,6 +583,176 @@ func TestMemberChannelSupportTicketOwnerOnly(t *testing.T) {
} }
} }
func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.a",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
if !quote.MembershipActivationIncluded {
t.Fatalf("expected bundled membership on first checkout: %+v", quote)
}
if len(quote.LineItems) < 2 {
t.Fatalf("expected license + membership line items: %+v", quote.LineItems)
}
confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
QuoteID: quote.QuoteID,
Wallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.a",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
TxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ChainID: cfg.ChainID,
}, http.StatusOK)
if confirm.Status != "entitlement_active" || confirm.EntitlementID == "" {
t.Fatalf("unexpected confirm response: %+v", confirm)
}
status := getJSONExpect[membershipStatusResponse](t, a, "/secret/membership/status?wallet="+ownerAddr, http.StatusOK)
if status.Status != "active" {
t.Fatalf("expected active membership after bundled checkout, got %+v", status)
}
entitlements := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr, http.StatusOK)
if len(entitlements.Entitlements) == 0 {
t.Fatalf("expected active entitlement after confirm")
}
if entitlements.Entitlements[0].OfferID != offerIDWorkspaceCore {
t.Fatalf("unexpected entitlement offer: %+v", entitlements.Entitlements[0])
}
}
func TestMarketplaceDistinctPayerRequiresOwnershipProof(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
payerKey := mustKey(t)
payerAddr := strings.ToLower(crypto.PubkeyToAddress(payerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
PayerWallet: payerAddr,
OfferID: offerIDWorkspaceCore,
}, http.StatusForbidden)
if code := errResp["code"]; code != "ownership_proof_required" {
t.Fatalf("expected ownership_proof_required, got %+v", errResp)
}
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
PayerWallet: payerAddr,
OfferID: offerIDWorkspaceCore,
OwnershipProof: "0x" + strings.Repeat("a", 130),
OrgRootID: "org.marketplace.ownerproof",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
if got := strings.ToLower(strings.TrimSpace(quote.PayerWallet)); got != payerAddr {
t.Fatalf("payer wallet mismatch: got=%s want=%s", got, payerAddr)
}
if quote.Decimals != 6 || quote.OfferID != offerIDWorkspaceCore || quote.AccessClass != "connected" {
t.Fatalf("unexpected quote response: %+v", quote)
}
}
func TestMarketplaceWorkspaceAddOnRequiresCoreEntitlement(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceAI,
OrgRootID: "org.marketplace.addon",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusForbidden)
if code := errResp["code"]; code != "prerequisite_required" {
t.Fatalf("expected prerequisite_required, got %+v", errResp)
}
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:seed:workspace-core",
QuoteID: "seed-q-workspace-core",
OfferID: offerIDWorkspaceCore,
Wallet: ownerAddr,
OrgRootID: "org.marketplace.addon",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: time.Now().UTC(),
TxHash: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}); err != nil {
t.Fatalf("seed workspace core entitlement: %v", err)
}
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceAI,
OrgRootID: "org.marketplace.addon",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
if quote.OfferID != offerIDWorkspaceAI || quote.AmountAtomic != marketplaceStandardOfferAtomic {
t.Fatalf("unexpected add-on quote response: %+v", quote)
}
}
func TestMarketplaceSoloCoreScopeValidation(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusOK)
expectedRoot := defaultSoloOrgRootID(ownerAddr)
if !strings.EqualFold(strings.TrimSpace(quote.OrgRootID), expectedRoot) {
t.Fatalf("expected auto solo org root %s, got %+v", expectedRoot, quote)
}
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
OrgRootID: "org.invalid.workspace",
PrincipalRole: "org_root_owner",
}, http.StatusBadRequest)
if code := errResp["code"]; code != "invalid_scope" {
t.Fatalf("expected invalid_scope, got %+v", errResp)
}
}
type tWalletIntentResponse struct { type tWalletIntentResponse struct {
IntentID string `json:"intent_id"` IntentID string `json:"intent_id"`
DesignationCode string `json:"designation_code"` DesignationCode string `json:"designation_code"`

View File

@ -51,9 +51,9 @@ func loadConfig() Config {
DomainName: env("SECRET_API_DOMAIN_NAME", "EDUT Designation"), DomainName: env("SECRET_API_DOMAIN_NAME", "EDUT Designation"),
VerifyingContract: strings.ToLower(env("SECRET_API_VERIFYING_CONTRACT", "0x0000000000000000000000000000000000000000")), VerifyingContract: strings.ToLower(env("SECRET_API_VERIFYING_CONTRACT", "0x0000000000000000000000000000000000000000")),
MembershipContract: strings.ToLower(env("SECRET_API_MEMBERSHIP_CONTRACT", "0x0000000000000000000000000000000000000000")), MembershipContract: strings.ToLower(env("SECRET_API_MEMBERSHIP_CONTRACT", "0x0000000000000000000000000000000000000000")),
MintCurrency: strings.ToUpper(env("SECRET_API_MINT_CURRENCY", "ETH")), MintCurrency: strings.ToUpper(env("SECRET_API_MINT_CURRENCY", "USDC")),
MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "5000000000000000"), MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "100000000"),
MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 18), MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 6),
ChainRPCURL: env("SECRET_API_CHAIN_RPC_URL", ""), ChainRPCURL: env("SECRET_API_CHAIN_RPC_URL", ""),
RequireOnchainTxVerify: envBool("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION", false), RequireOnchainTxVerify: envBool("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION", false),
GovernanceRuntimeVersion: env("SECRET_API_GOV_RUNTIME_VERSION", "0.1.0"), GovernanceRuntimeVersion: env("SECRET_API_GOV_RUNTIME_VERSION", "0.1.0"),

View File

@ -0,0 +1,714 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
const (
marketplaceMembershipActivationAtomic = "100000000" // 100.00 USDC (6 decimals)
marketplaceStandardOfferAtomic = "1000000000" // 1000.00 USDC (6 decimals)
offerIDSoloCore = "edut.solo.core"
offerIDWorkspaceCore = "edut.workspace.core"
offerIDWorkspaceAI = "edut.workspace.ai"
offerIDWorkspaceLane24 = "edut.workspace.lane24"
offerIDWorkspaceSovereign = "edut.workspace.sovereign"
)
func (a *app) marketplaceOffers() []marketplaceOffer {
offers := []marketplaceOffer{
{
OfferID: offerIDSoloCore,
IssuerID: "edut.firstparty",
Title: "EDUT Solo Core",
Summary: "Single-principal governance runtime for personal operations.",
Status: "active",
Pricing: marketplaceOfferPrice{
Currency: "USDC",
AmountAtomic: marketplaceStandardOfferAtomic,
Decimals: 6,
ChainID: a.cfg.ChainID,
},
Policies: marketplaceOfferPolicy{
MemberOnly: true,
WorkspaceBound: true,
Transferable: false,
InternalUseOnly: true,
MultiTenant: false,
EntitlementClass: "solo_core",
},
SortOrder: 10,
},
{
OfferID: offerIDWorkspaceCore,
IssuerID: "edut.firstparty",
Title: "EDUT Workspace Core",
Summary: "Org-bound deterministic governance runtime for team operations.",
Status: "active",
Pricing: marketplaceOfferPrice{
Currency: "USDC",
AmountAtomic: marketplaceStandardOfferAtomic,
Decimals: 6,
ChainID: a.cfg.ChainID,
},
Policies: marketplaceOfferPolicy{
MemberOnly: true,
WorkspaceBound: true,
Transferable: false,
InternalUseOnly: true,
MultiTenant: false,
EntitlementClass: "workspace_core",
},
SortOrder: 20,
},
{
OfferID: offerIDWorkspaceAI,
IssuerID: "edut.firstparty",
Title: "EDUT Workspace AI Layer",
Summary: "AI reasoning layer for governed workspace operations.",
Status: "active",
Pricing: marketplaceOfferPrice{
Currency: "USDC",
AmountAtomic: marketplaceStandardOfferAtomic,
Decimals: 6,
ChainID: a.cfg.ChainID,
},
Policies: marketplaceOfferPolicy{
MemberOnly: true,
WorkspaceBound: true,
Transferable: false,
InternalUseOnly: true,
MultiTenant: false,
EntitlementClass: "workspace_ai",
RequiresOffers: []string{offerIDWorkspaceCore},
},
SortOrder: 30,
},
{
OfferID: offerIDWorkspaceLane24,
IssuerID: "edut.firstparty",
Title: "EDUT Workspace 24h Lane",
Summary: "Autonomous execution lane capacity for workspace queue throughput.",
Status: "active",
Pricing: marketplaceOfferPrice{
Currency: "USDC",
AmountAtomic: marketplaceStandardOfferAtomic,
Decimals: 6,
ChainID: a.cfg.ChainID,
},
Policies: marketplaceOfferPolicy{
MemberOnly: true,
WorkspaceBound: true,
Transferable: false,
InternalUseOnly: true,
MultiTenant: false,
EntitlementClass: "workspace_lane24",
RequiresOffers: []string{offerIDWorkspaceCore},
},
SortOrder: 40,
},
{
OfferID: offerIDWorkspaceSovereign,
IssuerID: "edut.firstparty",
Title: "EDUT Workspace Sovereign Continuity",
Summary: "Workspace continuity profile for stronger local/offline operation.",
Status: "active",
Pricing: marketplaceOfferPrice{
Currency: "USDC",
AmountAtomic: marketplaceStandardOfferAtomic,
Decimals: 6,
ChainID: a.cfg.ChainID,
},
Policies: marketplaceOfferPolicy{
MemberOnly: true,
WorkspaceBound: true,
Transferable: false,
InternalUseOnly: true,
MultiTenant: false,
EntitlementClass: "workspace_sovereign",
RequiresOffers: []string{offerIDWorkspaceCore},
},
SortOrder: 50,
},
}
sort.SliceStable(offers, func(i, j int) bool {
return offers[i].SortOrder < offers[j].SortOrder
})
return offers
}
func (a *app) marketplaceOfferByID(offerID string) (marketplaceOffer, error) {
target := strings.TrimSpace(strings.ToLower(offerID))
for _, offer := range a.marketplaceOffers() {
if strings.EqualFold(strings.TrimSpace(offer.OfferID), target) {
return offer, nil
}
}
return marketplaceOffer{}, errNotFound
}
func isSoloOffer(offerID string) bool {
return strings.EqualFold(strings.TrimSpace(offerID), offerIDSoloCore)
}
func isWorkspaceOffer(offerID string) bool {
switch strings.ToLower(strings.TrimSpace(offerID)) {
case offerIDWorkspaceCore, offerIDWorkspaceAI, offerIDWorkspaceLane24, offerIDWorkspaceSovereign:
return true
default:
return false
}
}
func shouldBindPrincipalOnOffer(offerID string) bool {
switch strings.ToLower(strings.TrimSpace(offerID)) {
case offerIDSoloCore, offerIDWorkspaceCore:
return true
default:
return false
}
}
func defaultSoloOrgRootID(wallet string) string {
return "solo:" + strings.ToLower(strings.TrimSpace(wallet))
}
func (a *app) handleMarketplaceOffers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
writeJSON(w, http.StatusOK, marketplaceOffersResponse{Offers: a.marketplaceOffers()})
}
func (a *app) handleMarketplaceOfferByID(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
const prefix = "/marketplace/offers/"
offerID := strings.TrimSpace(strings.TrimPrefix(r.URL.Path, prefix))
if offerID == "" || strings.Contains(offerID, "/") {
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
return
}
offer, err := a.marketplaceOfferByID(offerID)
if err != nil {
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
return
}
writeJSON(w, http.StatusOK, offer)
}
func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req marketplaceCheckoutQuoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_request", "invalid request body")
return
}
wallet, err := normalizeAddress(req.Wallet)
if err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_wallet", err.Error())
return
}
offer, err := a.marketplaceOfferByID(req.OfferID)
if err != nil {
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
return
}
payerWallet := wallet
if strings.TrimSpace(req.PayerWallet) != "" {
payerWallet, err = normalizeAddress(req.PayerWallet)
if err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_payer_wallet", err.Error())
return
}
}
if !strings.EqualFold(wallet, payerWallet) {
if !isLikelyHexSignature(req.OwnershipProof) {
writeErrorCode(w, http.StatusForbidden, "ownership_proof_required", "distinct payer requires ownership proof")
return
}
}
orgRootID := strings.TrimSpace(req.OrgRootID)
principalID := strings.TrimSpace(req.PrincipalID)
if principalID == "" {
principalID = wallet
}
principalRole := strings.ToLower(strings.TrimSpace(req.PrincipalRole))
if principalRole == "" {
principalRole = "org_root_owner"
}
workspaceID := strings.TrimSpace(req.WorkspaceID)
if isSoloOffer(offer.OfferID) {
expectedSoloRoot := defaultSoloOrgRootID(wallet)
if orgRootID == "" {
orgRootID = expectedSoloRoot
}
if !strings.EqualFold(orgRootID, expectedSoloRoot) {
writeErrorCode(w, http.StatusBadRequest, "invalid_scope", "solo core must use wallet-scoped solo org root")
return
}
if principalRole != "org_root_owner" {
writeErrorCode(w, http.StatusForbidden, "role_insufficient", "solo core checkout requires org_root_owner role")
return
}
if workspaceID != "" {
writeErrorCode(w, http.StatusBadRequest, "invalid_scope", "solo core does not bind workspace_id")
return
}
}
if isWorkspaceOffer(offer.OfferID) {
if orgRootID == "" {
writeErrorCode(w, http.StatusBadRequest, "invalid_scope", "workspace offer requires org_root_id")
return
}
if principalRole != "org_root_owner" {
writeErrorCode(w, http.StatusForbidden, "role_insufficient", "workspace checkout requires org_root_owner role")
return
}
}
for _, requiredOfferID := range offer.Policies.RequiresOffers {
hasRequired, err := a.store.hasActiveEntitlement(r.Context(), wallet, requiredOfferID, orgRootID)
if err != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve prerequisite entitlements")
return
}
if !hasRequired {
writeErrorCode(w, http.StatusForbidden, "prerequisite_required", fmt.Sprintf("active entitlement required: %s", requiredOfferID))
return
}
}
includeMembership := true
if req.IncludeMembershipIfMissing != nil {
includeMembership = *req.IncludeMembershipIfMissing
}
membershipStatus, statusErr := a.resolveMembershipStatusForWallet(r, wallet)
if statusErr != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve membership status")
return
}
membershipIncluded := false
switch membershipStatus {
case "active":
// no-op
case "none", "unknown", "":
if includeMembership {
membershipIncluded = true
} else {
writeErrorCode(w, http.StatusForbidden, "membership_required", "active membership is required for checkout")
return
}
case "suspended", "revoked":
writeErrorCode(w, http.StatusForbidden, "membership_required", "active membership is required for checkout")
return
default:
if includeMembership {
membershipIncluded = true
} else {
writeErrorCode(w, http.StatusForbidden, "membership_required", "active membership is required for checkout")
return
}
}
licenseAmount := offer.Pricing.AmountAtomic
totalAmount := new(big.Int)
if _, ok := totalAmount.SetString(licenseAmount, 10); !ok {
writeErrorCode(w, http.StatusInternalServerError, "pricing_invalid", "offer pricing is invalid")
return
}
lineItems := []marketplaceQuoteLineItem{
{
Kind: "license",
Label: offer.Title,
Amount: formatAtomicAmount(licenseAmount, offer.Pricing.Decimals),
AmountAtomic: licenseAmount,
Decimals: offer.Pricing.Decimals,
Currency: offer.Pricing.Currency,
},
}
if membershipIncluded {
membershipAmount := new(big.Int)
if _, ok := membershipAmount.SetString(marketplaceMembershipActivationAtomic, 10); !ok {
writeErrorCode(w, http.StatusInternalServerError, "pricing_invalid", "membership pricing is invalid")
return
}
totalAmount = new(big.Int).Add(totalAmount, membershipAmount)
lineItems = append(lineItems, marketplaceQuoteLineItem{
Kind: "membership",
Label: "EDUT Membership Activation",
Amount: formatAtomicAmount(marketplaceMembershipActivationAtomic, offer.Pricing.Decimals),
AmountAtomic: marketplaceMembershipActivationAtomic,
Decimals: offer.Pricing.Decimals,
Currency: offer.Pricing.Currency,
})
}
lineItemsJSON, err := json.Marshal(lineItems)
if err != nil {
writeErrorCode(w, http.StatusInternalServerError, "encoding_failed", "failed to encode quote line items")
return
}
quoteIDRaw, err := randomHex(16)
if err != nil {
writeErrorCode(w, http.StatusInternalServerError, "quote_generation_failed", "failed to generate quote id")
return
}
now := time.Now().UTC()
expiresAt := now.Add(a.cfg.QuoteTTL)
accessClass := "connected"
if strings.EqualFold(strings.TrimSpace(offer.OfferID), offerIDWorkspaceSovereign) {
accessClass = "sovereign"
}
quote := marketplaceQuoteRecord{
QuoteID: "cq_" + quoteIDRaw,
Wallet: wallet,
PayerWallet: payerWallet,
OfferID: offer.OfferID,
OrgRootID: orgRootID,
PrincipalID: principalID,
PrincipalRole: principalRole,
WorkspaceID: workspaceID,
Currency: offer.Pricing.Currency,
AmountAtomic: licenseAmount,
TotalAmountAtomic: totalAmount.String(),
Decimals: offer.Pricing.Decimals,
MembershipIncluded: membershipIncluded,
LineItemsJSON: string(lineItemsJSON),
PolicyHash: a.cfg.GovernancePolicyHash,
AccessClass: accessClass,
AvailabilityState: "active",
CreatedAt: now,
ExpiresAt: expiresAt,
}
if err := a.store.putMarketplaceQuote(r.Context(), quote); err != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to persist quote")
return
}
writeJSON(w, http.StatusOK, marketplaceCheckoutQuoteResponse{
QuoteID: quote.QuoteID,
Wallet: quote.Wallet,
PayerWallet: quote.PayerWallet,
OfferID: quote.OfferID,
OrgRootID: quote.OrgRootID,
PrincipalID: quote.PrincipalID,
PrincipalRole: quote.PrincipalRole,
Currency: quote.Currency,
Amount: formatAtomicAmount(quote.AmountAtomic, quote.Decimals),
AmountAtomic: quote.AmountAtomic,
TotalAmount: formatAtomicAmount(quote.TotalAmountAtomic, quote.Decimals),
TotalAmountAtomic: quote.TotalAmountAtomic,
Decimals: quote.Decimals,
MembershipActivationIncluded: quote.MembershipIncluded,
LineItems: lineItems,
PolicyHash: quote.PolicyHash,
ExpiresAt: quote.ExpiresAt.Format(time.RFC3339Nano),
Tx: map[string]any{
"to": strings.ToLower(a.cfg.MembershipContract),
"value": "0x0",
"data": "0x",
},
AccessClass: quote.AccessClass,
AvailabilityState: quote.AvailabilityState,
})
}
func (a *app) handleMarketplaceCheckoutConfirm(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req marketplaceCheckoutConfirmRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_request", "invalid request body")
return
}
wallet, err := normalizeAddress(req.Wallet)
if err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_wallet", err.Error())
return
}
if req.ChainID != a.cfg.ChainID {
writeErrorCode(w, http.StatusBadRequest, "unsupported_chain_id", fmt.Sprintf("unsupported chain_id: %d", req.ChainID))
return
}
if !isTxHash(req.TxHash) {
writeErrorCode(w, http.StatusBadRequest, "invalid_tx_hash", "invalid tx hash")
return
}
quote, err := a.store.getMarketplaceQuote(r.Context(), strings.TrimSpace(req.QuoteID))
if err != nil {
if errors.Is(err, errNotFound) {
writeErrorCode(w, http.StatusNotFound, "quote_not_found", "quote not found")
return
}
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to load quote")
return
}
if time.Now().UTC().After(quote.ExpiresAt) {
writeErrorCode(w, http.StatusConflict, "quote_expired", "quote expired")
return
}
if !strings.EqualFold(quote.Wallet, wallet) ||
!strings.EqualFold(strings.TrimSpace(quote.OfferID), strings.TrimSpace(req.OfferID)) {
writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "quote context mismatch")
return
}
if strings.TrimSpace(req.OrgRootID) != "" && !strings.EqualFold(strings.TrimSpace(req.OrgRootID), quote.OrgRootID) {
writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "org_root_id mismatch")
return
}
if strings.TrimSpace(req.PrincipalID) != "" && !strings.EqualFold(strings.TrimSpace(req.PrincipalID), quote.PrincipalID) {
writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "principal_id mismatch")
return
}
if strings.TrimSpace(req.PrincipalRole) != "" && !strings.EqualFold(strings.TrimSpace(req.PrincipalRole), quote.PrincipalRole) {
writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "principal_role mismatch")
return
}
if strings.TrimSpace(req.WorkspaceID) != "" && !strings.EqualFold(strings.TrimSpace(req.WorkspaceID), quote.WorkspaceID) {
writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "workspace_id mismatch")
return
}
if quote.ConfirmedAt != nil {
if existing, existingErr := a.store.getMarketplaceEntitlementByQuote(r.Context(), quote.QuoteID); existingErr == nil {
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
Status: "entitlement_active",
EntitlementID: existing.EntitlementID,
OfferID: existing.OfferID,
OrgRootID: existing.OrgRootID,
PrincipalID: existing.PrincipalID,
PrincipalRole: existing.PrincipalRole,
Wallet: existing.Wallet,
TxHash: existing.TxHash,
PolicyHash: existing.PolicyHash,
ActivatedAt: existing.IssuedAt.Format(time.RFC3339Nano),
AccessClass: existing.AccessClass,
AvailabilityState: existing.AvailabilityState,
})
return
}
writeErrorCode(w, http.StatusConflict, "quote_already_confirmed", "quote already confirmed")
return
}
now := time.Now().UTC()
quote.ConfirmedAt = &now
quote.ConfirmedTxHash = strings.ToLower(strings.TrimSpace(req.TxHash))
if err := a.store.putMarketplaceQuote(r.Context(), quote); err != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to persist quote confirmation")
return
}
if quote.MembershipIncluded {
if err := a.ensureMembershipActiveForWallet(r.Context(), wallet, quote.ConfirmedTxHash); err != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to activate membership")
return
}
}
entitlementID := buildEntitlementID(a.cfg.ChainID, wallet)
ent := marketplaceEntitlementRecord{
EntitlementID: entitlementID,
QuoteID: quote.QuoteID,
OfferID: quote.OfferID,
Wallet: wallet,
PayerWallet: quote.PayerWallet,
OrgRootID: quote.OrgRootID,
PrincipalID: quote.PrincipalID,
PrincipalRole: quote.PrincipalRole,
WorkspaceID: quote.WorkspaceID,
State: "active",
AccessClass: quote.AccessClass,
AvailabilityState: quote.AvailabilityState,
PolicyHash: quote.PolicyHash,
IssuedAt: now,
TxHash: quote.ConfirmedTxHash,
}
if err := a.store.putMarketplaceEntitlement(r.Context(), ent); err != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to persist entitlement")
return
}
if shouldBindPrincipalOnOffer(quote.OfferID) {
principal, principalErr := a.resolveOrCreatePrincipal(r.Context(), wallet, quote.OrgRootID, quote.PrincipalID, quote.PrincipalRole)
if principalErr == nil {
principal.EntitlementID = ent.EntitlementID
principal.EntitlementStatus = "active"
principal.AccessClass = quote.AccessClass
principal.AvailabilityState = quote.AvailabilityState
principal.UpdatedAt = now
_ = a.store.putGovernancePrincipal(r.Context(), principal)
}
}
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
Status: "entitlement_active",
EntitlementID: ent.EntitlementID,
OfferID: ent.OfferID,
OrgRootID: ent.OrgRootID,
PrincipalID: ent.PrincipalID,
PrincipalRole: ent.PrincipalRole,
Wallet: ent.Wallet,
TxHash: ent.TxHash,
PolicyHash: ent.PolicyHash,
ActivatedAt: ent.IssuedAt.Format(time.RFC3339Nano),
AccessClass: ent.AccessClass,
AvailabilityState: ent.AvailabilityState,
})
}
func (a *app) handleMarketplaceEntitlements(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
wallet, err := normalizeAddress(strings.TrimSpace(r.URL.Query().Get("wallet")))
if err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_wallet", err.Error())
return
}
records, err := a.store.listMarketplaceEntitlementsByWallet(r.Context(), wallet)
if err != nil {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to list entitlements")
return
}
out := make([]marketplaceEntitlement, 0, len(records))
for _, rec := range records {
out = append(out, marketplaceEntitlement{
EntitlementID: rec.EntitlementID,
OfferID: rec.OfferID,
WalletAddress: rec.Wallet,
WorkspaceID: rec.WorkspaceID,
OrgRootID: rec.OrgRootID,
State: rec.State,
AccessClass: rec.AccessClass,
AvailabilityState: rec.AvailabilityState,
PolicyHash: rec.PolicyHash,
IssuedAt: rec.IssuedAt.Format(time.RFC3339Nano),
})
}
writeJSON(w, http.StatusOK, marketplaceEntitlementsResponse{Entitlements: out})
}
func (a *app) resolveMembershipStatusForWallet(r *http.Request, wallet string) (string, error) {
rec, err := a.store.getDesignationByAddress(r.Context(), wallet)
if err != nil {
if errors.Is(err, errNotFound) {
return "none", nil
}
return "unknown", err
}
status := strings.ToLower(strings.TrimSpace(rec.MembershipStatus))
if status == "" {
status = "none"
}
return status, nil
}
func (a *app) ensureMembershipActiveForWallet(ctx context.Context, wallet, txHash string) error {
rec, err := a.store.getDesignationByAddress(ctx, wallet)
if err != nil {
if !errors.Is(err, errNotFound) {
return err
}
now := time.Now().UTC()
intentID, err := randomHex(16)
if err != nil {
return err
}
nonce, err := randomHex(16)
if err != nil {
return err
}
code, displayToken, err := newDesignationCode()
if err != nil {
return err
}
rec = designationRecord{
Code: code,
DisplayToken: displayToken,
IntentID: intentID,
Nonce: nonce,
Origin: "edut-launcher",
Locale: "en",
Address: wallet,
ChainID: a.cfg.ChainID,
IssuedAt: now,
ExpiresAt: now.Add(a.cfg.IntentTTL),
VerifiedAt: &now,
MembershipStatus: "none",
}
}
now := time.Now().UTC()
rec.MembershipStatus = "active"
rec.MembershipTxHash = strings.ToLower(strings.TrimSpace(txHash))
rec.ActivatedAt = &now
return a.store.putDesignation(ctx, rec)
}
func buildEntitlementID(chainID int64, wallet string) string {
token, _ := randomHex(4)
return fmt.Sprintf("ent:%d:%s:%s", chainID, wallet, token)
}
func formatAtomicAmount(amountAtomic string, decimals int) string {
amountAtomic = strings.TrimSpace(amountAtomic)
if amountAtomic == "" {
return "0"
}
n := new(big.Int)
if _, ok := n.SetString(amountAtomic, 10); !ok {
return "0"
}
sign := ""
if n.Sign() < 0 {
sign = "-"
n.Abs(n)
}
raw := n.String()
if decimals <= 0 {
return sign + raw
}
if len(raw) <= decimals {
raw = strings.Repeat("0", decimals-len(raw)+1) + raw
}
whole := raw[:len(raw)-decimals]
fraction := strings.TrimRight(raw[len(raw)-decimals:], "0")
if fraction == "" {
return sign + whole
}
return sign + whole + "." + fraction
}
func isLikelyHexSignature(value string) bool {
value = strings.TrimSpace(strings.ToLower(value))
if !strings.HasPrefix(value, "0x") || len(value) < 132 {
return false
}
_, err := strconv.ParseUint(value[2:18], 16, 64)
return err == nil
}

View File

@ -0,0 +1,166 @@
package main
import "time"
type marketplaceOffer struct {
OfferID string `json:"offer_id"`
IssuerID string `json:"issuer_id"`
Title string `json:"title"`
Summary string `json:"summary,omitempty"`
Status string `json:"status"`
Pricing marketplaceOfferPrice `json:"pricing"`
Policies marketplaceOfferPolicy `json:"policies"`
SortOrder int `json:"-"`
}
type marketplaceOfferPrice struct {
Currency string `json:"currency"`
AmountAtomic string `json:"amount_atomic"`
Decimals int `json:"decimals"`
ChainID int64 `json:"chain_id"`
}
type marketplaceOfferPolicy struct {
MemberOnly bool `json:"member_only"`
WorkspaceBound bool `json:"workspace_bound"`
Transferable bool `json:"transferable"`
InternalUseOnly bool `json:"internal_use_only"`
MultiTenant bool `json:"multi_tenant"`
EntitlementClass string `json:"entitlement_class,omitempty"`
RequiresOffers []string `json:"requires_offers,omitempty"`
}
type marketplaceOffersResponse struct {
Offers []marketplaceOffer `json:"offers"`
}
type marketplaceCheckoutQuoteRequest struct {
Wallet string `json:"wallet"`
PayerWallet string `json:"payer_wallet,omitempty"`
OfferID string `json:"offer_id"`
OrgRootID string `json:"org_root_id,omitempty"`
PrincipalID string `json:"principal_id,omitempty"`
PrincipalRole string `json:"principal_role,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
OwnershipProof string `json:"ownership_proof,omitempty"`
IncludeMembershipIfMissing *bool `json:"include_membership_if_missing,omitempty"`
}
type marketplaceQuoteLineItem struct {
Kind string `json:"kind"`
Label string `json:"label"`
Amount string `json:"amount"`
AmountAtomic string `json:"amount_atomic"`
Decimals int `json:"decimals"`
Currency string `json:"currency"`
}
type marketplaceCheckoutQuoteResponse struct {
QuoteID string `json:"quote_id"`
Wallet string `json:"wallet"`
PayerWallet string `json:"payer_wallet,omitempty"`
OfferID string `json:"offer_id"`
OrgRootID string `json:"org_root_id,omitempty"`
PrincipalID string `json:"principal_id,omitempty"`
PrincipalRole string `json:"principal_role,omitempty"`
Currency string `json:"currency"`
Amount string `json:"amount"`
AmountAtomic string `json:"amount_atomic"`
TotalAmount string `json:"total_amount"`
TotalAmountAtomic string `json:"total_amount_atomic"`
Decimals int `json:"decimals"`
MembershipActivationIncluded bool `json:"membership_activation_included"`
LineItems []marketplaceQuoteLineItem `json:"line_items"`
PolicyHash string `json:"policy_hash"`
ExpiresAt string `json:"expires_at"`
Tx map[string]any `json:"tx"`
AccessClass string `json:"access_class"`
AvailabilityState string `json:"availability_state"`
}
type marketplaceCheckoutConfirmRequest struct {
QuoteID string `json:"quote_id"`
Wallet string `json:"wallet"`
PayerWallet string `json:"payer_wallet,omitempty"`
OfferID string `json:"offer_id"`
OrgRootID string `json:"org_root_id,omitempty"`
PrincipalID string `json:"principal_id,omitempty"`
PrincipalRole string `json:"principal_role,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
TxHash string `json:"tx_hash"`
ChainID int64 `json:"chain_id"`
}
type marketplaceCheckoutConfirmResponse struct {
Status string `json:"status"`
EntitlementID string `json:"entitlement_id"`
OfferID string `json:"offer_id"`
OrgRootID string `json:"org_root_id,omitempty"`
PrincipalID string `json:"principal_id,omitempty"`
PrincipalRole string `json:"principal_role,omitempty"`
Wallet string `json:"wallet"`
TxHash string `json:"tx_hash"`
PolicyHash string `json:"policy_hash"`
ActivatedAt string `json:"activated_at"`
AccessClass string `json:"access_class"`
AvailabilityState string `json:"availability_state"`
}
type marketplaceEntitlement struct {
EntitlementID string `json:"entitlement_id"`
OfferID string `json:"offer_id"`
WalletAddress string `json:"wallet_address"`
WorkspaceID string `json:"workspace_id,omitempty"`
OrgRootID string `json:"org_root_id,omitempty"`
State string `json:"state"`
AccessClass string `json:"access_class"`
AvailabilityState string `json:"availability_state"`
PolicyHash string `json:"policy_hash"`
IssuedAt string `json:"issued_at"`
}
type marketplaceEntitlementsResponse struct {
Entitlements []marketplaceEntitlement `json:"entitlements"`
}
type marketplaceQuoteRecord struct {
QuoteID string
Wallet string
PayerWallet string
OfferID string
OrgRootID string
PrincipalID string
PrincipalRole string
WorkspaceID string
Currency string
AmountAtomic string
TotalAmountAtomic string
Decimals int
MembershipIncluded bool
LineItemsJSON string
PolicyHash string
AccessClass string
AvailabilityState string
CreatedAt time.Time
ExpiresAt time.Time
ConfirmedAt *time.Time
ConfirmedTxHash string
}
type marketplaceEntitlementRecord struct {
EntitlementID string
QuoteID string
OfferID string
Wallet string
PayerWallet string
OrgRootID string
PrincipalID string
PrincipalRole string
WorkspaceID string
State string
AccessClass string
AvailabilityState string
PolicyHash string
IssuedAt time.Time
TxHash string
}

View File

@ -79,6 +79,48 @@ func (s *store) migrate(ctx context.Context) error {
FOREIGN KEY(designation_code) REFERENCES designations(code) FOREIGN KEY(designation_code) REFERENCES designations(code)
);`, );`,
`CREATE INDEX IF NOT EXISTS idx_quotes_designation ON quotes(designation_code);`, `CREATE INDEX IF NOT EXISTS idx_quotes_designation ON quotes(designation_code);`,
`CREATE TABLE IF NOT EXISTS marketplace_quotes (
quote_id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
payer_wallet TEXT,
offer_id TEXT NOT NULL,
org_root_id TEXT,
principal_id TEXT,
principal_role TEXT,
workspace_id TEXT,
currency TEXT NOT NULL,
amount_atomic TEXT NOT NULL,
total_amount_atomic TEXT NOT NULL,
decimals INTEGER NOT NULL,
membership_included INTEGER NOT NULL DEFAULT 0,
line_items_json TEXT NOT NULL,
policy_hash TEXT NOT NULL,
access_class TEXT NOT NULL,
availability_state TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
confirmed_at TEXT,
confirmed_tx_hash TEXT
);`,
`CREATE INDEX IF NOT EXISTS idx_marketplace_quotes_wallet ON marketplace_quotes(wallet);`,
`CREATE TABLE IF NOT EXISTS marketplace_entitlements (
entitlement_id TEXT PRIMARY KEY,
quote_id TEXT NOT NULL UNIQUE,
offer_id TEXT NOT NULL,
wallet TEXT NOT NULL,
payer_wallet TEXT,
org_root_id TEXT,
principal_id TEXT,
principal_role TEXT,
workspace_id TEXT,
state TEXT NOT NULL,
access_class TEXT NOT NULL,
availability_state TEXT NOT NULL,
policy_hash TEXT NOT NULL,
issued_at TEXT NOT NULL,
tx_hash TEXT NOT NULL
);`,
`CREATE INDEX IF NOT EXISTS idx_marketplace_entitlements_wallet ON marketplace_entitlements(wallet);`,
`CREATE TABLE IF NOT EXISTS governance_principals ( `CREATE TABLE IF NOT EXISTS governance_principals (
wallet TEXT PRIMARY KEY, wallet TEXT PRIMARY KEY,
org_root_id TEXT NOT NULL, org_root_id TEXT NOT NULL,
@ -373,6 +415,259 @@ func (s *store) getQuote(ctx context.Context, quoteID string) (quoteRecord, erro
return rec, nil return rec, nil
} }
func (s *store) putMarketplaceQuote(ctx context.Context, quote marketplaceQuoteRecord) error {
_, err := s.db.ExecContext(ctx, `
INSERT INTO marketplace_quotes (
quote_id, wallet, payer_wallet, offer_id, org_root_id, principal_id, principal_role, workspace_id,
currency, amount_atomic, total_amount_atomic, decimals, membership_included, line_items_json,
policy_hash, access_class, availability_state, created_at, expires_at, confirmed_at, confirmed_tx_hash
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(quote_id) DO UPDATE SET
wallet=excluded.wallet,
payer_wallet=excluded.payer_wallet,
offer_id=excluded.offer_id,
org_root_id=excluded.org_root_id,
principal_id=excluded.principal_id,
principal_role=excluded.principal_role,
workspace_id=excluded.workspace_id,
currency=excluded.currency,
amount_atomic=excluded.amount_atomic,
total_amount_atomic=excluded.total_amount_atomic,
decimals=excluded.decimals,
membership_included=excluded.membership_included,
line_items_json=excluded.line_items_json,
policy_hash=excluded.policy_hash,
access_class=excluded.access_class,
availability_state=excluded.availability_state,
created_at=excluded.created_at,
expires_at=excluded.expires_at,
confirmed_at=excluded.confirmed_at,
confirmed_tx_hash=excluded.confirmed_tx_hash
`, quote.QuoteID,
strings.ToLower(strings.TrimSpace(quote.Wallet)),
nullableString(strings.ToLower(strings.TrimSpace(quote.PayerWallet))),
strings.TrimSpace(quote.OfferID),
nullableString(strings.TrimSpace(quote.OrgRootID)),
nullableString(strings.TrimSpace(quote.PrincipalID)),
nullableString(strings.ToLower(strings.TrimSpace(quote.PrincipalRole))),
nullableString(strings.TrimSpace(quote.WorkspaceID)),
strings.TrimSpace(quote.Currency),
strings.TrimSpace(quote.AmountAtomic),
strings.TrimSpace(quote.TotalAmountAtomic),
quote.Decimals,
boolToInt(quote.MembershipIncluded),
quote.LineItemsJSON,
strings.TrimSpace(quote.PolicyHash),
strings.ToLower(strings.TrimSpace(quote.AccessClass)),
strings.ToLower(strings.TrimSpace(quote.AvailabilityState)),
quote.CreatedAt.UTC().Format(time.RFC3339Nano),
quote.ExpiresAt.UTC().Format(time.RFC3339Nano),
formatNullableTime(quote.ConfirmedAt),
nullableString(strings.ToLower(strings.TrimSpace(quote.ConfirmedTxHash))),
)
return err
}
func (s *store) getMarketplaceQuote(ctx context.Context, quoteID string) (marketplaceQuoteRecord, error) {
row := s.db.QueryRowContext(ctx, `
SELECT quote_id, wallet, payer_wallet, offer_id, org_root_id, principal_id, principal_role, workspace_id,
currency, amount_atomic, total_amount_atomic, decimals, membership_included, line_items_json,
policy_hash, access_class, availability_state, created_at, expires_at, confirmed_at, confirmed_tx_hash
FROM marketplace_quotes
WHERE quote_id = ?
`, strings.TrimSpace(quoteID))
var rec marketplaceQuoteRecord
var payerWallet, orgRootID, principalID, principalRole, workspaceID sql.NullString
var createdAt, expiresAt, confirmedAt, confirmedTxHash sql.NullString
var membershipIncluded int
err := row.Scan(
&rec.QuoteID,
&rec.Wallet,
&payerWallet,
&rec.OfferID,
&orgRootID,
&principalID,
&principalRole,
&workspaceID,
&rec.Currency,
&rec.AmountAtomic,
&rec.TotalAmountAtomic,
&rec.Decimals,
&membershipIncluded,
&rec.LineItemsJSON,
&rec.PolicyHash,
&rec.AccessClass,
&rec.AvailabilityState,
&createdAt,
&expiresAt,
&confirmedAt,
&confirmedTxHash,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return marketplaceQuoteRecord{}, errNotFound
}
return marketplaceQuoteRecord{}, err
}
rec.PayerWallet = payerWallet.String
rec.OrgRootID = orgRootID.String
rec.PrincipalID = principalID.String
rec.PrincipalRole = principalRole.String
rec.WorkspaceID = workspaceID.String
rec.MembershipIncluded = membershipIncluded == 1
rec.CreatedAt = parseRFC3339Nullable(createdAt)
rec.ExpiresAt = parseRFC3339Nullable(expiresAt)
rec.ConfirmedAt = parseRFC3339Ptr(confirmedAt)
rec.ConfirmedTxHash = confirmedTxHash.String
return rec, nil
}
func (s *store) putMarketplaceEntitlement(ctx context.Context, ent marketplaceEntitlementRecord) error {
_, err := s.db.ExecContext(ctx, `
INSERT INTO marketplace_entitlements (
entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(entitlement_id) DO UPDATE SET
quote_id=excluded.quote_id,
offer_id=excluded.offer_id,
wallet=excluded.wallet,
payer_wallet=excluded.payer_wallet,
org_root_id=excluded.org_root_id,
principal_id=excluded.principal_id,
principal_role=excluded.principal_role,
workspace_id=excluded.workspace_id,
state=excluded.state,
access_class=excluded.access_class,
availability_state=excluded.availability_state,
policy_hash=excluded.policy_hash,
issued_at=excluded.issued_at,
tx_hash=excluded.tx_hash
`, strings.TrimSpace(ent.EntitlementID),
strings.TrimSpace(ent.QuoteID),
strings.TrimSpace(ent.OfferID),
strings.ToLower(strings.TrimSpace(ent.Wallet)),
nullableString(strings.ToLower(strings.TrimSpace(ent.PayerWallet))),
nullableString(strings.TrimSpace(ent.OrgRootID)),
nullableString(strings.TrimSpace(ent.PrincipalID)),
nullableString(strings.ToLower(strings.TrimSpace(ent.PrincipalRole))),
nullableString(strings.TrimSpace(ent.WorkspaceID)),
strings.ToLower(strings.TrimSpace(ent.State)),
strings.ToLower(strings.TrimSpace(ent.AccessClass)),
strings.ToLower(strings.TrimSpace(ent.AvailabilityState)),
strings.TrimSpace(ent.PolicyHash),
ent.IssuedAt.UTC().Format(time.RFC3339Nano),
strings.ToLower(strings.TrimSpace(ent.TxHash)),
)
return err
}
func (s *store) getMarketplaceEntitlementByQuote(ctx context.Context, quoteID string) (marketplaceEntitlementRecord, error) {
row := s.db.QueryRowContext(ctx, `
SELECT entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
FROM marketplace_entitlements
WHERE quote_id = ?
`, strings.TrimSpace(quoteID))
return scanMarketplaceEntitlement(row)
}
func (s *store) listMarketplaceEntitlementsByWallet(ctx context.Context, wallet string) ([]marketplaceEntitlementRecord, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
FROM marketplace_entitlements
WHERE wallet = ?
ORDER BY issued_at DESC
`, strings.ToLower(strings.TrimSpace(wallet)))
if err != nil {
return nil, err
}
defer rows.Close()
records := make([]marketplaceEntitlementRecord, 0)
for rows.Next() {
rec, err := scanMarketplaceEntitlement(rows)
if err != nil {
return nil, err
}
records = append(records, rec)
}
if err := rows.Err(); err != nil {
return nil, err
}
return records, nil
}
func (s *store) hasActiveEntitlement(ctx context.Context, wallet, offerID, orgRootID string) (bool, error) {
wallet = strings.ToLower(strings.TrimSpace(wallet))
offerID = strings.TrimSpace(offerID)
orgRootID = strings.TrimSpace(orgRootID)
if wallet == "" || offerID == "" {
return false, nil
}
query := `
SELECT 1
FROM marketplace_entitlements
WHERE wallet = ?
AND offer_id = ?
AND state = 'active'
`
args := []any{wallet, offerID}
if orgRootID != "" {
query += " AND org_root_id = ?"
args = append(args, orgRootID)
}
query += " LIMIT 1"
row := s.db.QueryRowContext(ctx, query, args...)
var marker int
if err := row.Scan(&marker); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, err
}
return marker == 1, nil
}
func scanMarketplaceEntitlement(row interface{ Scan(dest ...any) error }) (marketplaceEntitlementRecord, error) {
var rec marketplaceEntitlementRecord
var payerWallet, orgRootID, principalID, principalRole, workspaceID sql.NullString
var issuedAt sql.NullString
err := row.Scan(
&rec.EntitlementID,
&rec.QuoteID,
&rec.OfferID,
&rec.Wallet,
&payerWallet,
&orgRootID,
&principalID,
&principalRole,
&workspaceID,
&rec.State,
&rec.AccessClass,
&rec.AvailabilityState,
&rec.PolicyHash,
&issuedAt,
&rec.TxHash,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return marketplaceEntitlementRecord{}, errNotFound
}
return marketplaceEntitlementRecord{}, err
}
rec.PayerWallet = payerWallet.String
rec.OrgRootID = orgRootID.String
rec.PrincipalID = principalID.String
rec.PrincipalRole = principalRole.String
rec.WorkspaceID = workspaceID.String
rec.IssuedAt = parseRFC3339Nullable(issuedAt)
return rec, nil
}
func (s *store) putGovernancePrincipal(ctx context.Context, rec governancePrincipalRecord) error { func (s *store) putGovernancePrincipal(ctx context.Context, rec governancePrincipalRecord) error {
_, err := s.db.ExecContext(ctx, ` _, err := s.db.ExecContext(ctx, `
INSERT INTO governance_principals ( INSERT INTO governance_principals (

View File

@ -8,14 +8,14 @@ Success (`200`):
{ {
"offers": [ "offers": [
{ {
"offer_id": "edut.governance.core", "offer_id": "edut.workspace.core",
"issuer_id": "edut.firstparty", "issuer_id": "edut.firstparty",
"title": "EDUT Governance Core", "title": "EDUT Workspace Core",
"summary": "First paid runtime license.", "summary": "Org-bound deterministic governance runtime.",
"status": "active", "status": "active",
"pricing": { "pricing": {
"currency": "USDC", "currency": "USDC",
"amount_atomic": "499000000", "amount_atomic": "1000000000",
"decimals": 6, "decimals": 6,
"chain_id": 8453 "chain_id": 8453
}, },
@ -39,7 +39,7 @@ Request:
{ {
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11", "payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11",
"offer_id": "edut.governance.core", "offer_id": "edut.workspace.core",
"org_root_id": "org.acme.root", "org_root_id": "org.acme.root",
"principal_id": "human.joshua", "principal_id": "human.joshua",
"principal_role": "org_root_owner", "principal_role": "org_root_owner",
@ -55,31 +55,31 @@ Success (`200`):
"quote_id": "cq_01HZZXFQ27ZP6MP0V2R9M6V3KX", "quote_id": "cq_01HZZXFQ27ZP6MP0V2R9M6V3KX",
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11", "payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11",
"offer_id": "edut.governance.core", "offer_id": "edut.workspace.core",
"org_root_id": "org.acme.root", "org_root_id": "org.acme.root",
"principal_id": "human.joshua", "principal_id": "human.joshua",
"principal_role": "org_root_owner", "principal_role": "org_root_owner",
"currency": "USDC", "currency": "USDC",
"amount": "500.00", "amount": "1000.00",
"amount_atomic": "500000000", "amount_atomic": "1000000000",
"total_amount": "505.00", "total_amount": "1100.00",
"total_amount_atomic": "505000000", "total_amount_atomic": "1100000000",
"decimals": 6, "decimals": 6,
"membership_activation_included": true, "membership_activation_included": true,
"line_items": [ "line_items": [
{ {
"kind": "license", "kind": "license",
"label": "Governance Core License", "label": "EDUT Workspace Core",
"amount": "500.00", "amount": "1000.00",
"amount_atomic": "500000000", "amount_atomic": "1000000000",
"decimals": 6, "decimals": 6,
"currency": "USDC" "currency": "USDC"
}, },
{ {
"kind": "membership", "kind": "membership",
"label": "EDUT Membership Activation", "label": "EDUT Membership Activation",
"amount": "5.00", "amount": "100.00",
"amount_atomic": "5000000", "amount_atomic": "100000000",
"decimals": 6, "decimals": 6,
"currency": "USDC" "currency": "USDC"
} }
@ -120,7 +120,7 @@ Request:
"quote_id": "cq_01HZZXFQ27ZP6MP0V2R9M6V3KX", "quote_id": "cq_01HZZXFQ27ZP6MP0V2R9M6V3KX",
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11", "payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11",
"offer_id": "edut.governance.core", "offer_id": "edut.workspace.core",
"org_root_id": "org.acme.root", "org_root_id": "org.acme.root",
"principal_id": "human.joshua", "principal_id": "human.joshua",
"principal_role": "org_root_owner", "principal_role": "org_root_owner",
@ -136,7 +136,7 @@ Success (`200`):
{ {
"status": "entitlement_active", "status": "entitlement_active",
"entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001", "entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001",
"offer_id": "edut.governance.core", "offer_id": "edut.workspace.core",
"org_root_id": "org.acme.root", "org_root_id": "org.acme.root",
"principal_id": "human.joshua", "principal_id": "human.joshua",
"principal_role": "org_root_owner", "principal_role": "org_root_owner",
@ -158,7 +158,7 @@ Success (`200`):
"entitlements": [ "entitlements": [
{ {
"entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001", "entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001",
"offer_id": "edut.crm.pro.annual", "offer_id": "edut.workspace.core",
"wallet_address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "wallet_address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"workspace_id": "workspace.work.acme", "workspace_id": "workspace.work.acme",
"org_root_id": "org.acme.root", "org_root_id": "org.acme.root",

View File

@ -95,8 +95,8 @@ Success (`200`):
"quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W", "quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W",
"chain_id": 8453, "chain_id": 8453,
"currency": "USDC", "currency": "USDC",
"amount": "5.00", "amount": "100.00",
"amount_atomic": "5000000", "amount_atomic": "100000000",
"decimals": 6, "decimals": 6,
"deadline": "2026-02-17T07:36:12Z", "deadline": "2026-02-17T07:36:12Z",
"contract_address": "0x1111111111111111111111111111111111111111", "contract_address": "0x1111111111111111111111111111111111111111",

View File

@ -4,7 +4,7 @@ This policy defines deterministic rules for membership mint pricing.
## Policy Objectives ## Policy Objectives
1. Keep onboarding friction low. 1. Keep onboarding deterministic and explicit.
2. Guarantee mint-cost coverage. 2. Guarantee mint-cost coverage.
3. Support configurable growth tiers. 3. Support configurable growth tiers.
4. Keep pricing behavior transparent and auditable. 4. Keep pricing behavior transparent and auditable.
@ -13,35 +13,22 @@ This policy defines deterministic rules for membership mint pricing.
1. Membership is paid. 1. Membership is paid.
2. Membership is required for marketplace purchases. 2. Membership is required for marketplace purchases.
3. Membership price is configurable but must not go below policy floor. 3. Membership price is fixed at `100 USDC` for launch.
## Floor Rule ## Floor Rule
1. Default floor target: USD 5.00 equivalent. 1. Launch membership price: `100.00 USDC` (`100000000` atomic with 6 decimals).
2. Effective configured price must satisfy: 2. No discounts, no bundles, no alternate path pricing.
3. Any future change must be owner-governed and event-emitted.
```text
configured_price >= max(minimum_floor, estimated_network_cost * safety_multiplier)
```
3. Recommended default `safety_multiplier`: `1.5`.
## Supported Settlement Currencies (v1) ## Supported Settlement Currencies (v1)
1. `USDC` on Base (preferred stable settlement). 1. `USDC` on Base.
2. `ETH` on Base (optional).
`BTC` display may be shown as reference only; settlement remains Base-native. ## Tier Policy
## Tier Policy (Optional) 1. Disabled for launch.
2. No supply curve, no promotions, no coupons.
1. Tiering is supply-based (`total_membership_minted`).
2. Each tier has:
- `max_supply`
- `currency`
- `amount_atomic`
3. Price transitions occur automatically when supply crosses a tier boundary.
4. Tier changes emit on-chain events.
## Quote Lock Requirements ## Quote Lock Requirements
@ -65,9 +52,8 @@ A wallet confirmation is valid only when tx matches quote values exactly.
## UX Disclosure Rules ## UX Disclosure Rules
1. Show exact payable amount before wallet confirmation. 1. Show exact payable amount before wallet confirmation.
2. Show currency clearly (`USDC` or `ETH`). 2. Show currency clearly (`USDC`).
3. If BTC equivalent is shown, label it `reference only`. 3. Never imply investment return or speculative upside.
4. Never imply investment return or speculative upside.
## Evidence Requirements ## Evidence Requirements

View File

@ -1,7 +1,7 @@
{ {
"schema_version": "entitlement.v1", "schema_version": "entitlement.v1",
"entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001", "entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001",
"offer_id": "edut.crm.pro.annual", "offer_id": "edut.workspace.core",
"issuer_id": "edut.firstparty", "issuer_id": "edut.firstparty",
"wallet_address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "wallet_address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"workspace_id": "workspace.work.acme", "workspace_id": "workspace.work.acme",
@ -10,7 +10,7 @@
"tx_hash": "0x1111111111111111111111111111111111111111111111111111111111111111", "tx_hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
"chain_id": 8453, "chain_id": 8453,
"currency": "USDC", "currency": "USDC",
"amount_atomic": "199000000", "amount_atomic": "1000000000",
"policy_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "policy_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}, },
"issued_at": "2026-02-17T00:02:12Z", "issued_at": "2026-02-17T00:02:12Z",

View File

@ -3,10 +3,10 @@
"catalog_id": "launch-2026-operator", "catalog_id": "launch-2026-operator",
"offers": [ "offers": [
{ {
"offer_id": "edut.governance.core", "offer_id": "edut.solo.core",
"title": "EDUT Governance Core", "title": "EDUT Solo Core",
"summary": "First paid runtime license. Activates deterministic governance runtime.", "summary": "Single-principal governance runtime for personal operations.",
"price": "499.00", "price": "1000.00",
"currency": "USDC", "currency": "USDC",
"member_only": true, "member_only": true,
"workspace_bound": false, "workspace_bound": false,
@ -15,10 +15,10 @@
"multi_tenant": false "multi_tenant": false
}, },
{ {
"offer_id": "edut.crm.pro.annual", "offer_id": "edut.workspace.core",
"title": "EDUT CRM Pro", "title": "EDUT Workspace Core",
"summary": "Workspace-bound CRM module with governance and evidence integration.", "summary": "Org-bound deterministic governance runtime for team operations.",
"price": "199.00", "price": "1000.00",
"currency": "USDC", "currency": "USDC",
"member_only": true, "member_only": true,
"workspace_bound": true, "workspace_bound": true,
@ -27,10 +27,34 @@
"multi_tenant": false "multi_tenant": false
}, },
{ {
"offer_id": "edut.invoicing.core.annual", "offer_id": "edut.workspace.ai",
"title": "EDUT Invoicing Core", "title": "EDUT Workspace AI Layer",
"summary": "Invoicing workflow module for member workspaces.", "summary": "AI reasoning layer for governed workspace operations.",
"price": "99.00", "price": "1000.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
},
{
"offer_id": "edut.workspace.lane24",
"title": "EDUT Workspace 24h Lane",
"summary": "Autonomous execution lane capacity for workspace throughput.",
"price": "1000.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
},
{
"offer_id": "edut.workspace.sovereign",
"title": "EDUT Workspace Sovereign Continuity",
"summary": "Continuity profile for stronger local/offline workspace operation.",
"price": "1000.00",
"currency": "USDC", "currency": "USDC",
"member_only": true, "member_only": true,
"workspace_bound": true, "workspace_bound": true,

View File

@ -1,13 +1,13 @@
{ {
"schema_version": "offer.v1", "schema_version": "offer.v1",
"offer_id": "edut.crm.pro.annual", "offer_id": "edut.workspace.core",
"issuer_id": "edut.firstparty", "issuer_id": "edut.firstparty",
"title": "EDUT CRM Pro", "title": "EDUT Workspace Core",
"summary": "Workspace-bound CRM module entitlement with annual billing.", "summary": "Org-bound deterministic governance runtime entitlement.",
"status": "active", "status": "active",
"pricing": { "pricing": {
"currency": "USDC", "currency": "USDC",
"amount_atomic": "199000000", "amount_atomic": "1000000000",
"decimals": 6, "decimals": 6,
"chain_id": 8453 "chain_id": 8453
}, },
@ -17,18 +17,18 @@
"transferable": false, "transferable": false,
"internal_use_only": true, "internal_use_only": true,
"multi_tenant": false, "multi_tenant": false,
"max_per_wallet": 5, "max_per_wallet": 1,
"requires_admin_approval": false "requires_admin_approval": false
}, },
"entitlement": { "entitlement": {
"type": "module_license", "type": "runtime_license",
"scope": "workspace", "scope": "org_root",
"runtime_policy_ref": "policy.crm.pro.v1" "runtime_policy_ref": "policy.workspace.core.v1"
}, },
"created_at": "2026-02-17T00:00:00Z", "created_at": "2026-02-17T00:00:00Z",
"updated_at": "2026-02-17T00:00:00Z", "updated_at": "2026-02-17T00:00:00Z",
"metadata": { "metadata": {
"category": "business", "category": "governance",
"support_tier": "standard" "support_tier": "zero_ops"
} }
} }

View File

@ -191,8 +191,8 @@ Response:
"quote_id": "mq_...", "quote_id": "mq_...",
"chain_id": 8453, "chain_id": 8453,
"currency": "USDC", "currency": "USDC",
"amount": "5.00", "amount": "100.00",
"amount_atomic": "5000000", "amount_atomic": "100000000",
"deadline": "2026-02-17T07:36:12Z", "deadline": "2026-02-17T07:36:12Z",
"contract_address": "0x...", "contract_address": "0x...",
"method": "mintMembership", "method": "mintMembership",

View File

@ -406,13 +406,13 @@
setCheckoutLog('Offer catalog loaded: ' + payload.offers.length + ' offers.'); setCheckoutLog('Offer catalog loaded: ' + payload.offers.length + ' offers.');
} catch (err) { } catch (err) {
state.offers = [{ state.offers = [{
offer_id: 'edut.governance.core', offer_id: 'edut.workspace.core',
title: 'EDUT Governance Core (fallback)', title: 'EDUT Workspace Core (fallback)',
summary: 'Fallback governance offer loaded because catalog fetch failed.', summary: 'Fallback workspace core offer loaded because catalog fetch failed.',
price: '499.00', price: '1000.00',
currency: 'USDC', currency: 'USDC',
member_only: true, member_only: true,
workspace_bound: false, workspace_bound: true,
transferable: false, transferable: false,
}]; }];
state.selectedOfferId = state.offers[0].offer_id; state.selectedOfferId = state.offers[0].offer_id;

View File

@ -2,22 +2,10 @@
"catalog_id": "launch-2026-operator", "catalog_id": "launch-2026-operator",
"offers": [ "offers": [
{ {
"offer_id": "edut.governance.core", "offer_id": "edut.solo.core",
"title": "EDUT Governance Core", "title": "EDUT Solo Core",
"summary": "First paid runtime license. Activates deterministic governance engine on entitled devices.", "summary": "Single-principal governance runtime for personal operations.",
"price": "499.00", "price": "1000.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": false,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
},
{
"offer_id": "edut.crm.pro.annual",
"title": "EDUT CRM Pro",
"summary": "Workspace-bound CRM module with governance and evidence integration.",
"price": "199.00",
"currency": "USDC", "currency": "USDC",
"member_only": true, "member_only": true,
"workspace_bound": true, "workspace_bound": true,
@ -26,10 +14,46 @@
"multi_tenant": false "multi_tenant": false
}, },
{ {
"offer_id": "edut.invoicing.core.annual", "offer_id": "edut.workspace.core",
"title": "EDUT Invoicing Core", "title": "EDUT Workspace Core",
"summary": "Invoicing workflow module for member workspaces.", "summary": "Org-bound deterministic governance runtime for team operations.",
"price": "99.00", "price": "1000.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
},
{
"offer_id": "edut.workspace.ai",
"title": "EDUT Workspace AI Layer",
"summary": "AI reasoning layer for governed workspace operations.",
"price": "1000.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
},
{
"offer_id": "edut.workspace.lane24",
"title": "EDUT Workspace 24h Lane",
"summary": "Autonomous execution lane capacity for workspace queue throughput.",
"price": "1000.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
},
{
"offer_id": "edut.workspace.sovereign",
"title": "EDUT Workspace Sovereign Continuity",
"summary": "Stronger local/offline continuity profile for governed workspaces.",
"price": "1000.00",
"currency": "USDC", "currency": "USDC",
"member_only": true, "member_only": true,
"workspace_bound": true, "workspace_bound": true,