Skip to content

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.

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-Account header 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 stripe SDK; every other consumer talks to a narrow StripeClient interface. 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.

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.

#GateFailure behavior
1Structural authorization (re-read the audit row; assert decision='approved' and chain_class='transactional')Abort, no Stripe call.
2Two-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.
3Tenancy (resolved Stripe credential’s account_id MUST equal the mandate’s account_id)Abort with stripe_account_inactive and a critical log.
4Per-account rate limit (trailing-60s attempt count under the account ceiling)NACK; queue retries with backoff.
5Idempotency (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.

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 livemode boolean from Stripe’s token-exchange response. This is the authoritative test-vs-live discriminator and is asserted against the account’s resolved stripe_mode on 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.

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)”
Terminal window
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.

{
"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.

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 statusSill outcomeAudit record
succeededsettled_succeededdecision=approved, settlement evidence with stripe_payment_intent_id + stripe_charge_id.
processingpending_webhookReconciler waits for payment_intent.succeeded or payment_intent.payment_failed.
requires_actionsettled_failed (authentication_required)The off-session charge needs SCA / further customer action — rejected for the agent flow.
requires_payment_methodsettled_failed (card_declined or similar)The payment method failed; the merchant or buyer must intervene.
canceledsettled_failedCharge 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.

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.

EventWhat Sill does
payment_intent.succeededFlip charge_state.pending_webhooksettled_succeeded; append audit.
payment_intent.payment_failedFlip → settled_failed with a bounded reason; append audit.
charge.refundedRecord refund evidence (refunded_minor cumulative) against the original mandate. See Refunds.
charge.dispute.createdRecord dispute evidence (dispute_reason, disputed_minor) against the original mandate.
account.application.deauthorizedRevoke 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.

Stripe’s test and live environments are independent at every layer Sill touches.

  • Account mode. Each Sill account has a single stripe_modetest or live. 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_KEY or STRIPE_LIVE_SECRET_KEY). Missing the secret aborts the charge with credential_missing before 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.

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 stored pm_* token + owning customer.

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.

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.

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.

No. The amount is derived from the signed mandateintent.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.

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.

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.