Overview#
Base URL: https://strikeablock.com/api/v1. Every request and
response is JSON (Content-Type: application/json); every response carries
X-API-Version: 2026-09-10. There is no sandbox: every strike is real work
on real hardware, so buy a small pack and use the Genesis tier
to integrate.
What a strike is. A block header whose double-SHA-256 hash starts with at
least eight hex zeros (Bitcoin's difficulty on 3 January 2009), whose coinbase
transaction carries your inscription, mined on a GekkoScience Compac F
(Bitmain BM1397) ASIC. The block is a standalone artifact: it is not part of
Bitcoin or any public chain, has no monetary value, and is published
anonymously in the vault with a page at
strikeablock.com/b/<block_id> that re-verifies the proof of work in the
visitor's browser.
Shape of an integration.
One physical machine strikes everything, first in, first out. Your strike never gets lost: if the foundry is offline it waits in the queue and is struck when the chip is back.
Quickstart#
- Get a key. Sign in at the developer console, create an API key, copy it (it is shown once).
- Buy credits on the same page (a $10 pack is enough for a Genesis strike). 1 credit = $1; a Genesis strike costs 7.
- Create a strike:
curl https://strikeablock.com/api/v1/strikes \
-H "Authorization: Bearer sab_live_…" \
-H "Content-Type: application/json" \
-d '{"text": "For Mom, 1958-2024", "zeros": 8,
"webhook_url": "https://example.com/strikeablock",
"metadata": {"order": "A-1042"}}'
# 202 Accepted
{
"id": "k3Jx9…", "object": "strike", "status": "queued",
"text": "For Mom, 1958-2024", "zeros": 8, "tier": "genesis",
"credits_charged": 7, "credits_remaining": 18, "queue_ahead": 0,
"webhook_url": "https://example.com/strikeablock", "metadata": {"order": "A-1042"},
"block": null, "url": null, "created_at": "2026-09-10T18:04:11.201Z", …
}
- Wait for it. Either poll
GET /v1/strikes/{id}every few seconds untilstatusis terminal, or handle thestrike.donewebhook. When it lands:
{
"id": "k3Jx9…", "status": "done", "credits_charged": 7, "refunded": false,
"queued_ms": 1840, "mine_ms": 2210,
"block_id": "lHKO2Epan2YzwndTyFOd",
"url": "https://strikeablock.com/b/lHKO2Epan2YzwndTyFOd",
"block": {
"hash": "000000002bfdd443b79ac22e8abc216d1c055598fb581c42113d8e6c0f69afec",
"zeros": 8, "zeros_deep": 8, "nonce": 1930422341, "nonce_hex": "0x730fe845",
"nbits": "0x1d00ffff", "timestamp": 1757527451,
"header_hex": "01000000…", "coinbase_tx_hex": "01000000…", "merkle_root": "…",
"target": "00000000ffff0000…", "device": "compacf-GS10061433", "mine_ms": 2210, …
}
}
That is the whole loop. The rest of this page is detail.
Authentication#
Send your key as a bearer token on every request:
Authorization: Bearer sab_live_Ab3dEf…
- Keys look like
sab_live_+ 32 characters. They are stored only as a SHA-256 hash; if you lose one, create another and revoke the old one. - Up to 10 active keys per account. Keys are created and revoked in the console (or via the keys endpoints with a sign-in token); a key cannot mint other keys.
- Keys never expire. Revocation takes effect on the next request.
- Never ship a key in a browser or mobile app. Anyone holding it can spend your credits.
Requests without a valid key get 401 with error.type =
"authentication_error". Two endpoints need no key at all:
GET /v1/pricing and the public block endpoints.
Credits & pricing#
Strikes are prepaid in credits. One credit is one US dollar. The price of a strike is its depth, the number of leading hex zeros you ask for. Every extra zero is sixteen times the expected work (the depth ladder has the math).
| zeros | tier | credits | work vs. Bitcoin's block 0 | typical time once mining |
|---|---|---|---|---|
| 00000000 | genesis | 7 | 1× (the original bar) | seconds |
| 0000000000 | gold | 10 | 256× | under a minute |
| 00000000000 | platinum | 29 | 4,096× | several minutes |
| 000000000000 | prismatic | 99 | 65,536× | one to three hours |
Nine zeros is not for sale: one in sixteen Genesis strikes lands a ninth zero by
luck, and the block keeps it (zeros_deep reports what actually landed).
GET /v1/pricing returns this table as JSON so you never hard-code it.
Returns {"object": "balance", "credits": 18}.
Buying credits#
The simplest way is the console. From code, create a Stripe Checkout session and send the buyer to its URL:
| field | type | notes |
|---|---|---|
| credits | integer | required. 10 to 1000. Charged at $1.00 each. |
| success_url | string | https URL to return to after payment. Include the literal {CHECKOUT_SESSION_ID} and Stripe substitutes the session id. Default: the console. |
| cancel_url | string | https URL for an abandoned checkout. Default: the console. |
{
"object": "checkout_session", "id": "cs_live_…",
"url": "https://checkout.stripe.com/c/pay/cs_live_…",
"credits": 25, "amount_cents": 2500, "currency": "usd",
"expires_at": "2026-09-10T19:04:11.000Z", "status": "open"
}
Credits are added the moment Stripe reports the session paid, three ways,
each safe to overlap: a Stripe webhook into this API, the buyer returning to
success_url, and a periodic reconciliation by the foundry. If your own
success_url receives the buyer, call claim to credit immediately
rather than waiting for reconciliation:
{"session_id": "cs_live_…"}
# →
{"object": "credit_claim", "session_id": "cs_live_…", "applied": true,
"reason": null, "credits": 25, "balance": 43, "payment_status": "paid"}
applied is false with reason "already_applied" (harmless)
or "unpaid" (try again in a moment). A session bought by a different
account is refused with 403.
Every balance change, newest first: kind is purchase, strike,
refund or grant, with delta, balance_after, and
strike_id where relevant. Paginated with limit (≤ 200) and
starting_after=<entry id>.
Refunds#
Credits are deducted when a strike is created and given back automatically
when it does not land: status rejected (content, junk, or a duplicate of
a block that landed first) or failed (the chip exhausted its search budget
without a solution; rare, and never charged). The strike then shows
"refunded": true and the ledger gets a refund row. Credits themselves
are not refundable to cash except as required by law; see the
terms.
Strikes#
Create a strike#
| field | type | notes |
|---|---|---|
| text | string | required. The inscription, 3 to 90 characters after whitespace is collapsed. Any Unicode; no control characters. Written into the coinbase transaction exactly as sent and shown publicly in the vault. Must not already exist in the vault (409 duplicate_inscription) and must pass the content policy (422). |
| zeros | integer | 8 (default), 10, 11 or 12. Sets the price and the target; see pricing. |
| webhook_url | string | https URL to notify for this strike. Overrides the account webhook (if any) for this strike only. Signed with your account's secret. |
| metadata | object | Up to 10 keys ([A-Za-z0-9_.-], ≤ 40 chars) with string, number or boolean values (≤ 200 chars). Echoed back on the strike and in webhooks; never shown publicly. |
| string | Optional. Emails the block certificate (hash, nonce, full header, coinbase transaction, JSON attachment) to this address when the strike lands. Only that one message; never marketing. |
| header | notes |
|---|---|
| Idempotency-Key | Optional, ≤ 100 printable ASCII characters. See idempotency. |
On success the response is 202 Accepted with a Location header
pointing at the strike, the strike object, plus two
create-time extras: credits_remaining and queue_ahead (strikes in front
of yours across all customers). Validation and moderation happen before any
credits move: a 4xx never charges you.
Idempotency-Key so a retried
request cannot strike twice.The strike object#
| field | type | meaning |
|---|---|---|
| id | string | Strike id. Use it to poll. |
| object | string | Always "strike". |
| status | string | queued · mining · done · rejected · failed. The last three are terminal. |
| text | string | The inscription as it will be (or was) struck. |
| zeros | integer | Depth requested (8, 10, 11, 12). |
| tier | string | genesis · gold · platinum · prismatic. |
| credits_charged | integer | Credits deducted at creation. |
| refunded | boolean | True once a rejected/failed strike's credits are back. |
| reason | string|null | On rejected: content, junk, duplicate or invalid. Otherwise null. |
| created_at | ISO 8601 | When you created it. |
| started_at | ISO 8601|null | When the chip picked it up (status left queued). |
| completed_at | ISO 8601|null | When it reached a terminal status. |
| queued_ms | integer|null | Time spent waiting for the chip. |
| mine_ms | integer|null | Wall-clock time of the strike itself. |
| attempts | integer | Mining passes so far (a pass that finds no solution is retried with fresh timestamps). |
| queue_ahead | integer | Only while queued: strikes ahead of this one. |
| webhook_url | string|null | Where this strike's events go. |
| metadata | object | Your metadata, verbatim. |
| block_id | string|null | Set on done. |
| block | object|null | The full block object on done. |
| url | string|null | The block's public page on done. |
Lifecycle & timing#
queued. Waiting for the single ASIC, first in first out across all customers.
Usually seconds; longer if a deep strike is ahead of you (queue_ahead tells
you how many). Moderation runs again here as a backstop.
mining. The chip sweeps all 4.3 billion nonces of a header in about 21 milliseconds. A header only contains a solution part of the time, so the foundry rolls the timestamp and tries fresh headers until one lands. Expected durations once mining starts:
| zeros | typical | search budget before failed |
|---|---|---|
| 8 | 2–10 seconds | about a minute across retries (P(fail) ≈ 2%, then requeued up to three times) |
| 10 | under a minute | a few minutes |
| 11 | several minutes | up to ~45 minutes per attempt |
| 12 | one to three hours | up to ~5 hours per attempt; may be requeued once more |
Mining is a lottery; a strike can land early or late. Design for
done arriving any time from seconds to hours, and never block a user-facing
request on it.
done. The block is published to the vault, its page and social image exist, the
certificate email (if requested) is queued, and the webhook fires. rejected
and failed refund the credits before the webhook fires, so
refunded is already true in the event.
Retrieve & list#
The strike, with block joined once done and queue_ahead while
queued. Strikes belong to the account that created them; any other account
gets 404.
Your strikes, newest first. Query parameters: limit (default 20, max
100), status (filter to one status), starting_after=<strike id> for the
next page. The list omits block; fetch a strike for the full block.
{"object": "list", "data": [ …strikes… ], "has_more": true, "next_cursor": "k3Jx9…"}
Idempotency#
Send an Idempotency-Key header (any unique string per intended strike, an
order id works well) with POST /v1/strikes. A retry with the same key and
the same account returns the original strike with 200 and the header
Idempotent-Replayed: true, and charges nothing. Keys are scoped per account
and kept indefinitely. Because the vault also refuses duplicate inscriptions,
a retried strike without the header would be rejected (and refunded) rather
than struck twice, but the header spares you the round trip.
Polling#
Polling works well for genesis and gold strikes and as a fallback for the rest. Guidance:
- Poll
GET /v1/strikes/{id}; stop whenstatusisdone,rejectedorfailed. - Intervals: every 3–5 s for 8 zeros, 10 s for 10, 30 s for 11, 60 s or more for 12. Add jitter. A few hundred polls per strike is fine; a poll every second across many strikes will hit the rate limit.
- While
queued,queue_aheadis a good progress hint to show users. - Honor
429with itsRetry-Afterheader, and treat5xxas transient.
# Python
import time, requests
H = {"Authorization": "Bearer sab_live_…"}
s = requests.post("https://strikeablock.com/api/v1/strikes", headers=H,
json={"text": "For Mom, 1958-2024"}).json()
while s["status"] in ("queued", "mining"):
time.sleep(4)
s = requests.get(f"https://strikeablock.com/api/v1/strikes/{s['id']}", headers=H).json()
print(s["status"], s.get("url"))
Webhooks#
A webhook is an HTTPS POST from the foundry to you the moment a strike
reaches a terminal status. Every delivery is signed with your account's
secret, retried for about a day, and inspectable afterwards.
Configuring#
Two levels, and you can use both: an account webhook receives every
strike's events unless the strike names its own webhook_url, which then
receives that strike's events instead. Both are signed with the one account
secret.
{"url": "https://example.com/strikeablock", "rotate_secret": false}
# →
{"object": "webhook", "url": "https://example.com/strikeablock",
"secret": "whsec_…", "events": ["strike.done", "strike.rejected", "strike.failed"],
"signature_header": "X-SAB-Signature", "updated_at": "…"}
The secret is created on first use and returned by GET /v1/webhook whenever
you need it. Pass "rotate_secret": true to replace it; deliveries queued
after that moment use the new one.
Removes the account URL (the secret stays, so per-strike URLs keep working).
Queues a strike.test event carrying a sample done strike (Satoshi's
block 0) to your account URL, or to a {"url": …} you pass. It goes through
the same signing and retry machinery as a real event, so use it to prove your
verifier end to end. Returns 202 with a delivery_id.
Events & payload#
| type | when |
|---|---|
| strike.done | The block landed. data.strike.block is the full block. |
| strike.rejected | Moderation or duplicate at mining time; credits already refunded. data.strike.reason says why. |
| strike.failed | The search budget ran out; credits already refunded. |
| strike.test | Only from POST /v1/webhook/test; livemode is false. |
POST /strikeablock HTTP/1.1
Content-Type: application/json
User-Agent: StrikeABlock-Webhooks/1.0 (+https://strikeablock.com/docs.html#webhooks)
X-SAB-Event: strike.done
X-SAB-Strike-Id: k3Jx9…
X-SAB-Delivery-Id: 7Qm…
X-SAB-Attempt: 1
X-SAB-Signature: t=1757527456,v1=5f8c…e21a
{
"id": "evt_…", "object": "event", "type": "strike.done",
"api_version": "2026-09-10", "created": 1757527456, "livemode": true,
"data": { "strike": { …the strike object, with block… } }
}
Respond with any 2xx within 10 seconds. Do your real work after
acknowledging (queue it); a slow handler is retried as a failure and you will
see the same event twice.
Verifying signatures#
X-SAB-Signature is t=<unix seconds>,v1=<hex> where v1 is
HMAC-SHA256 with your secret over the string <t>.<raw body>. Verify
against the raw request bytes, not a re-serialized object, and reject
timestamps older than a few minutes to blunt replays. This is the scheme
Stripe uses, so an existing verifier ports in one line.
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], expected)
# Flask
@app.post("/strikeablock")
def hook():
if not verify(SECRET, request.headers["X-SAB-Signature"], request.get_data()):
return "", 400
event = request.get_json()
if event["type"] == "strike.done":
mark_struck(event["data"]["strike"]["metadata"]["order"],
event["data"]["strike"]["url"])
return "", 200
const crypto = require("crypto");
function verify(secret, header, rawBody, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const t = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - t) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
return parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
// Express: keep the raw body
app.post("/strikeablock", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(SECRET, req.get("X-SAB-Signature"), req.body)) return res.sendStatus(400);
const event = JSON.parse(req.body);
if (event.type === "strike.done") markStruck(event.data.strike.metadata.order, event.data.strike.url);
res.sendStatus(200);
});
Handle events idempotently: key on data.strike.id (or the event
id) and ignore a repeat. Retries and the test endpoint can both deliver
the same event more than once.
Retries & deliveries#
A delivery that does not get a 2xx within 10 seconds (timeouts, connection
errors, redirects and 4xx/5xx included) is retried on this schedule
after the previous attempt: 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h. Eight
attempts in all, about a day; after that the delivery is failed and the
strike is still there to poll. Deliveries are sent from the
foundry itself (not from a cloud IP range), so allow-listing by IP is not
supported; verify signatures instead.
{"object": "list", "data": [{
"id": "7Qm…", "object": "webhook_delivery", "strike_id": "k3Jx9…",
"event": "strike.done", "url": "https://example.com/strikeablock",
"state": "delivered", // pending · delivered · failed
"attempts": 2, "next_attempt_at": null,
"last_attempt_at": "…", "delivered_at": "…",
"last_status": 200, "last_error": null, "created_at": "…"
}], "has_more": false, "next_cursor": null}
Blocks & verification#
Any public block, by id, or the vault newest first (limit ≤ 100,
starting_after). Cacheable for a minute. The block object:
| field | meaning |
|---|---|
| id | Block id; the page is https://strikeablock.com/b/{id} (also in url). |
| text | The inscription in the coinbase. |
| hash | Double-SHA-256 of the 80-byte header, displayed big-endian as Bitcoin does. 64 hex chars. |
| zeros / zeros_deep | Depth requested / leading zeros actually present (luck can add one). |
| nonce / nonce_hex | The winning nonce. |
| nbits / target | Compact difficulty and the expanded 256-bit target (hex). A valid block has hash ≤ target. |
| timestamp | Header nTime (unix). Deep strikes roll this forward, so it can run ahead of the clock. |
| header_hex | The full 80-byte header: version · prev-hash (zeros; it is a genesis block) · merkle root · time · nBits · nonce. |
| coinbase_tx_hex | The coinbase transaction carrying the inscription. Its txid is the merkle root. |
| merkle_root | Big-endian merkle root taken from the header. |
| device | The chip that struck it. |
| mine_ms / mined_at | How long the strike took and when it landed. |
Verify it yourself. Trust nothing you have not hashed: the proof is two SHA-256 calls away.
# Python
import hashlib
b = requests.get("https://strikeablock.com/api/v1/blocks/lHKO2Epan2YzwndTyFOd").json()
header = bytes.fromhex(b["header_hex"])
digest = hashlib.sha256(hashlib.sha256(header).digest()).digest()[::-1].hex()
assert digest == b["hash"]
assert int(digest, 16) <= int(b["target"], 16)
assert b["text"].encode() in bytes.fromhex(b["coinbase_tx_hex"])
The same check runs in every visitor's browser on the block page, which is also what the Block Coin's NFC chip opens.
Account & keys#
{"object": "account", "id": "…", "email": "you@example.com",
"credits": 18, "strikes_total": 12, "pending_strikes": 1,
"webhook": {"url": "https://example.com/strikeablock", "configured": true},
"created_at": "…", "auth": "key", "pricing": { …the pricing object… }}
Key management is meant for the console, which calls
these endpoints with a Google sign-in token rather than an API key. An API
key cannot create or revoke keys (403 portal_only).
A created key returns key exactly once; listings show only prefix
(sab_live_Ab3dEf…), name, created_at, last_used_at (updated at
most once a minute) and revoked_at.
Errors#
Errors are JSON with a stable machine-readable code and a human message:
{"error": {"type": "insufficient_credits", "code": "insufficient_credits",
"message": "This strike costs 10 credits; your balance is 3. …",
"required": 10, "balance": 3}}
| status | type | codes |
|---|---|---|
| 400 | invalid_request_error | text_required text_too_short text_too_long text_control_chars invalid_zeros invalid_webhook_url invalid_email invalid_metadata invalid_json body_too_large invalid_idempotency_key invalid_credits invalid_success_url invalid_cancel_url invalid_session_id not_credits no_webhook. The offending field is in param. |
| 401 | authentication_error | missing_key invalid_key revoked_key invalid_token |
| 402 | insufficient_credits | insufficient_credits with required and balance. |
| 403 | authentication_error | portal_only wrong_account |
| 404 | not_found | resource_missing unknown_route unknown_version |
| 409 | invalid_request_error | duplicate_inscription (with existing_block_id and existing_block_url) queue_full too_many_keys |
| 422 | moderation_error | rejected_content rejected_junk; reason repeats the short form. |
| 429 | rate_limit_error | rate_limited, with a Retry-After header in seconds. |
| 502 / 503 | api_error | stripe_error (Stripe declined or was unreachable) · purchases_unavailable (checkout not configured). |
| 500 | api_error | internal. Retry with backoff; email us if it persists. |
Rate limits & quotas#
- 60 requests per minute per account, sustained, on authenticated endpoints.
Public endpoints are not counted. Over the limit you get
429andRetry-After. - 25 pending strikes per account (
queued+mining). A 26th is refused with409 queue_fulluntil some finish. This protects one physical chip; if you need more throughput, write to us. - Request bodies are limited to 16 KB. Inscriptions to 90 characters. Metadata to 10 keys.
- Purchases: 10 to 1000 credits per checkout, as many checkouts as you like.
Every strike you queue is a promise of real hardware time, so the API is
generous with reads and careful with writes. Cache GET /v1/pricing; it
changes rarely and any change lands here in the changelog first.
End-to-end examples#
Python: strike, wait by webhook, verify
import hashlib, hmac, time, requests
from flask import Flask, request
API = "https://strikeablock.com/api/v1"
H = {"Authorization": "Bearer sab_live_…"}
SECRET = requests.get(f"{API}/webhook", headers=H).json()["secret"] # after PUT /v1/webhook once
def strike(order_id: str, text: str, zeros: int = 8) -> dict:
r = requests.post(f"{API}/strikes", headers={**H, "Idempotency-Key": order_id},
json={"text": text, "zeros": zeros, "metadata": {"order": order_id}})
if r.status_code == 402:
raise RuntimeError("top up: " + r.json()["error"]["message"])
r.raise_for_status()
return r.json()
app = Flask(__name__)
@app.post("/strikeablock")
def hook():
sig = dict(p.split("=", 1) for p in request.headers["X-SAB-Signature"].split(","))
expected = hmac.new(SECRET.encode(), f"{sig['t']}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig["v1"], expected) or abs(time.time() - int(sig["t"])) > 300:
return "", 400
ev = request.get_json(); s = ev["data"]["strike"]
if ev["type"] == "strike.done":
b = s["block"]
h = hashlib.sha256(hashlib.sha256(bytes.fromhex(b["header_hex"])).digest()).digest()[::-1].hex()
assert h == b["hash"] and int(h, 16) <= int(b["target"], 16)
fulfil(s["metadata"]["order"], s["url"], b["hash"])
elif ev["type"] in ("strike.rejected", "strike.failed"):
notify_customer(s["metadata"]["order"], s["reason"] or "unlucky") # credits already back
return "", 200
Node: strike and poll
const API = "https://strikeablock.com/api/v1";
const H = { Authorization: "Bearer sab_live_…", "Content-Type": "application/json" };
async function strikeAndWait(text, zeros = 8) {
let r = await fetch(`${API}/strikes`, { method: "POST", headers: H, body: JSON.stringify({ text, zeros }) });
if (!r.ok) throw new Error((await r.json()).error.message);
let s = await r.json();
const every = { 8: 4000, 10: 10000, 11: 30000, 12: 60000 }[zeros];
while (s.status === "queued" || s.status === "mining") {
await new Promise(res => setTimeout(res, every + Math.random() * 1000));
r = await fetch(`${API}/strikes/${s.id}`, { headers: H });
if (r.status === 429) { await new Promise(res => setTimeout(res, 1000 * (+r.headers.get("retry-after") || 5))); continue; }
s = await r.json();
}
return s; // done (with s.block, s.url) · rejected · failed
}
Content policy#
The vault is public and permanent, so every inscription is gated twice: at
POST time (a 422 that costs nothing) and again by the foundry before
the chip spends a hash (a rejected strike, refunded). The gate is a
wordlist, not a judge: slurs, strong profanity and sexual terms are refused
(content); filler like test, keyboard mashes and repeated characters
are refused (junk); an inscription already in the vault is refused
(duplicate). Mild language, any script, dates, names and numbers are all
fine. Do not inscribe other people's personal data, anything you do not have
the right to publish, or anything illegal; blocks can be hidden from the vault
after the fact for those reasons, and the terms govern.
If your use case needs private (unlisted) blocks,
tell us.
Versioning & changelog#
The API is versioned by date; the current version is 2026-09-10 and is
echoed in X-API-Version and in every webhook's api_version. Additive
changes (new fields, new event types, new endpoints) ship without a version
bump; anything that would break a correct client gets a new dated version and
notice by email to every account with an active key. The full machine-readable
contract is the OpenAPI 3.1 document.
- 2026-09-10: first public version. Strikes, credits, webhooks, public blocks.
Questions, higher limits, private blocks, or a use case we should hear about: hello@strikeablock.com.