Document agent BTCPay connect (sandbox, scoped key)

Add the "Connect BTCPay programmatically" agent workflow and a payment_providers:write
extra-scope note to agent.html; correct the "not exposed to agents" section to the
accurate gate (scoped connect is sandbox + non-mainnet only; disconnect and
production/mainnet stay master-only). Fix the BTCPay permission list in install.html to
the five permissions the daemon actually requests, and point operators at the agent path.
This commit is contained in:
Keysat
2026-06-17 09:32:21 -05:00
parent 47facc8909
commit 7e6f752462
2 changed files with 31 additions and 4 deletions
+26 -3
View File
@@ -88,7 +88,10 @@ curl https://your-keysat-host/v1/admin/licenses?status=active \
<tr><td><code>full-admin</code></td><td>Every scope. Equivalent to the master key for most endpoints.</td></tr> <tr><td><code>full-admin</code></td><td>Every scope. Equivalent to the master key for most endpoints.</td></tr>
</tbody> </tbody>
</table> </table>
<p>Endpoints that touch settings (operator name, payment provider connections, self-license activation, scoped API key management) always require the master admin key. A <code>full-admin</code> scoped key cannot, for example, generate another scoped key. That's a self-defeating elevation path.</p> <p>Endpoints that touch settings (operator name, self-license activation, scoped API key management) always require the master admin key. A <code>full-admin</code> scoped key cannot, for example, generate another scoped key. That's a self-defeating elevation path.</p>
<h3>A-la-carte extra scopes</h3>
<p>An operator can grant a single sensitive capability on top of a role when minting a key (admin UI &rarr; Settings &rarr; API keys). The only one today is <code>payment_providers:write</code>, which lets a scoped key connect a BTCPay payment provider, but <em>only on a sandbox daemon and only for a non-mainnet network</em> (see <a href="#connect-btcpay">Connect BTCPay programmatically</a>). It belongs to no role by default (not even <code>full-admin</code>): a credential that can repoint where settlement lands is a fund-redirection key, so on a production daemon connecting a provider always stays master-only.</p>
<h2 id="discovery">Discovering the API</h2> <h2 id="discovery">Discovering the API</h2>
<p>Two complementary discovery mechanisms.</p> <p>Two complementary discovery mechanisms.</p>
@@ -248,6 +251,26 @@ curl -X POST $KS/v1/admin/subscriptions/$SUB_ID/cancel \
<p>Always applies as comp (no invoice) from the admin path. Buyer-initiated paid upgrades go through <code>/v1/upgrade</code> (different endpoint, signed-license auth).</p> <p>Always applies as comp (no invoice) from the admin path. Buyer-initiated paid upgrades go through <code>/v1/upgrade</code> (different endpoint, signed-license auth).</p>
<p><em>Scope required: <code>licenses:write</code>.</em></p> <p><em>Scope required: <code>licenses:write</code>.</em></p>
<h3 id="connect-btcpay">Connect BTCPay programmatically (sandbox)</h3>
<p>On a <strong>sandbox</strong> daemon (<code>KEYSAT_SANDBOX_MODE=1</code>), a scoped key carrying <code>payment_providers:write</code> can connect a BTCPay store over the API with no browser step, as long as the store settles on a <strong>non-mainnet</strong> network (regtest / testnet / signet). On a production daemon, or for a mainnet store, connect stays master-only. This is the path a delegated setup agent uses to stand up a disposable test instance end to end. You need a BTCPay API key for the target store (the operator's BTCPay access, delegated to you) carrying the same store and invoice permissions the browser flow grants (see <a href="install.html#connect-btcpay">Install &amp; setup</a>): the store-settings permissions complete the connect, and the invoice permissions let settled purchases issue licenses.</p>
<pre class="code"># 1. Start the connect. Returns a one-time `state` token + the BTCPay authorize URL.
curl -X POST $KS/v1/admin/btcpay/connect \
-H "Authorization: Bearer ks_..."
# -> { "authorize_url": "https://btcpay.example/api-keys/authorize?...", "state": "STATE", "merchant_profile_id": "..." }
# 2. Complete the connect by handing Keysat your BTCPay store API key, keyed by the
# `state` token (no Authorization header here: the single-use state token is the tie).
# A human approving in the browser at authorize_url reaches this same callback.
# Keysat resolves the store's network here and returns a 4xx if it is mainnet.
curl "$KS/v1/btcpay/authorize/callback?state=STATE&apiKey=BTCPAY_STORE_API_KEY"
# -> "BTCPay connected successfully." (HTTP 4xx with an error page if the gate refuses)
# 3. Confirm.
curl $KS/v1/admin/btcpay/status -H "Authorization: Bearer ks_..."
# -> { "connected": true, "store_id": "...", "base_url": "...", ... }</pre>
<p>Scope the BTCPay key to exactly the store you want to connect: Keysat attaches the first store the key can see. If the store's network cannot be confirmed as non-mainnet (mainnet, a Lightning-only store, or any detection failure), the callback fails closed with a 4xx and nothing is persisted. Disconnect (<code>POST /v1/admin/btcpay/disconnect</code>) is always master-only.</p>
<p><em>Scope required: <code>payment_providers:write</code>, on a sandbox daemon, for a non-mainnet store. The master key may connect any network on any daemon.</em></p>
<h2 id="worked-example">Worked example: gate an app behind a license</h2> <h2 id="worked-example">Worked example: gate an app behind a license</h2>
<p>End to end, with nothing but a <code>merchant-onboard</code> scoped key and the SDK: stand up a product, issue a license, and gate a feature in a Next.js app. No payment provider is needed for this path (you're issuing comp/dev licenses by hand; wiring BTCPay so buyers can self-checkout is covered in <a href="install.html">Install &amp; setup</a>). This is the minimal true path, nothing more.</p> <p>End to end, with nothing but a <code>merchant-onboard</code> scoped key and the SDK: stand up a product, issue a license, and gate a feature in a Next.js app. No payment provider is needed for this path (you're issuing comp/dev licenses by hand; wiring BTCPay so buyers can self-checkout is covered in <a href="install.html">Install &amp; setup</a>). This is the minimal true path, nothing more.</p>
@@ -377,13 +400,13 @@ def issue_comp_license(buyer_email: str, product_slug: str, reason: str) -> str:
<p>Some operations are deliberately operator-only and not accessible to any scoped key, including <code>full-admin</code>:</p> <p>Some operations are deliberately operator-only and not accessible to any scoped key, including <code>full-admin</code>:</p>
<ul> <ul>
<li>Generating / revoking scoped API keys (<code>/v1/admin/api-keys</code>)</li> <li>Generating / revoking scoped API keys (<code>/v1/admin/api-keys</code>)</li>
<li>Connecting / disconnecting payment providers</li> <li>Disconnecting payment providers, and connecting a provider on a production daemon. (A scoped key with <code>payment_providers:write</code> may connect a <em>non-mainnet</em> provider on a <em>sandbox</em> daemon only; see <a href="#connect-btcpay">Connect BTCPay programmatically</a>.)</li>
<li>Setting the operator name</li> <li>Setting the operator name</li>
<li>Activating the self-license (<code>/v1/admin/self-license</code>)</li> <li>Activating the self-license (<code>/v1/admin/self-license</code>)</li>
<li>Resetting the analytics install_uuid</li> <li>Resetting the analytics install_uuid</li>
<li>Changing the web UI password (StartOS Action only)</li> <li>Changing the web UI password (StartOS Action only)</li>
</ul> </ul>
<p>These all require the master <code>KEYSAT_ADMIN_API_KEY</code>. The reasoning: an agent that can rotate its own credentials, connect arbitrary payment processors, or change the operator identity is no longer bounded by the role it was given.</p> <p>These require the master <code>KEYSAT_ADMIN_API_KEY</code>. The reasoning: an agent that can rotate its own credentials, repoint settlement to an arbitrary wallet, or change the operator identity is no longer bounded by the role it was given. The one deliberate carve-out is sandbox payment-provider connect (above): bounded to a sandbox daemon and a non-mainnet network, it lets a delegated agent stand up a disposable test instance end to end without ever touching mainnet funds or the master key.</p>
<h2 id="feedback">Help us improve this guide</h2> <h2 id="feedback">Help us improve this guide</h2>
<p>The OpenAPI spec is the source of truth for the API surface. This guide is a hand-curated overlay focused on the workflows we've seen agents actually need. If you're building something the spec covers but this guide doesn't make obvious, open an issue at <a href="https://github.com/keysat-xyz/keysat">github.com/keysat-xyz/keysat</a> with the workflow shape and we'll add it.</p> <p>The OpenAPI spec is the source of truth for the API surface. This guide is a hand-curated overlay focused on the workflows we've seen agents actually need. If you're building something the spec covers but this guide doesn't make obvious, open an issue at <a href="https://github.com/keysat-xyz/keysat">github.com/keysat-xyz/keysat</a> with the workflow shape and we'll add it.</p>
+5 -1
View File
@@ -88,9 +88,11 @@
<p>In Keysat&rsquo;s service page, click <strong>Actions &rarr; Connect BTCPay</strong>. You&rsquo;ll be redirected to BTCPay&rsquo;s authorize page, where you grant Keysat the permissions it needs:</p> <p>In Keysat&rsquo;s service page, click <strong>Actions &rarr; Connect BTCPay</strong>. You&rsquo;ll be redirected to BTCPay&rsquo;s authorize page, where you grant Keysat the permissions it needs:</p>
<ul> <ul>
<li><code>btcpay.store.canviewstoresettings</code></li>
<li><code>btcpay.store.canmodifystoresettings</code> (to register the settle webhook)</li>
<li><code>btcpay.store.canviewinvoices</code></li> <li><code>btcpay.store.canviewinvoices</code></li>
<li><code>btcpay.store.cancreateinvoice</code></li> <li><code>btcpay.store.cancreateinvoice</code></li>
<li><code>btcpay.store.canmodifywebhooks</code></li> <li><code>btcpay.store.canmodifyinvoices</code></li>
</ul> </ul>
<p>Once you confirm, BTCPay redirects back to Keysat with an API key and store id. Keysat:</p> <p>Once you confirm, BTCPay redirects back to Keysat with an API key and store id. Keysat:</p>
@@ -105,6 +107,8 @@
<p><strong>Connect is idempotent.</strong> If you click it again later, Keysat detects the existing connection and returns success without re-authorizing. To force a re-authorize, run the <strong>Disconnect BTCPay</strong> action first.</p> <p><strong>Connect is idempotent.</strong> If you click it again later, Keysat detects the existing connection and returns success without re-authorizing. To force a re-authorize, run the <strong>Disconnect BTCPay</strong> action first.</p>
</div> </div>
<p>Automating setup? On a <strong>sandbox</strong> daemon you can connect a non-mainnet BTCPay over the API instead of clicking, using a scoped key carrying the <code>payment_providers:write</code> scope. See <a href="agent.html#connect-btcpay">Agent integration: Connect BTCPay programmatically</a>.</p>
<p>Click <strong>Actions &rarr; Check BTCPay connection</strong> to verify the wiring. It should report:</p> <p>Click <strong>Actions &rarr; Check BTCPay connection</strong> to verify the wiring. It should report:</p>
<pre class="code"><span class="c"># Expected output:</span> <pre class="code"><span class="c"># Expected output:</span>