| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Stackflow is a payment channel network built on Stacks that enables off-chain, non-custodial, high-speed payments between users and is designed to be simple, secure, and efficient. It supports payments in STX or SIP-010 fungible tokens.
Note
The Stackflow trait is published by stackflow.btc on mainnet at SP126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT6AD08RV.stackflow-token-0-5-0 and on testnet at ST126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT59ZTE2J.stackflow-token-0-5-0.
Note
The official Stackflow contract for STX is published by stackflow.btc on mainnet at SP126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT6AD08RV.stackflow-0-6-0 and on testnet at ST126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT59ZTE2J.stackflow-0-6-0. Contracts for other tokens should copy this contract exactly to ensure that all Stackflow frontends will support it.
Note
The official Stackflow contract for sBTC is published by stackflow.btc on mainnet at SP126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT6AD08RV.stackflow-sbtc-0-6-0 and on testnet at ST126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT59ZTE2J.stackflow-sbtc-0-6-0.
A Stacks address can open a Pipe with any other Stacks address or open a Tap to the Reservoir by calling the Stackflow contract. In a Pipe, tokens are sent into escrow and locked in the contract, and can only be withdrawn by the Pipe participants. Alternatively, by opening a Tap, a user deposits tokens directly into the contract where their balance is tracked on a shared ledger (the Reservoir). Users can then send payments off-chain using either direct Pipes or via the Reservoir.
In both cases, off-chain signed messages are exchanged to update balances. These messages include a nonce to track their ordering. SIP-018 structured data indicating the agreed upon balances of each participant. These signed messages can be exchanged off-chain and only need to be submitted on-chain when closing the Pipe, depositing additional funds to, or withdrawing funds from the Pipe. The messages include a nonce to track their ordering. The SIP-018 domain is bound to the specific StackFlow contract principal, so signatures are not reusable across different StackFlow contract instances.
A Pipe can be closed cooperatively at any time, with both parties agreeing on the final balances and signing off on the closure. If either party becomes uncooperative or unresponsive, the Pipe can also be forcefully closed by the other party. One party can initiate a waiting period to cancel the Pipe and refund the latest balances recorded in the contract, or they can submit signatures verifying an agreed upon balance. In either case, the counterparty has 144 Bitcoin blocks (~1 day) to rebut the submitted balances by providing a newer set of signed messages. If the closure is successfully disputed with valid signatures, those balances are immediately paid out and the Pipe is closed. If the deadline passes with no dispute, a final contract call triggers the payout to both parties.
Pipes can be chained together to enable payments between users who do not have a direct Pipe open between them. This is done by routing the payment through intermediaries who have Pipes open with both the sender and receiver. Off-chain, tools can find the most efficient path to route the payment. Intermediary nodes can charge a fee for routing payments. In the case of chained Pipes, the signed messages passed around include a requirement for a secret to unlock the funds. This ensures that if any hop in the chain fails to send the tokens as promised, the whole transaction can be reverted, allowing the sender and receiver to be sure that the payment goes through completely or they get their tokens back, even with untrusted intermediaries.
Example:
In the above scenario, Bob cannot obtain the secret unless he sends the payment to Charlie, so he is unable to keep the payment from Alice without sending the payment to Charlie. Details of how this works are discussed below.
To send tokens between two parties with an active Pipe open in the contract, they simply send signatures back and forth off-chain. The signatures are signing over structured data with the details of the Pipe:
{
;; The token for which the Pipe is open
token: (optional principal),
;; The first principal, ordered by the principals' consensus serialization
principal-1: principal,
;; The second principal, ordered by the principals' consensus serialization
principal-2: principal,
;; The balance of principal-1
balance-1: uint,
;; The balance of principal-2
balance-2: uint,
;; A monotonically increasing identifier for this action
nonce: uint,
;; The type of action (u0 = close, u1 = transfer, u2 = deposit, u3 = withdraw)
action: uint,
;; The principal initiating the action, `none` for close
actor: (optional principal),
;; An optional sha256 hash of a secret, used for multi-hop transfers or
;; other advanced functionality
hashed-secret: (optional (buff 32))
;; An optional Bitcoin block height at which this transfer becomes valid. If
;; set, this transfer cannot be used in `force-close` or `dispute-closure`
;; to validate balances until the specified block height has passed.
valid-after: (optional uint)
}In this direct transfer scenario, the hashed-secret is not needed, and can be set to none.
When A wants to send to B, A builds this data structure with the adjusted balances and the appropriate nonce, signs over it, and then sends these in a message to B. B validates the signature, and verifies that the new balances match an incoming transfer. If everything is correct, then B responds to A with a signature of its own. Both parties (or a representative thereof) should record these new signatures and balances. Future transactions should build from these balances, and the signatures may be needed to dispute if something goes wrong later.
When principal A wants to send tokens to principal D using StackFlow, first, a path from A to D is identified using existing open Pipes with sufficient balances, for example using Dijkstra's algorithm. If a path is not possible, then a new Pipe must be opened. Once a path is defined, A builds the transfer message as described above, with the addition of a secret.
The next part of the transfer message describes what the receiver must do in order to get the Pipe balance confirmed. For the final hop in the chain, the target receiver of the payment will have no next hop, since at that point, the payment is complete. For all other hops in the chain, the receiver of the message uses the next hop to determine how to build its transfer message, defining which principal to send to, receiver, how much to send, amount, how much should be leftover to keep for itself as a fee, fee, an encrypted secret, secret.
type Hop = {
receiver: string;
amount: bigint;
fee: bigint;
secret: Uint8Array;
nextHop: Number;
};The initial sender constructs an array of 8 of these Hop objects, encrypting each with the public key of the intended target. If less than 8 hops are needed, then random data is filled into the remaining slots. The sender then sends this array, along with its signature to the first hop in the chain, along with the index for that hop. That first hop decrypts the value at the specified hop and uses the information within to send a signature and the array to the next hop, along with the index for that next hop.
For the final hop in the chain, D in our example, the receiver is set to D itself and the amount is set to 0. This is the signal to D that it is the final destination for this payment, so it can reply back to the previous hop with the decrypted secret. Each hop validates, then passes this decrypted secret back to its predecessor, eventually reaching the source and validating the completed payment.
There are two ways to close a Pipe, cooperatively or forcefully. In the normal scenario, both parties agree to close the Pipe and authorize the closure by signing a message agreeing upon the final balances. One party calls close-pipe, passing in these balances and signatures, and then the contract pays out both parties and the Pipe is closed.
In the unfortunate scenario where one party becomes uncooperative or unresponsive, the other party needs a way to retrieve their funds from the Pipe unilaterally. This user has two options:
During the waiting period for both of these closures, the other party may call dispute-closure, passing in balances and signatures from the latest transfer. If the signatures are confirmed, and the nonce is higher in the case of a force-close, then the Pipe is immediately closed, transferring with the balances specified to both parties.
If the closure is not disputed by the time the waiting period is over, the user may call finalize to complete the closure and transfer the appropriate balances to both parties.
This repo now includes a minimal stackflow-node service at server/src/index.ts. It is designed to run as a Stacks-node event observer, ingest print events from Stackflow contracts, store latest signed states, and auto-submit dispute-closure-for when a force-close or force-cancel is observed.
Run it with:
npm run stackflow-nodeOptional environment variables:
STACKFLOW_NODE_HOST=127.0.0.1
STACKFLOW_NODE_PORT=8787
STACKFLOW_NODE_DB_FILE=server/data/stackflow-node-state.db
STACKFLOW_NODE_MAX_RECENT_EVENTS=500
STACKFLOW_NODE_LOG_RAW_EVENTS=false
STACKFLOW_CONTRACTS=ST....stackflow-0-6-0,ST....stackflow-sbtc-0-6-0
STACKFLOW_NODE_PRINCIPALS=ST...,ST...
STACKS_NETWORK=devnet
STACKS_API_URL=http://localhost:20443
STACKFLOW_NODE_DISPUTE_SIGNER_KEY=<private-key-used-to-submit-disputes>
STACKFLOW_NODE_COUNTERPARTY_KEY=<private-key-used-to-sign-off-chain-states>
STACKFLOW_NODE_COUNTERPARTY_PRINCIPAL=<optional principal to sign for, e.g. ST... or ST....contract>
STACKFLOW_NODE_COUNTERPARTY_SIGNER_MODE=local-key
STACKFLOW_NODE_COUNTERPARTY_KMS_KEY_ID=<aws-kms-key-id-or-arn>
STACKFLOW_NODE_COUNTERPARTY_KMS_REGION=<aws-region>
STACKFLOW_NODE_COUNTERPARTY_KMS_ENDPOINT=<optional custom endpoint, e.g. localstack>
STACKFLOW_NODE_STACKFLOW_MESSAGE_VERSION=0.6.0
STACKFLOW_NODE_SIGNATURE_VERIFIER_MODE=readonly
STACKFLOW_NODE_DISPUTE_EXECUTOR_MODE=auto
STACKFLOW_NODE_DISPUTE_ONLY_BENEFICIAL=false
STACKFLOW_NODE_PEER_WRITE_RATE_LIMIT_PER_MINUTE=120
STACKFLOW_NODE_TRUST_PROXY=false
STACKFLOW_NODE_OBSERVER_LOCALHOST_ONLY=true
STACKFLOW_NODE_OBSERVER_ALLOWED_IPS=127.0.0.1,192.0.2.10
STACKFLOW_NODE_ADMIN_READ_TOKEN=<optional bearer/admin token for sensitive reads>
STACKFLOW_NODE_ADMIN_READ_LOCALHOST_ONLY=true
STACKFLOW_NODE_REDACT_SENSITIVE_READ_DATA=true
STACKFLOW_NODE_FORWARDING_ENABLED=false
STACKFLOW_NODE_FORWARDING_MIN_FEE=0
STACKFLOW_NODE_FORWARDING_TIMEOUT_MS=10000
STACKFLOW_NODE_FORWARDING_ALLOW_PRIVATE_DESTINATIONS=false
STACKFLOW_NODE_FORWARDING_ALLOWED_BASE_URLS=https://node-b.example.com,http://127.0.0.1:9797
STACKFLOW_NODE_FORWARDING_REVEAL_RETRY_INTERVAL_MS=15000
STACKFLOW_NODE_FORWARDING_REVEAL_RETRY_MAX_ATTEMPTS=20If STACKFLOW_CONTRACTS is omitted, the stackflow-node automatically monitors any contract identifier matching *.stackflow*. When set, STACKFLOW_CONTRACTS entries are trimmed of whitespace and matched case-sensitively (Stacks contract identifiers are case-sensitive). The current implementation uses Node's node:sqlite module for persistence. STACKFLOW_NODE_SIGNATURE_VERIFIER_MODE supports readonly (default), accept-all, and reject-all. Non-readonly modes are intended for testing. STACKFLOW_NODE_DISPUTE_EXECUTOR_MODE supports auto (default), noop, and mock. mock is intended for local integration testing. STACKFLOW_NODE_COUNTERPARTY_SIGNER_MODE supports local-key (default) and kms. For kms, set STACKFLOW_NODE_COUNTERPARTY_KMS_KEY_ID; the server derives the signer address from the KMS public key at startup. KMS mode requires the AWS KMS SDK package: npm install @aws-sdk/client-kms. Set STACKFLOW_NODE_LOG_RAW_EVENTS=true to print raw stackflow print event objects received via /new_block for payload inspection/debugging. STACKFLOW_NODE_PEER_WRITE_RATE_LIMIT_PER_MINUTE applies per-IP write limits to POST /signature-states, POST /counterparty/transfer, and POST /counterparty/signature-request (0 disables rate limiting). STACKFLOW_NODE_TRUST_PROXY defaults to false; when enabled, the server uses x-forwarded-for for client IP extraction (rate limiting and localhost checks). STACKFLOW_NODE_HOST defaults to 127.0.0.1 to reduce accidental network exposure. Use a public bind only with hardened ingress controls. STACKFLOW_NODE_PORT must be a valid TCP port (1-65535) and fails fast on invalid values. STACKFLOW_NODE_MAX_RECENT_EVENTS is clamped to at least 1 so event pruning cannot be disabled accidentally via negative values. Boolean env vars accept true/false, 1/0, yes/no, and on/off (case-insensitive); invalid boolean text now fails fast to prevent silent misconfiguration. Integer env vars must be plain integer text (for example 10000, not 10s or 12.5); malformed numeric values fail fast instead of being silently coerced. STACKS_NETWORK accepts only mainnet, testnet, devnet, or mocknet (case-insensitive); invalid values fail fast instead of silently defaulting. Observer ingress controls:
STACKFLOW_NODE_OBSERVER_LOCALHOST_ONLY defaults to true and restricts POST /new_block and POST /new_burn_block to loopback sources.
STACKFLOW_NODE_OBSERVER_ALLOWED_IPS (optional CSV) restricts observer routes to explicit source IPs. When set, this allowlist takes precedence over localhost-only mode. Sensitive read controls:
STACKFLOW_NODE_ADMIN_READ_TOKEN (optional) requires this token (via Authorization: Bearer ... or x-stackflow-admin-token) for GET /signature-states and GET /forwarding/payments.
STACKFLOW_NODE_ADMIN_READ_LOCALHOST_ONLY defaults to true; when no admin token is configured, sensitive reads are limited to localhost sources.
STACKFLOW_NODE_REDACT_SENSITIVE_READ_DATA defaults to true; without an admin token, signatures and revealed secrets are redacted in response bodies. STACKFLOW_NODE_FORWARDING_ENABLED enables routed transfer forwarding support. STACKFLOW_NODE_FORWARDING_MIN_FEE sets the minimum forwarding spread: incomingAmount - outgoingAmount. STACKFLOW_NODE_FORWARDING_TIMEOUT_MS controls timeout for next-hop signing calls. STACKFLOW_NODE_FORWARDING_ALLOW_PRIVATE_DESTINATIONS defaults to false; when false, forwarding destinations resolving to loopback/private/link-local/non-public IP ranges are rejected. STACKFLOW_NODE_FORWARDING_ALLOWED_BASE_URLS (optional) restricts allowed next-hop base URLs for forwarding. STACKFLOW_NODE_FORWARDING_REVEAL_RETRY_INTERVAL_MS controls how often pending upstream reveal propagation retries are attempted. STACKFLOW_NODE_FORWARDING_REVEAL_RETRY_MAX_ATTEMPTS sets the maximum reveal propagation attempts before a payment is marked failed. If STACKFLOW_NODE_PRINCIPALS is set, the stackflow-node only:
When STACKFLOW_NODE_PRINCIPALS is omitted:
Health and inspection endpoints:
Sensitive inspection endpoints (/signature-states, /forwarding/payments) return 401 when an admin token is configured but missing/invalid, and return 403 for non-local access when tokenless localhost-only mode is active.
Public deployment hardening checklist:
Counterparty signing endpoints:
/counterparty/transfer signs transfer states (action = 1). /counterparty/signature-request signs close/deposit/withdraw states (action = 0|2|3). For action = 2|3 (deposit/withdraw), include amount in the request body. Both endpoints:
Responses from counterparty endpoints include:
If the per-IP write limit is exceeded, these endpoints return 429 with reason = "rate-limit-exceeded" and a Retry-After header.
Forwarding endpoint:
/forwarding/transfer coordinates a routed transfer by:
Request body shape:
{
"paymentId": "pay-2026-02-28-0001",
"incomingAmount": "1000",
"outgoingAmount": "950",
"hashedSecret": "0x...",
"upstream": {
"baseUrl": "http://127.0.0.1:8787",
"paymentId": "upstream-pay-0001",
"revealEndpoint": "/forwarding/reveal"
},
"incoming": { "...": "same payload as /counterparty/transfer" },
"outgoing": {
"baseUrl": "http://127.0.0.1:9797",
"endpoint": "/counterparty/transfer",
"payload": { "...": "payload sent to next hop counterparty endpoint" }
}
}POST /forwarding/transfer requires the same peer protocol headers as counterparty endpoints. For SSRF hardening, only these forwarding API paths are supported:
Custom endpoint paths are rejected.
POST /forwarding/reveal request body:
{
"paymentId": "pay-2026-02-28-0001",
"secret": "0x...32-byte-preimage"
}The server checks sha256(secret) == hashedSecret for that payment and stores the revealed secret for later dispute/finality workflows. If an upstream route is stored on that payment, the server immediately tries to propagate the reveal upstream and persists retry state in SQLite:
Forwarding retention notes:
Counterparty signature verification uses verify-signature for transfer actions and verify-signature-request for close/deposit/withdraw actions, preserving on-chain validation semantics for each action type.
Signature state ingestion endpoint:
Example payload:
{
"contractId": "ST....stackflow-0-6-0",
"forPrincipal": "ST...",
"withPrincipal": "ST...",
"token": null,
"myBalance": "900000",
"theirBalance": "100000",
"mySignature": "0x...",
"theirSignature": "0x...",
"nonce": "42",
"action": "1",
"actor": "ST...",
"secret": null,
"validAfter": null,
"beneficialOnly": false
}The stackflow-node stores one latest state per (contract, pipe, forPrincipal), replacing only when the incoming nonce is strictly greater. Before storing, the stackflow-node verifies signatures by calling the Stackflow contract read-only function verify-signatures. If validation fails, the request returns 401 and nothing is stored. If the incoming nonce is not strictly higher than the stored nonce for that (contract, pipe, forPrincipal), the request returns 409. If forPrincipal is not in the effective watchlist, the request returns 403. If the per-IP write limit is exceeded, the request returns 429.
On-chain pipe tracking:
Event observer ingestion endpoint:
When Clarinet/stacks-node observer config uses events_keys = ["*"], stacks-node can also call additional observer endpoints. The stackflow-node responds 200 (no-op) for compatibility on:
For Clarinet devnet, set the observer in settings/Devnet.toml:
[devnet]
stacks_node_events_observers = ["host.docker.internal:8787"]Devnet contracts and initialization plan:
Open the built-in UI in your browser:
http://localhost:8787/app
The UI lets you:
This repo includes a starter x402-style gateway at server/src/x402-gateway.ts. It sits in front of your app server and:
Detailed technical design and production guidance:
Run it with:
npm run x402-gatewayRun a full local end-to-end demo (unpaid + direct + indirect):
npm run demo:x402-e2eThe demo script starts:
Then it automatically exercises:
Run an interactive browser demo (click link -> 402 -> sign -> unlock):
npm run demo:x402-browserThis repo now includes a static pipe interaction page at docs/:
It includes forms/buttons for:
To publish with GitHub Pages (no build step):
Browser demo flow:
Notes about the browser demo:
Example browser demo config:
{
"stacksNetwork": "devnet",
"stacksApiUrl": "http://127.0.0.1:20443",
"contractId": "ST1...stackflow",
"priceAmount": "10",
"priceAsset": "STX",
"openPipeAmount": "1000",
"stackflowNodeHost": "127.0.0.1",
"stackflowNodePort": 8787,
"stacksNodeEventsObserver": "host.docker.internal:8787",
"observerLocalhostOnly": false,
"observerAllowedIps": []
}Gateway environment variables:
STACKFLOW_X402_GATEWAY_HOST=127.0.0.1
STACKFLOW_X402_GATEWAY_PORT=8790
STACKFLOW_X402_UPSTREAM_BASE_URL=http://127.0.0.1:3000
STACKFLOW_X402_STACKFLOW_NODE_BASE_URL=http://127.0.0.1:8787
STACKFLOW_X402_PROTECTED_PATH=/paid-content
STACKFLOW_X402_PRICE_AMOUNT=1000
STACKFLOW_X402_PRICE_ASSET=STX
STACKFLOW_X402_STACKFLOW_TIMEOUT_MS=10000
STACKFLOW_X402_UPSTREAM_TIMEOUT_MS=10000
STACKFLOW_X402_PROOF_REPLAY_TTL_MS=86400000
STACKFLOW_X402_INDIRECT_WAIT_TIMEOUT_MS=30000
STACKFLOW_X402_INDIRECT_POLL_INTERVAL_MS=1000
STACKFLOW_X402_STACKFLOW_ADMIN_READ_TOKEN=<optional token for GET /forwarding/payments>The client supplies x-x402-payment containing JSON (or base64url-encoded JSON) for one of these modes:
Example flow (direct):
# 1) Unpaid request gets 402 challenge
curl -i http://127.0.0.1:8790/paid-content
# 2) Build proof header from a local JSON file
PAYMENT_PROOF=$(node -e "const fs=require('node:fs');const v=JSON.parse(fs.readFileSync('proof.json','utf8'));process.stdout.write(Buffer.from(JSON.stringify(v)).toString('base64url'));")
# 3) Paid request is verified by stackflow-node then proxied upstream
curl -i \
-H "x-x402-payment: ${PAYMENT_PROOF}" \
http://127.0.0.1:8790/paid-contentExample flow (indirect):
INDIRECT_PROOF=$(node -e "const v={mode:'indirect',paymentId:'pay-2026-03-01-0001',secret:'0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',expectedFromPrincipal:'ST2...'};process.stdout.write(Buffer.from(JSON.stringify(v)).toString('base64url'));")
curl -i \
-H "x-x402-payment: ${INDIRECT_PROOF}" \
http://127.0.0.1:8790/paid-contentCurrent scaffold scope:
This repo includes an agent-first Stackflow scaffold at packages/stackflow-agent/ for deployments that do not run local stacks-node/stackflow-node.
It provides:
See:
Use these commands depending on what changed:
npm testnpm run test:claritynpm run test:nodeIntegration tests for the HTTP server are opt-in (they spawn a real process and bind a local port):
npm run test:stackflow-node:httpAs discussed in the details, users of Stackflow should run a server to keep track of balances and signatures for its open Pipes, and to receive messages from its counterparts in those Pipes. A reference implementation of this server is provided in https://github.com/obycode/stackflow-server. This server consists of the backend for managing these details, as well as a frontend to provide a simple interface for opening Pipes and interacting with existing Pipes.
| Back | FazBrowse Home | New Git URL |