Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions cmd/obol/sell.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func sellInferenceCommand(cfg *config.Config) *cli.Command {
Usage: "Sell local model inference with x402 payments",
ArgsUsage: "<name>",
Description: `Starts an x402-gated reverse proxy in front of a local Ollama instance.
Buyers pay per-request in USDC to access inference endpoints.
Buyers pay per-request in the selected x402 token to access inference endpoints.

Examples:
obol sell inference my-qwen --model qwen3.5:4b --pay-to 0x... --price 0.001
Expand All @@ -102,7 +102,7 @@ Examples:
Name: "model",
Usage: "Model name to serve (e.g. qwen3.5:4b)",
},
payToFlag("USDC recipient address"),
payToFlag("Payment recipient address"),
&cli.StringFlag{
Name: "price",
Usage: "Per-request price (alias for --per-request)",
Expand All @@ -113,7 +113,7 @@ Examples:
},
&cli.StringFlag{
Name: "per-mtok",
Usage: "Per-million-tokens price in USDC (charged as an approximation at 1000 tok/request)",
Usage: "Per-million-tokens price in the selected payment token (charged as an approximation at 1000 tok/request)",
},
&cli.StringFlag{
Name: "chain",
Expand Down Expand Up @@ -240,7 +240,7 @@ Examples:
u.Infof("Using wallet from remote-signer: %s", wallet)
} else if u.IsTTY() {
var inputErr error
wallet, inputErr = u.Input("Wallet address (USDC recipient)", "")
wallet, inputErr = u.Input("Wallet address (payment recipient)", "")
if inputErr != nil || wallet == "" {
return fmt.Errorf("recipient required: use --pay-to <addr> or set X402_WALLET")
}
Expand Down Expand Up @@ -518,7 +518,7 @@ Examples:
obol sell http my-cool-api --upstream my-svc.my-namespace.svc.cluster.local --port 8080 --pay-to 0x... --price 0.01 --chain base
obol sell http my-cool-api --upstream my-svc --port 8080 --pay-to 0x... --price 0.01 --chain base --no-register`,
Flags: []cli.Flag{
payToFlag("USDC recipient address"),
payToFlag("Payment recipient address"),
&cli.StringFlag{
Name: "chain",
Usage: "Payment chain (base, base-sepolia, ethereum)",
Expand All @@ -531,19 +531,19 @@ Examples:
},
&cli.StringFlag{
Name: "price",
Usage: "Per-request price in USDC (e.g. 0.001)",
Usage: "Per-request price in the selected payment token (e.g. 0.001 USDC or 1 OBOL)",
},
&cli.StringFlag{
Name: "per-request",
Usage: "Per-request price in USDC (alias for --price)",
Usage: "Per-request price in the selected payment token (alias for --price)",
},
&cli.StringFlag{
Name: "per-mtok",
Usage: "Per-million-tokens price in USDC (charged as an approximation at 1000 tok/request)",
Usage: "Per-million-tokens price in the selected payment token (charged as an approximation at 1000 tok/request)",
},
&cli.StringFlag{
Name: "per-hour",
Usage: "Per-compute-hour price in USDC",
Usage: "Per-compute-hour price in the selected payment token",
},
&cli.StringFlag{
Name: "namespace",
Expand Down Expand Up @@ -689,7 +689,7 @@ Examples:
u.Infof("Using wallet from remote-signer: %s", wallet)
} else if u.IsTTY() {
var inputErr error
wallet, inputErr = u.Input("Wallet address (USDC recipient)", "")
wallet, inputErr = u.Input("Wallet address (payment recipient)", "")
if inputErr != nil || wallet == "" {
return fmt.Errorf("recipient required: use --pay-to <addr> or set X402_WALLET")
}
Expand Down Expand Up @@ -2397,26 +2397,26 @@ Examples:
Usage: "Namespace of the ServiceOffer",
Required: true,
},
payToFlag("New USDC recipient address"),
payToFlag("New payment recipient address"),
&cli.StringFlag{
Name: "chain",
Usage: "New payment chain (base, base-sepolia, ethereum)",
},
&cli.StringFlag{
Name: "price",
Usage: "New per-request price in USDC (alias for --per-request)",
Usage: "New per-request price in the selected payment token (alias for --per-request)",
},
&cli.StringFlag{
Name: "per-request",
Usage: "New per-request price in USDC",
Usage: "New per-request price in the selected payment token",
},
&cli.StringFlag{
Name: "per-mtok",
Usage: "New per-million-tokens price in USDC",
Usage: "New per-million-tokens price in the selected payment token",
},
&cli.StringFlag{
Name: "per-hour",
Usage: "New per-compute-hour price in USDC",
Usage: "New per-compute-hour price in the selected payment token",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
Expand Down Expand Up @@ -2576,7 +2576,7 @@ func sellPricingCommand(cfg *config.Config) *cli.Command {
Description: `Sets the recipient address and chain for x402 payment collection.
Reloads the payment verifier when configuration is changed.`,
Flags: []cli.Flag{
payToFlag("USDC recipient address"),
payToFlag("Payment recipient address"),
&cli.StringFlag{
Name: "chain",
Usage: "Payment chain (base, base-sepolia, ethereum)",
Expand Down
25 changes: 18 additions & 7 deletions docs/guides/monetize-inference.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,17 @@ obol sell http my-qwen \

When `--token OBOL` is used without `--chain`, the chain defaults to `ethereum`.

That stores both values in the pricing config:
For the USDC examples above, the pricing config stores both values:

- source model: `perMTok = 1.25 USDC / 1M tokens`
- enforced phase-1 charge: `price = 0.00125 USDC / request`
- approximation input: `approxTokensPerRequest = 1000`

For OBOL release smokes, verify the generated payment config carries the selected
token metadata end to end instead of silently falling back to USDC wording or
USDC's ERC-3009 domain. The 402 requirement should expose the OBOL token address,
atomic OBOL amount, and Permit2/EIP-712 metadata needed by the buyer sidecar.

The stack now treats on-chain registration as part of the default selling flow:

```
Expand Down Expand Up @@ -284,7 +289,10 @@ A **402 Payment Required** response confirms the x402 gate is working. The respo
}
```

The `amount` is in USDC micro-units (6 decimals): `1000` = 0.001 USDC.
For this USDC example, `amount` is in USDC micro-units (6 decimals):
`1000` = 0.001 USDC. For an OBOL/Permit2 route, `asset` should be the OBOL
token address, `amount` should be atomic OBOL units, and `extra` should carry
the token/Permit2 metadata the buyer uses for signing.

### 1.7 Monitoring

Expand Down Expand Up @@ -356,11 +364,12 @@ The SDK handles the full x402 flow:

1. Sends the request
2. Receives 402 with payment requirements
3. Signs an EIP-712 `TransferWithAuthorization` message (ERC-3009)
3. Signs the token-specific EIP-712 payment payload: ERC-3009 for USDC or
Permit2 for OBOL routes
4. Retries with the `X-PAYMENT` header (base64-encoded x402 envelope)
5. The seller-owned x402 gateway verifies the payment with the facilitator
6. The seller gateway forwards the request to the protected upstream
7. After upstream success, the seller gateway settles USDC on-chain
7. After upstream success, the seller gateway settles the selected token on-chain
8. Returns the inference response

**Manual flow with curl** -- for debugging or custom integrations:
Expand All @@ -373,8 +382,9 @@ curl -s -X POST "$TUNNEL_URL/services/my-qwen/v1/chat/completions" \

# Step 2: Sign the EIP-712 payment (requires SDK or custom code)
# The 402 body contains: payTo, amount, asset, network, extra.name, extra.version
# Sign a TransferWithAuthorization (ERC-3009) message with:
# Domain: {name: "USDC", version: "2", chainId: 84532, verifyingContract: <USDC address>}
# USDC routes sign TransferWithAuthorization (ERC-3009) against the USDC domain.
# OBOL routes sign the configured Permit2 payload against the OBOL/token metadata
# published in the payment requirement.

# Step 3: Retry with payment header
curl -s -X POST "$TUNNEL_URL/services/my-qwen/v1/chat/completions" \
Expand All @@ -386,7 +396,8 @@ curl -s -X POST "$TUNNEL_URL/services/my-qwen/v1/chat/completions" \

### 2.4 Verify Payment Settlement

After a successful paid request, verify the USDC transfer on-chain using Foundry's `cast`:
After a successful paid request, verify the selected token transfer on-chain using
Foundry's `cast`. For the default USDC example:

```bash
USDC=0x036CbD53842c5426634e7929541eC2318f3dCF7e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ spec:
type: string
price:
type: string
description: "Micro-USDC per request"
description: "Atomic token units per request"
asset:
type: string
description: "ERC-20 contract address"
Expand Down
Loading