Sable Launchpad
A launchpad for backed tokens — tokenized stocks, bullion, credit, property. The problem it addresses is not fraud; it is a gap in the shape of the thing. A backed token is a promise that something real is sitting somewhere, and the evidence for that promise arrives a few times a year in an attestation letter. Between two letters nobody can check anything, and an honest issuer is in exactly the same position as a dishonest one.
Proof of Backing already closes that interval for an existing token. The Launchpad closes it from the beginning, and adds the half a backing check cannot reach on its own: what happens when a check fails.
An issuer locks a Launch Spec once. From then on:
- the backing is under continuous Proof of Backing — Sable reads the custodian and the chain itself, on a schedule;
- minting is authorized only against a check that covers the new supply — not the old one;
- transfers pause by rule after a shortfall has stood for the declared window;
- distributions and NAV are signed declarations, not blog posts.
Every outcome is a signed artifact anyone can verify at
POST /v1/receipts/verify, and the mint and pause authorizations are
EIP-712 typed messages a reference contract enforces on-chain with no
issuer admin key.
What Sable is here, stated first
Sable does not issue, custody, hold funds for, sell, or endorse any token on this page. There is no sale, no allocation and no price anywhere on this site.
The issuer deploys the contract under their own key. Sable is the verdict
oracle and the administrator: it reads two sources, applies a rule the issuer
locked, and signs the outcome under a key anyone can pin at
GET /v1/receipts/pubkey.
That makes a launch here provably administered and publicly verifiable. It does not make it trustless, and the docs, the API and the portal all say so rather than leaving the stronger reading available. What is proved is that the rules were applied to the readings and published; not that Sable could not have done otherwise.
Whether a particular token may be offered, and to whom, is a question for the issuer's own counsel. It is not one Sable answers, and a listing is not advice that it has been answered.
The sections
A Launch Spec has seven required sections and one optional eighth. All of them are locked: they are canonicalized, hashed, and the digest is stamped on the sealed-spec commitment, on every EIP-712 authorization, and on the contract as an immutable field. There is no route that edits them, because every already-signed artifact was produced under them.
| Section | Holds | Locked |
|---|---|---|
| Asset | Holdsname, symbol, underlying, optional ISIN, chain id | Lockedyes |
| Backing | Holdscustodian name, units per token, shortfall tolerance, pause-after window | Lockedyes |
| Supply | Holdsceiling, mint rule (verified-only) | Lockedyes |
| Distributions | Holdswhether income on the underlying is passed through | Lockedyes |
| Compliance | Holdsjurisdictions, sanctions screening, transfer restrictions | Lockedyes |
| Redemption | Holdssettlement window, notice period in seconds | Lockedyes |
| Operator | Holdswhich tools Sable may use | Lockedyes |
| Covenants (optional) | Holdsminimum coverage, maximum pause, maximum check age | Lockedyes — and omitted from the hash entirely when undeclared |
Rotatable, and deliberately not in the hashed spec: the custodian's endpoint URL, its credential, the JSON pointer to the figure, that figure's decimals, and the check cadence. Changing a URL does not change what backing means, so those are operational fields the issuer can move without touching what a published verdict meant.
The compliance section is the issuer's declarations. Sable does not verify them and is not a regulator; hashing them means they cannot be quietly revised, not that they are being honoured.
The canonical form
The spec's digest is its identity, so the recipe is exact and pinned on both
sides — the gateway holds three golden vectors in a Rust test, and the browser
implementation (lib/launch-spec.ts) is diffed against the same three.
- keys sorted by code point at every level;
- no whitespace;
- integers only — micro units, basis points and seconds.
redemption.noticeis a number of seconds, not a phrase: "two business days" is not a quantity two implementations could agree on; - optional keys omitted when empty rather than carried as
""ornull; asset.symboluppercased, and restricted toA-Zand0-9— a hyphen or a space is refused, not normalized past.asset.isinis uppercased and trimmed;compliance.jurisdictionsis a fixed vocabulary —US,EU,UK,SG,CH,AE, at most six. Free text on a public record would read as a legal claim Sable cannot check;- arrays (
jurisdictions,operator.tools) sorted and de-duplicated by the canonicalizer, so the digest does not depend on which checkbox the issuer clicked first.
{"asset":{"chain_id":1,"isin":"US0378331005","name":"Acme Tokenized AAPL","symbol":"AAAPL","underlying":"Apple Inc. common stock"},"backing":{"custodian_name":"Acme Custody Ltd","pause_after_secs":3600,"tolerance_bps":25,"units_per_token_micro":1000000},"compliance":{"jurisdictions":["CH","EU","US"],"sanctions_screening":true,"transfer_restrictions":true},"distributions":{"passthrough":true},"operator":{"tools":["mint.authorize","nav.publish"]},"redemption":{"notice":86400,"settlement":"T+2"},"supply":{"max_micro":1000000000000,"mint_rule":"verified-only"},"v":1}
spec_sha256 = sha256(canonical). For the line above that is
9d7c7bcbd759a5cdc5aad74a584c3a98c3395de3fd268a7e814dfc548b38d69b — check it
with printf '%s' '<the line>' | shasum -a 256 rather than taking this page's
word for it.
Note "v":1 sorts last, after supply. It reads wrong and it is right:
the sort is by code point, and v > s. An implementation that put the
version first because versions come first would produce a different digest for
the same spec.
Two details worth naming, because each is a way a re-implementation drifts
without anyone noticing: tolerance_bps: 0, pause_after_secs: 0,
notice: 0, "jurisdictions":[] and "tools":[] must all survive
canonicalization — a port that filters on falsiness drops them and renders a
digest no launch could carry. And an empty isin must be absent, not "".
Every public record publishes spec_canonical — the exact bytes the gateway
hashed — beside spec_sha256, so a reader recomputes rather than trusts. The
record page does exactly that in your browser, and renders a red MISMATCH if
they differ.
The optional eighth section
A spec may also carry covenants, with any of three integer keys:
"covenants":{"max_check_age_secs":7200,"max_pause_secs":86400,"min_coverage_bps":10500}
It sorts between compliance and distributions — com < cov < dis, by
code point — and inside it max_check_age_secs < max_pause_secs <
min_coverage_bps.
Each key is omitted when undeclared, and the whole section is omitted when
all three are. That is not a tidiness rule, it is the compatibility rule:
because a spec with no covenants emits no covenants key at all, every digest
written before covenants existed is byte-identical to what it always was, and
all three original golden vectors still hash to the same values. A build that
emitted "covenants":{} would change the identity of every launch on the
platform.
0 is a declared covenant, not an absence. min_coverage_bps: 0 is a
floor of zero — hashed, locked, and trivially kept. min_coverage_bps absent
is no promise at all. An implementation that filters covenants on falsiness
drops the first one out of the digest; one that treats absence as zero invents
promises nobody made. Three further golden vectors pin exactly this, in the
browser and in the gateway.
Sealed launches
A launch can commit to a spec before revealing it: reveal_at in the future
means the public record carries the commitment and the digest and returns
spec: null until the reveal.
The commitment is signed and timestamped now, so when the spec is published anyone can hash it and confirm it is the same one. Nothing can be edited in between. Until then the record shows a countdown and no terms — which is a real state with its own page, not a loading state.
EIP-712 — the on-chain interface
Domain:
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
with name = "Sable Launchpad", version = "1", chainId the launch's chain,
and verifyingContract the bound token address.
That binding is why no mint can be authorized before the issuer binds the deployed contract: the authorization is a message signed against one exact address, so before there is an address there is nothing to sign for.
MintAuthorization(bytes32 specHash,bytes32 checkHash,uint64 checkSeq,address to,uint256 amount,uint256 newSupply,uint256 custodianUnits,uint256 nonce,uint64 expiresAt)
PauseAuthorization(bytes32 specHash,bytes32 checkHash,uint64 checkSeq,bool paused,uint64 issuedAt)
checkHash is the Proof of Backing check's sha256 as bytes32; specHash is
spec_sha256. Amounts are in token base units, per the contract's
decimals.
The reference contract
SableBackedToken is a hand-written ERC-20 with immutable specHash,
maxSupply, verdictSigner and decimals.
-
mint(MintAuthorization a, bytes sig)requires a valid signature fromverdictSigner, an unusednonce,block.timestamp <= expiresAt,totalSupply + amount <= newSupply,newSupply <= maxSupply, and that the token is not paused.The inequality is deliberate and worth reading twice.
newSupplyis the supply ceiling the custodian's holdings were judged against, and every step of that judgement rounds up — so the true post-mint supply can be lower than the ceiling after a burn or a rounding, and the safety property is that holdings covered the ceiling. Requiring equality would make an authorization revert because a holder redeemed in the meantime, which is the one event that can only ever improve backing. Two authorizations computed from the same check still cannot both mint: each carries its ownnonce, and the second one'snewSupplyceiling is not raised by the first. -
setPaused(PauseAuthorization a, bytes sig)requires a valid signature andcheckSeq > lastPauseSeq— monotonic, so a stale unpause can never override a newer pause. -
transferandtransferFromrevert while paused. -
burn(amount)by any holder, with no authorization: a redemption reduces supply, which can never weaken backing. -
No owner. No other privileged function.
-
postCheck(CheckAttestation a, bytes sig)is permissionless and monotonic oncheckSeq, and is what lets the contract answerisBacked(),isBackedWithin(maxAge),coverageBps()andlastCheck()— see below.
Submitting a pause is permissionless: any holder can submit a valid signed pause. Sable does not deploy the contract and does not submit transactions — auto-submission through an EVM publisher is not built, and this page will say so until it is.
The operator rules
The pause rule
Find the latest
VERIFIEDcheck. If aDISCREPANCYexists after it andnow − its checked_at ≥ pause_after_secs, the token is paused.
UNAVAILABLE never pauses. A gap is not a shortfall. A custodian endpoint
that times out is not evidence that assets are missing, and treating it as
such would let an outage freeze a healthy token.
A leader-gated pass recomputes this for every live launch. On a transition
— not on every pass — it signs a PauseAuthorization, appends the action, and
fires launch.paused or launch.resumed.
The mint rule
A mint authorization is refused unless all of these hold: the launch is
live, a contract is bound, the latest check is VERIFIED, the token is not
paused, new_supply ≤ max_micro, and
custodian_units ≥ ceil(new_supply × units_per_token)
The ceiling and the new supply are both deliberate. Holdings must cover what will exist after the mint, not what existed before it, and the requirement rounds up so an issuer cannot sit a fraction short and still read as fully backed.
When it is refused, the reason is available before you click.
GET /v1/launches/:id carries mint_eligibility with a stable machine
reason (paused, backing_would_not_cover, …) and a message that says
it in a sentence; the portal renders the sentence under the disabled button
rather than letting you discover it in a 409.
Distributions and NAV
A distribution is refused unless distributions.passthrough is declared in the
locked spec. When it is, Sable signs a dated declaration naming the amount per
token. Sable records and signs the declaration; it moves no money.
A NAV publication names the check it was computed against. The figures are the issuer's; the signature says they published them, not that they are correct.
An action whose kind is not in operator.tools is refused, not silently
skipped — and the tool list is hashed into the spec, so it cannot be widened on
an existing launch.
Seals
Seals are recomputed from the chain and the actions on every read, never stored. A seal that stops being true stops being lit, and there is no counter anyone can bump. Unearned seals are shown beside earned ones, each with the sentence that makes it true.
The token carries its own proof
Everything above lives around the token: Sable's record, Sable's rules, Sable's signatures. The five features below move the proof into the token and into a holder's hands, so that checking a backed token does not require trusting — or even visiting — this site.
The contract is its own oracle
A third EIP-712 type, signed by the same key as the mint and pause authorizations:
CheckAttestation(bytes32 specHash,bytes32 checkHash,uint64 checkSeq,uint8 status,uint32 coverageBps,uint64 checkedAt)
status is 0 = UNAVAILABLE, 1 = VERIFIED, 2 = DISCREPANCY, and
coverageBps is 0 when the check published no figure — so read status
before you read coverageBps, never the other way round.
GET /v1/launches/public/:slug/attestation returns the signed struct, its
digest and the signature. It appends no action and changes nothing: it is a
signed view of a check that was already published, not a decision, which is
why a public read writes nothing. postCheck(a, sig) is permissionless and
monotonic on checkSeq — anyone may submit it, a stale attestation can never
overwrite a newer one, and Sable does not submit it. The person who wants
the chain to know is the person who pays the gas.
The contract then answers, on-chain, with no oracle call and no trip through this API:
token.isBacked() // status == VERIFIED && not stale
token.isBackedWithin(1 hours) // …and the attestation is younger than this
token.coverageBps()
token.lastCheck()
which is what makes it composable. A lending market can require
isBackedWithin(1 hours) before accepting the token as collateral, and gets a
freshness guarantee rather than a promise: if nobody posts, the market stops
accepting it, automatically. A pool can halt on DISCREPANCY. A wallet can
render coverage from the chain alone.
⚠️ isBacked() means: the last attestation POSTED to this contract said
VERIFIED, and it is not stale. It does not mean the backing is sound right
now, and it does not mean the custodian is honest. It fails closed on
staleness — status == VERIFIED && block.timestamp <= checkedAt + maxCheckAge — so a token nobody attests to stops claiming to be backed, which
is the safe direction to be wrong in.
It is also independent of paused(). The pause rule has its own window
(pause_after_secs), and the staleness window is a different number, so the
two can legitimately disagree: a token can be paused while its last posted
attestation still reads VERIFIED, and a token can be unpaused while
isBacked() is false because nobody posted. Both surfaces say so rather than
picking whichever reads better.
Anyone can demand a check
POST /v1/launches/public/:slug/check
Public. The issuer cannot refuse it and cannot schedule around it. The resulting check joins the same hash chain as every scheduled one, with the same verdict it would have had whoever asked.
A cooldown always applies (SABLE_LAUNCH_DEMAND_CHECK_COOLDOWN_SECS,
default 300) because the cost is an outbound dial of someone else's custodian —
it applies to everyone equally, including the issuer. Over it, the answer is
429 with retry_after_secs, which is a countdown rather than a failure.
A price (SABLE_LAUNCH_DEMAND_CHECK_MICRO_USD, default 0) is settled
inline with x402 when set: the 402 carries an accepts array an agent can
settle against and retry, with no account needed. The fee pays for the outbound
reading. It is never an input to the verdict, and a paid check is published
identically to a free one — including when it fails.
Sable records that a check was demanded rather than scheduled, as
origin = "demand". ⚠️ origin is deliberately NOT part of the canonical
line the check's hash is computed over. Every hash already published keeps
exactly the meaning it had, and the verdict does not depend on who asked — the
origin is Sable's own record about the check, not part of the signed chain, and
this page says that rather than implying a stronger provenance.
A redemption is receipted
POST /v1/launches/public/:slug/redemptions {chain_id, tx_hash}
Sable reads the transaction from the chain itself, finds the burn emitted by
the bound token, and signs a receipt naming the burner, the amount, the burn
transaction, the check sequence current at the time, and the settlement terms
from the locked spec — not from the request. Recording is idempotent on
(launch, chain, tx_hash, log_index), so a retry returns the same receipt with
replayed: true rather than minting a second one.
Refusals are fixed classes, not free text: tx_not_found, tx_reverted,
no_burn_in_tx, wrong_token, chain_not_configured.
Burning is allowed while the token is paused, deliberately: a redemption reduces supply, which can never weaken backing. A pause stops trading and never the exit.
⚠️ A redemption receipt proves tokens were destroyed under stated terms. It is NOT proof that the issuer delivered anything, and Sable neither holds nor moves the underlying. What it gives a holder is a signed, portable, verifiable record that they exited and when — which is precisely what is missing when a dispute starts months later.
Covenants are graded, and a breach is permanent
The optional covenants section locks up to three promises:
| Covenant | Means |
|---|---|
min_coverage_bps | MeansCoverage may never fall below this. Above 10000 is an overcollateralization promise. |
max_pause_secs | MeansThe token may never stay paused by rule for longer than this. |
max_check_age_secs | MeansNo check may ever be older than this — and this becomes the contract's maxCheckAge. |
A pure grader runs over the check chain and the action log and returns
kept, broken or not_yet_gradable per covenant, with the check sequence
that broke it.
broken is permanent. A later recovery shows as currently_kept beside
the breach, never instead of it — on the public record, in the portal, and in
the API. There is no route, at any role, that clears one. A promise that
un-breaks itself is not a promise.
Covenants are derived from checks that are already anchored, so they need no anchoring of their own, and this page says that rather than implying a second proof exists.
⚠️ A covenant is graded from Sable's own readings, so a covenant kept over a lying custodian is still kept over a false figure. It tightens what the issuer promised; it does not improve what the custodian reported.
Binding verifies the contract
POST /v1/launches/:id/token reads the deployed contract before binding it
and refuses unless specHash(), maxSupply(), verdictSigner(), decimals()
and maxCheckAge() all agree with the locked spec and this deployment's
signer. A chain with no configured RPC cannot verify and therefore refuses,
rather than binding blind.
That is why max_check_age_secs has to be decided before the lock: it is
written into the contract as an immutable, and a contract deployed with a
different number will not bind.
The public record then reports verified_onchain and renders the two states
distinctly. "Bound" on its own means an address was submitted — the issuer's
claim. "Verified" means Sable read the chain. ⚠️ Verification covers the
contract's immutables only. It is not a statement that the contract is
otherwise safe, audited, or does what its name suggests.
Subscriptions — a primary sale that cannot settle unbacked
The rail is off by default and its go-live is counsel-gated, so this
section describes it in that tense. With the flag off, the terms of a bound
escrow are still published — a deployed contract is a fact about the token
whether or not Sable will sign for it today — and only the authorize call
answers 501.
What Sable is here, again and more narrowly
Sable does not sell the token, set the price, hold the escrow, run KYC, promote the sale, or run any secondary market. It does not decide who may buy: the allowlist root is the issuer's, built by the issuer and their counsel. Sable's entire part is one signature, and it is withheld when the backing does not cover the mint.
Whether an offering is lawful where a reader lives is not a question Sable answers, and nothing on the record is an answer to it.
How it works
SableSubscription is issuer-deployed and immutable: no owner, no admin,
no upgrade path, no pause, no sweep, and no function that can move the escrow
anywhere but to the buyer or to the two payees.
subscribe(amountTokens, proof)pulls payment into escrow while the window is open, under the issuer's optional Merkle allowlist. Nothing is minted yet.settle(buyer, MintAuthorization, sig)is permissionless and callstoken.mint(...)first, before any money moves. An authorization the token refuses — unbacked, paused, expired, past the ceiling, wrongly signed — reverts the whole settlement, and the buyer's payment is exactly where it was. That is the property: an unbacked sale cannot complete.refund()is buyer self-service afterclosesAtor at any time while the token is paused. A backing failure before delivery returns the money without anyone's permission and without waiting out the window. There is no owner to approve it and no pause that stops it.
The platform fee is capped at 500 basis points by a constant enforced in the constructor, so no deployment can exceed it. It is paid on-chain out of a settled subscription and Sable never touches the escrow.
⚠️ Why the mint is addressed to the escrow, not to the buyer
This is the least obvious line in the whole system and it is load-bearing.
SableBackedToken.mint is permissionless — anyone holding a valid
authorization may relay it straight to the token. If the authorization named
the buyer as recipient, the buyer could relay it themselves, take delivery,
and leave settle to revert on the consumed nonce with their pending
uncleared. After closesAt they would then refund and walk away with the
tokens and the money, with the issuer paid nothing against supply that now
exists. That is not a theoretical ordering concern — it was reproduced against
that shape.
So the authorization names the escrow and settle delivers onward. A front-run
then merely pre-delivers the units into the escrow, and settle completes
anyway: it notices the nonce is spent, verifies the authorization was genuine
regardless, finds the units, and finishes.
The consequence a reader has to be told rather than left to infer: units held
by the escrow are fungible, so settle pairs an authorization to a buyer by
AMOUNT, not by identity — a valid, unconsumed authorization for exactly N
units can settle any pending buyer for N units. Nothing is lost by that (the
buyer pays, the buyer receives, totals reconcile, and settling was
permissionless anyway), but the pairing of an authorization to a buyer lives
in Sable's record, not on the chain, and every surface that renders an
authorization says so.
The allowlist is the issuer's
When allowlistRoot is non-zero, subscribe requires a Merkle proof:
leaf(account) = keccak256(abi.encodePacked(account)) // 20 bytes
node(a, b) = keccak256(abi.encodePacked(min, max)) // sorted pair
That is the conventional EVM allowlist tree — merkletreejs with
{sortPairs: true}, or OpenZeppelin's StandardMerkleTree over a single
address — and deliberately not Sable's own SableMerkle, which is the
SHA-256 hex-text tree used for anchoring receipts. An issuer builds this list
with ordinary tooling and nothing about it touches the gateway. The full
construction, and why a 20-byte leaf preimage is safe under sorted pairs, is in
contracts/README.md.
⚠️ Sable does no KYC here. The root is the issuer's. Membership is not a statement by Sable that anyone is eligible to hold the instrument where they live.
The endpoints
GET /v1/launches/public/:slug/subscription is public and answers even
when the rail is off — enabled: false, authorizing: false, terms included.
404 means nothing is bound. The live block carries reserved, remaining
and is_open read from the chain, and all three are null when the escrow
could not be read — the record publishes no figure rather than a default,
because "sold out" or "closed" printed on the strength of an RPC timeout is a
claim about someone else's offering.
POST /v1/launches/public/:slug/subscription/authorize {buyer} is public.
Sable reads pendingOf(buyer) from the chain itself, screens the address, runs
the same mint rule as everywhere else, and signs. Its refusals are four
different facts and the portal and record render them as four different things:
501 the rail is off, 403 screened, 409 nothing_pending (no escrowed
subscription for that address), and 409 mint_refused with a fixed reason —
which is the rail working: an unbacked sale stopped before any money moved,
with the buyer's payment untouched.
The authorization is recorded as an ordinary mint_authorized action with
via: "subscription". There is no separate authorization table and the action
sequence stays the EIP-712 nonce.
Issuer-side, POST /v1/launches/:id/subscription {chain_id, address} binds a
deployed escrow once, verified against the chain exactly as the token
binding is (409 wrong_token / already_closed / fee_not_wired /
already_bound), and GET returns it.
What a subscription does not prove
It proves the sale could not settle unbacked. It says nothing about price, value, suitability, or whether the offering is lawful anywhere. It cannot know whether the custodian told Sable the truth — a custodian that misreports produces a VERIFIED check, an authorization signed against it, and a settlement that completes. And it cannot deliver the underlying: redemption terms are the issuer's, off-chain, and the contract holds no assets but the escrowed payment and, for the length of one call, the units it is delivering.
For agents
The MCP tool sable_backing_status(slug) returns the verdict, the covenants
and the contract's own answers in one call.
Fees
All fees ride the existing prepaid USDT credit ledger. There is no token, no
trading venue, no custody. The sheet is public at GET /v1/launches/fees so
no page ever prints a placeholder price, and both fees default to zero.
| Stream | When | Gate |
|---|---|---|
| Launch fee | Whendebited once, at lock | GateSABLE_LAUNCH_FEE_MICRO_USD |
| Monthly asset fee | Whenhourly worker, per period | GateSABLE_LAUNCH_MONTHLY_FEE_MICRO_USD |
| Metered operator compute | Whennot built — arrives with the narration agent | Gate— |
A fee never touches a verdict. If the monthly fee goes unpaid the launch is
marked fee_status: "overdue" publicly and a webhook fires — and the checks
keep running, the verdict is unaffected, and nothing on the record changes. The
verdict is not for sale, and the holders' record is not a hostage.
Not built, and counsel-gated: a basis-point fee on assets under proof (fund-administration territory), any per-transfer fee (it would make Sable a venue), and any bridge into the Vault as an issuance rail.
The whole write surface sits behind SABLE_LAUNCHPAD_ENABLED, which defaults
to off. With it off, POST /v1/launches answers 501 and charges nothing;
reads always work.
What a launch record proves, and what it does not
An issuer locked a spec, Sable has been reading the named custodian and the chain against it on a schedule, and every mint, pause and declaration since was produced by a deterministic rule over those readings and signed.
That sentence travels inside the signature, so a reader who never opens this page still meets the limits when they verify the receipt.
It does not prove:
-
that the custodian is honest. A custodian that misreports to Sable produces a
VERIFIEDcheck over a false figure. What the product removes is the blind interval, not the need for a trustworthy custodian. -
that the issuer is solvent, or that the assets are unencumbered or segregated. A quantity was reported; that is all.
-
that the token is a good investment. A listing is not a rating, an endorsement or advice, and a paused launch is listed exactly like a passing one.
-
that Sable could not have signed otherwise. Sable is a trusted operator whose key is public and whose outputs are permanent. That is auditable; it is not trustless.
-
anything about compliance. The compliance section is what the issuer declared, hashed so it cannot be quietly revised.
-
that
isBacked()is true. It is a claim about the last attestation posted to the contract, never about the world, and it is independent ofpaused(). -
that a redemption was honoured. A redemption receipt proves a burn under stated terms; it is not proof the issuer delivered, and Sable neither holds nor moves the underlying.
-
that a kept covenant means a sound token. Covenants are graded from Sable's own readings, so a covenant kept over a lying custodian is still kept over a false figure.
-
that a verified binding means a safe contract. Bind verification proves the contract's immutables match the spec, nothing else.
-
that a subscription is a good deal, or a lawful one. It proves the sale could not settle unbacked. Sable does not sell, price, promote, screen for, or custody any part of it, and runs no secondary market.
Two more things that are true of every one of these, and are worth stating where they cannot be missed: a demanded check has exactly the same standing as a scheduled one, and its origin is Sable's own record rather than part of the signed chain. And Sable does not sell, promote, price, rate or endorse any of it, and runs no secondary market.
Endpoints
| Method | Path | Auth |
|---|---|---|
GET | Path/v1/launches | AuthPublic |
GET | Path/v1/launches/public/:slug | AuthPublic |
GET | Path/v1/launches/public/:slug/actions | AuthPublic |
GET | Path/v1/launches/public/:slug/attestation | AuthPublic |
GET | Path/v1/launches/public/:slug/onchain | AuthPublic |
POST | Path/v1/launches/public/:slug/check | AuthPublic (cooldown; x402 when priced) |
POST | Path/v1/launches/public/:slug/redemptions | AuthPublic |
GET | Path/v1/launches/public/:slug/redemptions | AuthPublic |
GET | Path/v1/launches/public/:slug/subscription | AuthPublic (answers when the rail is off) |
POST | Path/v1/launches/public/:slug/subscription/authorize | AuthPublic (501 when the rail is off) |
POST | Path/v1/launches/:id/subscription | AuthSession (Admin), once |
GET | Path/v1/launches/:id/subscription | AuthSession (Viewer+) |
GET | Path/v1/launches/fees | AuthPublic |
POST | Path/v1/launches | AuthSession (Admin) |
GET | Path/v1/launches/mine | AuthSession (Viewer+) |
GET | Path/v1/launches/:id | AuthSession (Viewer+) |
PATCH | Path/v1/launches/:id | AuthSession (Admin) |
POST | Path/v1/launches/:id/token | AuthSession (Admin) |
POST | Path/v1/launches/:id/check | AuthSession (Member+) |
POST | Path/v1/launches/:id/mint-authorizations | AuthSession (Admin) |
POST | Path/v1/launches/:id/distributions | AuthSession (Admin) |
POST | Path/v1/launches/:id/nav | AuthSession (Admin) |
POST | Path/v1/launches/:id/retire | AuthSession (Admin) |
Locking a launch and every action that signs something are Admin, because
each one writes something permanent and the launch fee comes out of the
organization's shared balance. Reading a launch is Viewer, and taking a check
is Member — neither produces a signed artifact. A role refusal is a 403; the
portal renders it as "your role cannot lock a launch" with the role named,
rather than as a failure.
PATCH accepts the rotatable fields only: the custodian endpoint, its
credential, the pointer, the decimals and the interval. The backing rule and
the asset are not among them, at any role.
POST /v1/launches/:id/token succeeds once; a second call answers 409 already_bound, because re-binding would invalidate every authorization already
signed against the first address.
Webhooks: launch.paused, launch.resumed, launch.mint_authorized,
launch.fee_overdue. Payloads are content-free — ids, the public labels, the
check sequence and the figures that are already on the public page.
Verify a launch yourself
Nothing below asks you to trust the page you are reading it on.
SLUG=acme-tokenized-aapl
# 1. the record, including the exact bytes the gateway hashed
curl -s https://api.buildsable.com/v1/launches/public/$SLUG > record.json
# 2. the spec digest is sha256 over those bytes
python3 - <<'PY'
import hashlib, json
r = json.load(open("record.json"))
c = r["spec_canonical"]
if c is None:
print("sealed: this launch has not revealed its spec yet")
else:
print("recomputed:", hashlib.sha256(c.encode()).hexdigest())
print("published :", r["spec_sha256"])
PY
# 3. the commitment is Sable's signature over that digest
curl -s https://api.buildsable.com/v1/receipts/verify \
-H 'content-type: application/json' \
-d "$(python3 -c 'import json;c=json.load(open("record.json"))["commitment"];print(json.dumps({"receipt":c["receipt"],"signature":c["signature"]}))')"
# 4. the signer it recovers to is the same key the contract enforces
curl -s https://api.buildsable.com/v1/receipts/pubkey
# 5. the backing chain, check by check — failures included
curl -s https://api.buildsable.com/v1/pob/public/$SLUG/checks
Step 4 is the one people skip. The contract's verdictSigner is an immutable
field; if it does not equal the address at /v1/receipts/pubkey, then whatever
signed the authorizations was not this gateway, and nothing else on the page
means what it appears to mean.