Payments
Payments is the side-effect step of the Sill Transactional pipeline. When the policy engine returns approve (or a human reviewer approves an escalation), Sill authorizes a charge against the merchant’s connected Stripe account using an opaque, merchant-saved payment-method token. Sill never custodies funds. Stripe holds the card, authorizes the charge, settles to the merchant’s connected account, and pays out. Sill issues the signed authorization and the signed audit record for the attempt — nothing else.
The architectural invariant
Section titled “The architectural invariant”Sill is not a payments processor, money transmitter, or PCI-certified entity. The cardinal invariant of the rail:
- No card data. Sill handles only opaque processor tokens — Stripe
pm_*PaymentMethod IDs and (on the ACP roadmap) Shared Payment Tokens. Raw PANs never enter any Sill system, enforced by a CI grep gate against PAN-shaped patterns. - No funds custody. The charge runs on the merchant’s connected Stripe account via the
Stripe-Accountheader on every API call. Settlement and payout land in the merchant’s bank account directly from Stripe; no funds enter a Sill-controlled account at any step. - One Stripe SDK boundary. Exactly one file in the Sill codebase imports the runtime
stripeSDK; every other consumer talks to a narrowStripeClientinterface. This bounds the supply-chain surface and makes future SDK upgrades a single, contained change.
These properties are architectural, not promotional. They are what lets Sill sit in front of Stripe without changing Stripe’s regulatory perimeter.
Pipeline shape
Section titled “Pipeline shape”Payments runs after the signed mandate is verified, the policy engine returns approve, and the audit row is committed with decision='approved'. The rail then performs a small, ordered sequence of gates before any external call.
sequenceDiagram
autonumber
participant Edge as Sill edge (mandate + policy)
participant Rail as Sill rail executor
participant DB as Sill origin
participant Stripe as Stripe (merchant connected acct)
participant Audit as Audit envelope
Edge->>DB: INSERT audit_record (decision=approved)
Edge->>Rail: Dispatch approved draft
Rail->>DB: Re-read audit row (gate 1: structural authorization)
Rail->>DB: Resolve site.stripe_rail_enabled + account.stripe_mode (gate 2)
Rail->>DB: Resolve merchant Stripe credential (gate 3: tenancy)
Rail->>DB: Rate-limit window check (gate 4)
Rail->>DB: INSERT charge_state ON CONFLICT DO NOTHING (gate 5: layer B idempotency)
Rail->>DB: Decrypt OAuth token (AAD-bound)
Rail->>Stripe: POST /v1/payment_intents (Stripe-Account header + Idempotency-Key)
Stripe-->>Rail: PaymentIntent (succeeded | requires_action | processing | failed)
Rail->>DB: charge_state.settled_succeeded | settled_failed | pending_webhook
Rail->>Audit: Signed, Merkle-chained settlement record
The five gates are deliberate and ordered. They fail closed: any gate that cannot answer aborts the charge with zero Stripe egress.
| # | Gate | Failure behavior |
|---|---|---|
| 1 | Structural authorization (re-read the audit row; assert decision='approved' and chain_class='transactional') | Abort, no Stripe call. |
| 2 | Two-flag rail gate (site.stripe_rail_enabled true AND account.stripe_mode resolved; live mode additionally requires the deploy-pipeline flag) | Abort with approved_but_rail_disabled or credential_missing. |
| 3 | Tenancy (resolved Stripe credential’s account_id MUST equal the mandate’s account_id) | Abort with stripe_account_inactive and a critical log. |
| 4 | Per-account rate limit (trailing-60s attempt count under the account ceiling) | NACK; queue retries with backoff. |
| 5 | Idempotency (layer B: charge_state UNIQUE(site_id, mandate_id) INSERT; layer C: deterministic Stripe Idempotency-Key) | Re-serve the prior outcome; no duplicate charge. |
Only after all five gates pass does the rail decrypt the merchant’s OAuth access token and issue the paymentIntents.create call.
The merchant Stripe connection
Section titled “The merchant Stripe connection”A site connects to Stripe via the standard Stripe Connect OAuth flow. The result, after redirect and token exchange:
- An
access_token(the merchant’s API credential), encrypted at rest with envelope encryption, cryptographically bound (AAD) to(account_id, site_id, kind, credential_id). - A
stripe_user_id(the connected account,acct_…) stored on the integration row. - A
livemodeboolean from Stripe’s token-exchange response. This is the authoritative test-vs-live discriminator and is asserted against the account’s resolvedstripe_modeon every charge — a mode mismatch fails the call closed.
Every Stripe API call sets Stripe-Account: <acct_…> on a per-call basis. A single Sill StripeClient instance can serve many merchants safely; the connected-account header is never on the global client config. If the header is ever missing at the SDK boundary, the client throws before any network egress — a defense-in-depth invariant to prevent a charge from ever landing on Sill’s platform account.
What the charge looks like
Section titled “What the charge looks like”The rail builds a PaymentIntent from the signed intent — amount and currency are derived from intent.quoted_total (or, for single-SKU intents, intent.max_amount), never asserted by the agent at call time. Buyer fields (receipt_email, shipping) ride to Stripe via the proper PaymentIntent top-level fields, not metadata. Metadata carries identifiers only (mandate, site, audit record, mode) and is PII-free by invariant.
Example: standard off-session charge (test mode)
Section titled “Example: standard off-session charge (test mode)”curl https://api.stripe.com/v1/payment_intents \ -u sk_test_…: \ -H "Stripe-Account: acct_TEST_MERCHANT" \ -H "Idempotency-Key: sill_mnd_v1_mnd_01J9XYZABCDEFGHJKMNPQRSTVW_place_order" \ -d "amount=4999" \ -d "currency=usd" \ -d "payment_method=pm_card_visa" \ -d "customer=cus_TEST_CUSTOMER" \ -d "off_session=true" \ -d "confirm=true" \ -d "metadata[mandate_id]=mnd_01J9XYZABCDEFGHJKMNPQRSTVW" \ -d "metadata[site_id]=01J9XYZACCOUNTSITEEXAMPLEAB" \ -d "metadata[sill_record_id]=rec_01J9XYZAUDITRECORDEXAMPLEX" \ -d "metadata[sill_environment]=test" \ -d "description=Sill mandate: example-merchant sku=ACME-WIDGET-42"The Idempotency-Key is deterministic per (mandate_id, attempt). A Sill-side retry, a Stripe-side network retry, or a redelivered queue message all collide on the same key — within Stripe’s 24-hour idempotency window, Stripe returns the original PaymentIntent rather than creating a second one. Layer B and layer C compose; the rail cannot double-charge a mandate even under arbitrary process or network failure.
Example: succeeded response (shape)
Section titled “Example: succeeded response (shape)”{ "id": "pi_3Ti4JaEAXJFotMa31abcDEFG", "object": "payment_intent", "amount": 4999, "currency": "usd", "status": "succeeded", "latest_charge": "ch_3Ti4JaEAXJFotMa31abcDEFG", "customer": "cus_TEST_CUSTOMER", "payment_method": "pm_card_visa", "livemode": false, "metadata": { "mandate_id": "mnd_01J9XYZABCDEFGHJKMNPQRSTVW", "site_id": "01J9XYZACCOUNTSITEEXAMPLEAB", "sill_record_id": "rec_01J9XYZAUDITRECORDEXAMPLEX", "sill_environment": "test" }}Identifier values above are illustrative shapes; real PaymentIntent IDs, charge IDs, and customer IDs are minted by Stripe.
PaymentIntent status routing
Section titled “PaymentIntent status routing”Sill maps Stripe’s PaymentIntent.status to a small set of audit-visible outcomes. The translation runs at the SDK boundary so the rail and audit code never see raw Stripe error bytes.
| Stripe status | Sill outcome | Audit record |
|---|---|---|
succeeded | settled_succeeded | decision=approved, settlement evidence with stripe_payment_intent_id + stripe_charge_id. |
processing | pending_webhook | Reconciler waits for payment_intent.succeeded or payment_intent.payment_failed. |
requires_action | settled_failed (authentication_required) | The off-session charge needs SCA / further customer action — rejected for the agent flow. |
requires_payment_method | settled_failed (card_declined or similar) | The payment method failed; the merchant or buyer must intervene. |
canceled | settled_failed | Charge canceled before capture. |
Every outcome — success, failure, abort — is recorded with the same shape: a signed audit record carrying the settlement evidence in an unsigned discovery_context.settlement JSONB sub-object. The settlement evidence is deliberately not part of the signed scope, preserving the signing-scope invariant; the signed surface is the decision, the mandate, and the policy outcome.
Webhooks
Section titled “Webhooks”Sill subscribes to a small set of Stripe webhook events on the platform account. The receiver verifies the Stripe-Signature header against the configured webhook secret (test or live, selected by the inbound event’s livemode flag), maps the event to a charge_state row by (stripe_payment_intent_id, stripe_charge_id), and writes the outcome to the audit envelope.
| Event | What Sill does |
|---|---|
payment_intent.succeeded | Flip charge_state.pending_webhook → settled_succeeded; append audit. |
payment_intent.payment_failed | Flip → settled_failed with a bounded reason; append audit. |
charge.refunded | Record refund evidence (refunded_minor cumulative) against the original mandate. See Refunds. |
charge.dispute.created | Record dispute evidence (dispute_reason, disputed_minor) against the original mandate. |
account.application.deauthorized | Revoke the Stripe credential for the affected connected account; future charges abort at gate 3. |
Sill is fail-closed on webhook signature verification: an unverified event is rejected with HTTP 400 and never reaches the dispatcher.
Test mode versus live mode
Section titled “Test mode versus live mode”Stripe’s test and live environments are independent at every layer Sill touches.
- Account mode. Each Sill account has a single
stripe_mode—testorlive. Set by the founder; not agent-controlled. - Per-charge credential. The merchant’s decrypted OAuth access token is environment-correct by construction (issued by the matching Stripe Connect app at OAuth time).
- Platform secret. The rail also requires a non-empty platform secret for the resolved mode (
STRIPE_TEST_SECRET_KEYorSTRIPE_LIVE_SECRET_KEY). Missing the secret aborts the charge withcredential_missingbefore any Stripe call. This is defense in depth on top of the deploy-pipeline live-mode gate. - Deploy-pipeline live gate. Live mode additionally requires the deploy-pipeline flag (
SILL_STRIPE_LIVE_GATE_PASSED). Without it, every live-mode mandate aborts. This was the controlled cut-over for the limited production live-rail validation. - Webhook secret selection. Inbound webhook events carry
livemode; the receiver selects the test or live webhook secret accordingly. A test event verified with the live secret (or vice versa) is rejected.
Today versus roadmap
Section titled “Today versus roadmap”What is live on the Payments rail today:
- Stripe live rail — operational in limited production. The signed mandate → policy → Stripe authorize → signed audit path has cleared and refunded real live-mode Stripe charges on a single Sill-controlled merchant.
- Stripe test rail — operational. The same pipeline runs unchanged on Stripe test mode; the only difference is the gate selection and the credential.
- Charge path: merchant-saved
PaymentMethod. The rail charges an off-session PaymentIntent against the merchant’s storedpm_*token + owningcustomer.
On the roadmap, not live:
- Live Shopify settlement. The Shopify connector is test-mode only; the live-rail gate for Shopify is a separate flip and has not been flipped. Live Shopify settlement requires its own founder/ops decision and external prerequisites.
- The Agentic Commerce Protocol (ACP) Shared Payment Tokens via Stripe. The charge branch is built but dormant behind a feature flag. The roadmap path is “Sill governs on top of Stripe agentic commerce” — Stripe owns SPT minting; Sill governs the redemption.
- x402 on-chain payments. Deferred.
Phase 2 remains in early access in limited production. Marketing or sales claims that go beyond what this page describes should be confirmed with Sill before publication.
Frequently asked
Section titled “Frequently asked”Does Sill ever hold the card or the money?
Section titled “Does Sill ever hold the card or the money?”No. Stripe holds the card, authorizes the charge against the merchant’s connected account, settles to that account, and pays out. Sill issues the signed authorization and the audit record. No funds enter a Sill-controlled account at any step. Sill handles only opaque processor tokens (Stripe pm_*, and Shared Payment Tokens on the roadmap); raw card numbers never enter any Sill system.
Whose Stripe account is the charge on?
Section titled “Whose Stripe account is the charge on?”The merchant’s connected Stripe account (acct_…), resolved at OAuth-install time and pinned per call via the Stripe-Account header. Sill operates as the Connect platform but does not move funds across its own balance — every charge lands in the merchant’s balance directly.
Can the agent choose the amount?
Section titled “Can the agent choose the amount?”No. The amount is derived from the signed mandate — intent.quoted_total for multi-line carts, intent.max_amount for single-SKU intents. The agent cannot assert a charge amount at call time; the rail’s deriveChargeAmount helper is the only source of the PaymentIntent.amount value. A multi-line mandate that omits quoted_total fails closed with quoted_total_required, on the grounds that the ceiling is not a price.
What if Stripe times out or returns a transient error?
Section titled “What if Stripe times out or returns a transient error?”The Stripe SDK retries transient 5xx and connection errors up to twice. Because every request carries a deterministic Idempotency-Key, Stripe’s idempotency contract guarantees a retried request returns the original PaymentIntent rather than creating a second one. If the call still fails, the rail records settled_failed (or pending_webhook for a processing outcome) and the audit envelope captures the bounded error class.
How are refunds handled?
Section titled “How are refunds handled?”Refunds run back through the same signed-mandate path. Sill’s webhook handlers cover charge.refunded and charge.dispute.created, and the refund or dispute outcome is appended to the audit envelope alongside the original mandate. See Refunds.
Is Sill PCI-certified?
Section titled “Is Sill PCI-certified?”No. Sill’s PCI scope is architecturally minimal because raw PANs never enter any Sill system (enforced by a CI grep gate against PAN-shaped patterns plus the architectural invariant that the rail handles only opaque processor tokens). “Architecturally minimal scope” is not a PCI certification claim. Sill holds no PCI attestation, ROC, or SAQ. See Compliance.
Can I enable Payments for my Stripe account today?
Section titled “Can I enable Payments for my Stripe account today?”Transactional is currently in early access, validated in limited production on the live Stripe rail. For evaluation access, contact the founder through the email on sill.so. Discovery is free, unlimited, and can be installed without any Transactional access.
See also
Section titled “See also”- Transactional overview — pipeline scope and honest bounds
- Signed mandates — the central object
- Policy engine — how rules drive approve/escalate/reject
- Human in the loop — escalation review surface
- Refunds — the refund mandate shape and webhook handlers
- Audit envelope — signed Merkle-chained record
- Verify a signature — verifier recipe
- Public JWKS — Sill’s published ed25519 key
- Protocols reference — A2A, AP2, MCP, and the roadmap
- Security overview — fail-closed posture and threat model
- Compliance — framework mappings, PCI posture
- External: Stripe PaymentIntents API, Stripe Connect, Stripe idempotent requests, Stripe Connect OAuth, RFC 8032 EdDSA